نمط الجسر

من موسوعة حسوب
اذهب إلى التنقل اذهب إلى البحث

نمط الجسر هو نمط تصميم هيكلي يسمح لك بتقسيم فئة كبيرة أو مجموعة فئات مرتبطة ببعضها إلى تشكيلين هرميين منفصلين -نظري وتطبيقي-، ومن ثم يمكن تطويرهما بشكل مستقل عن بعضهما.

المشكلة

لنقل أن لديك فئة هندسية اسمها shape، وتلك الفئة لها زوج من الفئات الفرعية هما circle وsquare، وتريد توسيع هرمية تلك الفئة لتضيف الألوان على فئات تلك الأشكال الهندسية، فسيكون الحل التقليدي هنا أن تنشئ فئتين فرعيتين لفئة shape هما Red و Blue مثلًا.

لكن بما أن لديك فئتين فرعيتين من البداية، فستحتاج إلى إنشاء أربع تجميعات لتحتوي الاحتمالات الممكنة للأشكال وألوانها التي قد تأخذها، مثل BlueCircle و RedSquare. انظر (ش.1)

ضع الصورة ، يزيد عدد تجميعات الفئات مع إضافة المزيد من الأشكال الهندسية.

سيكبر حجم الهرمية التي لدينا مع إضافة أشكال وألوان جديدة، فمن أجل إضافة مثلث على سبيل المثال سنضيف فئتين فرعيتين، واحدة لكل لون، وإضافة لون جديد سيتطلب إنشاء ثلاث فئات فرعية، واحدة لكل شكل، وهكذا يسوء الوضع كلما تطور الأمر وزادت المدخلات.

الحل

تحدث هذه المشكلة بسبب أننا نحاول توسيع فئات الشكل في بعديْن مستقليْن هما الهيئة (form) واللون، وهذه الحالة شائعة مع اكتساب الفئات (classes inheritance).

ويحاول نمط الجسر حل تلك المشكلة باستخدام أسلوب التركيب بدلًا من الاكتساب، وهذا يعني أنك تستخرج أحد الأبعاد إلى هرمية فئوية منفصلة كي تشير الفئات الأصلية إلى كائن من الهرمية الجديدة بدلًا من احتواء سلوكه وحالاته في فئة واحدة.

ضع الصورة. يمكنك تجنب انفجار هرمية فئة ما بتحويلها إلى هرميات متعددة مرتبطة ببعضها.

واتباع هذا المنظور يمكّننا من استخراج الشيفرة المتعلقة باللون إلى فئتها الخاصة مع فئتين فرعيتين هما Red و Blue، وعندها تحصل فئة Shape على حقل مرجعي (reference field) يشير إلى أحد كائنات الألوان. ويمكن لفئة Shape الآن أن تفوض أي عمل يتعلق بالألوان إلى فئتي Shape و Color. ومن هنا لن تتطلب إضافة ألوان جديدة تغييرَ هرمية الشكل، والعكس صحيح.

التجريد والتطبيق (Abstraction and Implementation)

يقدم كتاب عصابة الأربعة -إشارة إلى المؤلفين الأربعة لكتاب أنماط التصميم، عناصر البرمجيات كائنية التوجه القابلة لإعادة الاستخدام-، يقدم مصطلحات جديدة مثل التجريد والتطبيق كجزء من تعريف نمط الجسر، ويراها البعض على أنها تضفي طابعًا أكاديميًا على النمط وتجعله يبدو أكثر تعقيدًا مما هو عليه.

والتجريد (والذي هو الواجهة -interface- أيضًا) هو طبقة تحكم عالية المستوى لبعض الكيانات، ولا يفترض بتلك الطبقة أن تنفذ أي عمل حقيقي من تلقاء نفسها، بل تفوض الأعمال إلى طبقة التطبيق (والتي يطلق عليها المنصة أيضًا).

لاحظ أننا لا نتكلم عن الواجهات (interfaces) أو الفئات المجردة (abstract classes) في لغتك البرمجية، فتلك أمور مختلفة عما نتحدث عنه هنا، ذلك أنه عند الحديث عن التطبيقات الحقيقية فإن التجريد يمكن تمثيله بواجهة مستخدم رسومية GUIـ أما التطبيق فقد يكون أي شيفرة نظام تشغيل (API) تستدعيها طبقة الواجهة الرسومية كاستجابة لتفاعلات المستخدم.

وعمومًا يمكنك توسيع مثل ذلك التطبيق في اتجاهين منفصلين:

توفير عدة واجهات رسومية (مثلًا، واجهات مخصصة للعملاء العاديين أو مخصصة للمدراء).

دعم عدة واجهات لبرمجة تطبيقات (APIs) كأن يكون قادرًا على إطلاق التطبيق في ويندوز ولينكس وماك.

وقد يبدو هذا التطبيق في أسوأ حالة له مثل وعاء كبير مليء بالاسباجيتي، تنتشر فيه مئات من العبارات الشرطية التي تصل أنواعًا مختلفة من الواجهات الرسومية مع واجهات برمجة تطبيقات مختلفة.

ضع الصورة. يصعب إجراء أي تغيير ولو بسيط في الشيفرة الأحادية لحاجتك إلى فهم التطبيق ككل، على عكس تقسيم التطبيق إلى وحدات أصغر ثم تطبيق التغيير على الأجزاء التي تحتاج تغييرًا فقط.

تستطيع وضع نظام لتلك الفوضى باستخراج الشيفرة المتعلقة بتجميعات بعينها للواجهة-المنصة إلى فئات مستقلة منفصلة، لكن ستكتشف قريبًا أن هناك الكثير من تلك الفئات. ستنمو هرمية الفئة كثيرًا لأن إضافة واجهة رسومية جديدة أو دعم واجهة برمجة تطبيقات (API) مختلفة سيتطلب إنشاء فئات أكثر.

سنحاول حل هذه المشكلة باستخدام نمط الجسر الذي يقترح تقسيم الفئات إلى هرميتين مختلفتين:

التجريد (Abstraction): الواجهة الرسومية للتطبيق (App).

التطبيق (Implementation): واجهات برمجة التطبيقات (APIs) الخاصة بنظام التشغيل.

ضع الصورة. إحدى الطرق لهيكلة تطبيق متعدد المنصات.

يتحكم كائن التجريد (abstraction object) في مظهر التطبيق ويفوض المهام الفعلية إلى كائن التطبيق (implementation object) المربوط به، وتكون التطبيقات (implementations) المختلفة تبادلية طالما تتبع واجهة مشتركة، مما يجعل الواجهة الرسومية تعمل في ويندوز ولينكس معًا.

البُنية

ضع الصورة.

  1. يوفر التجريد (Abstraction) منطق تحكم عالي المستوى، إذ يعتمد على كائن التطبيق (implementation object) للقيام بالمهام الفعلية منخفضة المستوى.
  2. يصرح التطبيق (Implementation) عن الواجهة الشائعة لكل التطبيقات الحقيقية (Concrete Implementations)، ويتواصل أي تجريد مع كائن تطبيق من خلال الأساليب التي صُرِّح بها هنا فقط. وقد يسرد التجريد نفس الأساليب أيضًا لكنه عادة يصرح عن بعض السلوكيات المعقدة التي تعتمد على صور مختلفة من العمليات الأولية التي يصرح عنها التطبيق (implementation).
  3. تحتوي التطبيقات الحقيقية (Concrete Implementations) على شيفرات خاصة بالمنصات الموجه إليها البرنامج (Platform-specific).
  4. التجريدات المنقحة (refined abstractions) توفر صورًا مختلفة من منطق التحكم (control logic)، وتعمل مع التطبيقات (implementations) المختلفة من خلال واجهة تطبيق عامة (general implementation interface).
  5. عادة لا يهتم العميل إلا بالعمل مع التجريد، لكن وظيفته على أي حال هي ربط كائن التجريد مع أحد كائنات التطبيق.

مثال توضيحي

يشرح هذا المثال كيف يُستخدم نمط الجسر لتقسيم الشيفرة الأحادية لبرنامج يدير بعض الأجهزة الإلكترونية وأجهزة التحكم فيها عن بعد (remote controls)، ويمثل التطبيق هنا فئات Device، بينما يمثل التجريد فئات Remote.

ضع الصورة. الهرمية الأصلية للفئة مقسمة إلى جزئين، أجهزة ومتحكمات عن بعد.

تصرح فئة جهاز التحكم عن بعد الرئيسية عن حقل مرجعي يربطها بكائن جهاز إلكتروني (device object)، وتعمل كل المتحكمات مع الأجهزة من خلال واجهة عامة للأجهزة تسمح لنفس المتحكم أن يدعم أكثر من نوع واحد للأجهزة.

يمكنك تطوير فئات المتحكمات عن بعد بشكل مستقل عن فئات الأجهزة، وكل ما تحتاجه هو إنشاء فئة فرعية جديدة، فقد يحتوي متحكم بسيط على زرين فقط، وتريد زيادة ذلك بإضافة مزايا إضافية مثل البطارية الإضافية أو شاشة لمس.

تربط شيفرة العميل النوع الذي تريده من المتحكمات مع كائن جهاز محدد من خلال منشئ المتحكم (remote constructor).

// يحدد التجريد واجهة الجزء الخاص بالتحكم في هرمية الفئتين.
// ويحافظ على مرجع لكائن من هرمية التطبيق ويفوض كل المهام الحقيقية إليه.

class RemoteControl is
    protected field device: Device
    constructor RemoteControl(device: Device) is
        this.device = device
    method togglePower() is
        if (device.isEnabled()) then
            device.disable()
        else
            device.enable()
    method volumeDown() is
        device.setVolume(device.getVolume() - 10)
    method volumeUp() is
        device.setVolume(device.getVolume() + 10)
    method channelDown() is
        device.setChannel(device.getChannel() - 1)
    method channelUp() is
        device.setChannel(device.getChannel() + 1)


// تستطيع زيادة الفئات من هرمية التجريد بشكل مستقل عن فئات الأجهزة.
class AdvancedRemoteControl extends RemoteControl is
    method mute() is
        device.setVolume(0)


// تصرح واجهة التطبيق عن أساليب شائعة بين كل فئات التطبيقات الحقيقية، وليس
// ضروريًا أن تطابق واجهة التجريد، بل إنه قد تختلف واجهتين تمامًا.
//  وتوفر واجهة التطبيق عمليات أولية فقط، بينما يحدد التجريد عمليات ذات
// المستوى الأعلى بناءً على تلك الأولية.
interface Device is
    method isEnabled()
    method enable()
    method disable()
    method getVolume()
    method setVolume(percent)
    method getChannel()
    method setChannel(channel)


// كل الأجهزة تتبع نفس الواجهة.
class Tv implements Device is
    // ...

class Radio implements Device is
    // ...


// في مكان ما من شيفرة العميل.
tv = new Tv()
remote = new RemoteControl(tv)
remote.togglePower()

radio = new Radio()
remote = new AdvancedRemoteControl(radio)

قابلية الاستخدام

استخدم نمط الجسر حين تريد تقسيم وتنظيم فئة أحادية (Monolithic) فيها متغيرات عديدة لبعض الوظائف (كأن تعمل الفئة مع عدة خوادم لقواعد البيانات).

كلما كبر حجم الفئة صعُبَ فهم كيفية عملها، وطالت عملية إحداث تغيير فيها، وقد تتطلب التغييرات التي تُجرى على أحد صور الوظائف إحداث تغييرات في الفئة كلها، الأمر الذي يتسبب عادة في حدوث أخطاء أو ترك بعض الآثار الجانبية الحرجة دون معالجة.

يسمح لك نمط الجسر بتقسيم الفئة الأحادية إلى عدة هرميات فئوية، ثم بعد ذلك يمكنك تغيير الفئات في كل هرمية بشكل مستقل عن الفئات التي في الهرميات الأخرى، ويبسط هذا المنظور عملية صيانة الشيفرة ويقلل خطورة تعطيل شيفرة حالية.

استخدم نمط الجسر حين تحتاج إلى زيادة فئة في عدة أبعاد متعامدة (مستقلة)

يقترح الجسر أن تستخرج هرمية فئة منفصلة لكل واحد من الأبعاد، وتفوض الفئة الأصلية العمل المرتبط إلى الكائنات التي تنتمي إلى تلك الهرميات بدلًا من فعل كل شيء بنفسها.

استخدم الجسر إن كنت تريد أن تكون قادرًا على تبديل التطبيقات في وقت التشغيل.

رغم أنه أمر خياري، لكن نمط الجسر يسمح لكل باستبدال كائن الاستخدام داخل التجريد، الأمر سهل كتعيين قيمة جديدة إلى حقل ما.

بالمناسبة، فإن ذلك العنصر الأخير هو السبب الرئيسي الذي يجعل كثيرًا من الناس يخلطون بين نمط الجسر ونمط الخطة (Strategy)، فتذكر أن النمط هو أكثر من مجرد طريقة معينة لهيكلة الفئات، فقد يوصل الهدف والمشكلة التي تتم معالجتها كذلك.

كيفية الاستخدام

  1. حدد الأبعاد المتعامدة في فئاتك، قد تكون تلك التصاميم المستقلة أحد ما يلي: تجريد/منصة، نطاق/بنية تحتية، واجهة أمامية (front-end)/واجهة خلفية (back-end)، أو واجهة/تطبيق (implementation).
  2. انظر أي العمليات يحتاجها العميل وحددها في فئة التجرية الأساسية.
  3. حدد العمليات المتاحة لكل المنصات، وصرح عن العمليات التي يحتاجها التجريد في واجهة الاستخدام العامة.
  4. أنشئ فئات تطبيق حقيقية لكل المنصات في نطاقك، لكن تأكد أنها جميعًا تتبع نفس الواجهة.
  5. دخل فئة التجريد، أضف حقلًا مرجعيًا لنوع التطبيق، يفوض التجريد أغلب العمل إلى كائن التطبيق الذي أشير إليه في ذلك الحقل.
  6. إن كان لديك عدة متغيرات من منطق عالي المستوى، فأنشئ تجريدًا منقحًا لكل متغير من خلال توسيع فئة التجريد الأساسية.
  7. يجب أن تمرر شيفرة العميل كائن تطبيق إلى منشئ التجريد من أجل ربط أحدهما بالآخر، ثم بعدها يمكن للعميل أن ينسى التطبيق ويعمل مع كائن التجريد فقط.

المزايا والعيوب

المزايا

يمكنك إنشاء فئات وتطبيقات تعمل على أي نظام تشغيل.

تعمل شيفرة العميل مع تجريدات عالية المستوى، ولا تتعرض إلى تفاصيل المنصة.

مبدأ المفتوح/المغلق. يمكنك إدخال تجريدات وتطبيقات جديدة بشكل مستقل عن بعضها البعض.

مبدأ المسؤولية الواحدة، يمكنك التركيز على المنطق عالي المستوى في التجريد وعلى تفاصيل المنصة في التطبيق.

العيوب

قد تجعل الشيفرة أكثر تعقيدًا بتطبيق النمط على فئة عالية التماسك.

العلاقات مع الأنماط الأخرى

يُصمم نمط الجسر عادة بشكل مسبق، مما يسمح لك بتطوير اجزاء من التطبيق بشكل مستقل عن بعضها البعض، لكن من الناحية الأخرى فإن نمط المحول يستخدم بكثرة مع التطبيق الحالي لجعل بعض الفئات تعمل بشكل سليم بعد أن كانت غير متوافقة ولا تعمل.

أنماط الجسر والحالة والخطة (ونمط المحول إلى حد ما) لديهم هيكلة متشابهة جدًا، فكل تلك الأنماط قد بنيت على التركيب، لكنها جميعًا تحل مشاكل مختلفة، فالنمط ليس وصفة لهيكلة شيفرتك بشكل معين، فهو أيضًا يوصل إلى المطورين الآخرين المشكلة التي يحلها النمط.

يمكنك استخدام المصنع المجرد مع نمط الجسر حين لا تعمل بعض التجريدات التي يحددها نمط الجسر إلا مع تطبيقات بعينها، فعندها يحتوي المصنع المجرد تلك العلاقات ويخفي تعقيدها من شيفرة العميل.

يمكنك جمع نمط الباني والجسر: تلعب فئة direcotr دورًا في التجريد، بينما تعمل البانيات المختلفة كتطبيقات.

الاستخدام في لغة جافا

المستوى: ★ ★ ★

الانتشار:  ★ ★ ☆

أمثلة الاستخدام: نمط الجسر مفيد جدًا عند التعامل مع البرامج متعددة المنصات أو دعم أنواع متعددة من خوادم قواعد البيانات أو مع عدة مزودي واجهة برمجة التطبيقات (API) لنوع بعينه (مثل المنصات السحابية أو الشبكات الاجتماعية أو غيرها).

يمكن ملاحظة نمط الجسر من خلال فرق واضح بين وحدة التحكم والمنصات المختلفة التي تعتمد عليها.

مثال: جسر بين الأجهزة ومتحكماتها عن بعد

يبين هذا المثال الفرق بين فئات المتحكمات والأجهزة التي تتحكم فيها. تتصرف المتحكمات كتجريدات، والأجهزة كتطبيقات لها، وتعمل نفس المتحكمات مع أجهزة مختلفة والعكس بالعكس.

يسمح نمط الجسر بتغيير الفئات أو إنشاء أخرى جديدة دون لمس شيفرة الهرمية المقابلة.

الأجهزة

 devices/Device.java: واجهة مشتركة لكل الأجهزة
package refactoring_guru.bridge.example.devices;

public interface Device {
    boolean isEnabled();

    void enable();

    void disable();

    int getVolume();

    void setVolume(int percent);

    int getChannel();

    void setChannel(int channel);

    void printStatus();
}
 devices/Radio.java: راديو
package refactoring_guru.bridge.example.devices;

public class Radio implements Device {
    private boolean on = false;
    private int volume = 30;
    private int channel = 1;

    @Override
    public boolean isEnabled() {
        return on;
    }

    @Override
    public void enable() {
        on = true;
    }

    @Override
    public void disable() {
        on = false;
    }

    @Override
    public int getVolume() {
        return volume;
    }

    @Override
    public void setVolume(int volume) {
        if (volume > 100) {
            this.volume = 100;
        } else if (volume < 0) {
            this.volume = 0;
        } else {
            this.volume = volume;
        }
    }

    @Override
    public int getChannel() {
        return channel;
    }

    @Override
    public void setChannel(int channel) {
        this.channel = channel;
    }

    @Override
    public void printStatus() {
        System.out.println("------------------------------------");
        System.out.println("| أنا راديو.");
        System.out.println("| أنا " + (on ? "مفعّل" : "غير مفعّل"));
        System.out.println("| مستوى الصوت الحالي هو " + volume + "%");
        System.out.println("| القناة الحالية هي " + channel);
        System.out.println("------------------------------------\n");
    }
}
 devices/Tv.java: تلفاز
package refactoring_guru.bridge.example.devices;

public class Tv implements Device {
    private boolean on = false;
    private int volume = 30;
    private int channel = 1;

    @Override
    public boolean isEnabled() {
        return on;
    }

    @Override
    public void enable() {
        on = true;
    }

    @Override
    public void disable() {
        on = false;
    }

    @Override
    public int getVolume() {
        return volume;
    }

    @Override
    public void setVolume(int volume) {
        if (volume > 100) {
            this.volume = 100;
        } else if (volume < 0) {
            this.volume = 0;
        } else {
            this.volume = volume;
        }
    }

    @Override
    public int getChannel() {
        return channel;
    }

    @Override
    public void setChannel(int channel) {
        this.channel = channel;
    }

    @Override
    public void printStatus() {
        System.out.println("------------------------------------");
        System.out.println("| I'm TV set.");
        System.out.println("| I'm " + (on ? "enabled" : "disabled"));
        System.out.println("| مستوى الصوت الحالي هو " + volume + "%");
        System.out.println("| القناة الحالية هي " + channel);
        System.out.println("------------------------------------\n");
    }
}
 remotes/BasicRemote.java: متحكم عن بعد بسيط
package refactoring_guru.bridge.example.remotes;

import refactoring_guru.bridge.example.devices.Device;

public class BasicRemote implements Remote {
    protected Device device;

    public BasicRemote() {}

    public BasicRemote(Device device) {
        this.device = device;
    }

    @Override
    public void power() {
        System.out.println("المتحكم: زر تشغيل");
        if (device.isEnabled()) {
            device.disable();
        } else {
            device.enable();
        }
    }

    @Override
    public void volumeDown() {
        System.out.println("المتحكم: خفض الصوت");
        device.setVolume(device.getVolume() - 10);
    }

    @Override
    public void volumeUp() {
        System.out.println("المتحكم: رفع الصوت");
        device.setVolume(device.getVolume() + 10);
    }

    @Override
    public void channelDown() {
        System.out.println("المتحكم: قناة سابقة");
        device.setChannel(device.getChannel() - 1);
    }

    @Override
    public void channelUp() {
        System.out.println("المتحكم: قناة تالية");
        device.setChannel(device.getChannel() + 1);
    }
}
 remotes/AdvancedRemote.java: متحكم عن بعد متطور
package refactoring_guru.bridge.example.remotes;

import refactoring_guru.bridge.example.devices.Device;

public class AdvancedRemote extends BasicRemote {

    public AdvancedRemote(Device device) {
        super.device = device;
    }

    public void mute() {
        System.out.println("المتحكم: كتم الصوت");
        device.setVolume(0);
    }
}
 Demo.java: شيفرة العميل
package refactoring_guru.bridge.example;

import refactoring_guru.bridge.example.devices.Device;
import refactoring_guru.bridge.example.devices.Radio;
import refactoring_guru.bridge.example.devices.Tv;
import refactoring_guru.bridge.example.remotes.AdvancedRemote;
import refactoring_guru.bridge.example.remotes.BasicRemote;

public class Demo {
    public static void main(String[] args) {
        testDevice(new Tv());
        testDevice(new Radio());
    }

    public static void testDevice(Device device) {
        System.out.println("اختبارات مع المتحكم البسيط.");
        BasicRemote basicRemote = new BasicRemote(device);
        basicRemote.power();
        device.printStatus();

        System.out.println("اختبارات مع المتحكم المتطور.");
        AdvancedRemote advancedRemote = new AdvancedRemote(device);
        advancedRemote.power();
        advancedRemote.mute();
        device.printStatus();
    }
}
 OutputDemo.txt: نتائج التنفيذ
اختبارات مع المتحكم البسيط
المتحكم: زر التشغيل
------------------------------------
| أنا جهاز تلفاز
| أنا مفعّل
| مستوى الصوت الحالي هو %30
| القناة الحالية هي 1
------------------------------------

اختبارات مع المتحكم المتطور
المتحكم: زر التشغيل
المتحكم: كتم الصوت
------------------------------------
| أنا جهاز تلفاز
| أنا غير مفعّل
| مستوى الصوت الحالي هو %0
| القناة الحالية هي 1
------------------------------------

اختبارات مع المتحكم المتقدم
المتحكم: زر التشغيل
------------------------------------
| أنا راديو
| أنا مفعّل
| مستوى الصوت الحالي هو %30
| القناة الحالية هي 1
------------------------------------

اختبارات مع المتحكم المتقدم
المتحكم: زر التشغيل
المتحكم: كتم الصوت
------------------------------------
| أنا راديو
| أنا غير مفعّل
| مستوى الصوت الحالي هو %0
| القناة الحالية هي 1
------------------------------------