ConstructiCat Logo
CodeBust.
Browse section ▾

المراقب.

المراقب هو نمط تصميم سلوكي يتيح لك تحديد آلية اشتراك لإخطار كائنات متعددة بأي أحداث تحدث للكائن الذي يراقبونه.

##Intent

المراقب هو نمط تصميم سلوكي يتيح لك تحديد آلية اشتراك لإخطار كائنات متعددة بأي أحداث تحدث للكائن الذي يراقبونه.

Observer Design Pattern

##Problem

تخيل أن لديك نوعين من الكائنات: Customer وStore. العميل مهتم جداً بعلامة تجارية معينة من المنتجات (لنقل إنها طراز جديد من iPhone) والتي يُفترض أن تتوفر في المتجر قريباً.

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

Visiting store vs. sending spam

زيارة المتجر مقابل إرسال البريد العشوائي

من ناحية أخرى، يمكن للمتجر إرسال كميات ضخمة من الرسائل الإلكترونية (التي قد تُعدّ بريداً عشوائياً) لجميع العملاء في كل مرة يتوفر فيها منتج جديد. سيوفر هذا على بعض العملاء رحلات لا نهاية لها إلى المتجر. في الوقت ذاته، سيُزعج العملاء الآخرين الذين لا يهتمون بالمنتجات الجديدة.

يبدو أننا أمام تعارض. إما أن يُضيع العميل وقته في التحقق من توفر المنتج، أو أن يُهدر المتجر موارده في إخطار العملاء غير المعنيين.

##Solution

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

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

Subscription mechanism

تتيح آلية الاشتراك للكائنات الفردية الاشتراك في إشعارات الأحداث.

الآن، في كل مرة يحدث فيها حدث مهم للناشر، يمر على قائمة مشتركيه ويستدعي تابع الإخطار المحدد على كائناتهم.

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

لهذا السبب من الضروري أن يُنفِّذ جميع المشتركين الواجهة ذاتها وأن يتواصل الناشر معهم فقط عبر تلك الواجهة. يجب أن تُعلن هذه الواجهة عن تابع الإخطار مع مجموعة من المعاملات التي يمكن للناشر استخدامها لتمرير بعض البيانات السياقية مع الإخطار.

Notification methods

يُخطر الناشر المشتركين باستدعاء تابع الإخطار المحدد على كائناتهم.

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

##Structure

Publisher -subscribers: Subscriber[]-mainState+subscribe(s: Subscriber)+unsubscribe(s: Subscriber)+notifySubscribers()+mainBusinessLogic() «interface» Subscriber +update(context) Concrete Subscribers ...+update(context) Client foreach (s in subscribers)s.update(this) mainState = newStatenotifySubscribers() s = new ConcreteSubscriber()publisher.subscribe(s)
Publisher -subscribers: Subscriber[]-mainState+subscribe(s: Subscriber)+unsubscribe(s: Subscriber)+notifySubscribers()+mainBusinessLogic() «interface» Subscriber +update(context) Concrete Subscribers ...+update(context) Client foreach (s in subscribers)s.update(this) mainState = newStatenotifySubscribers() s = new ConcreteSubscriber()publisher.subscribe(s) 1 2 3 4 5 6
  1. يُصدر الناشر أحداثاً تهم الكائنات الأخرى. تحدث هذه الأحداث عندما يغير الناشر حالته أو ينفذ بعض السلوكيات. يحتوي الناشرون على بنية تحتية للاشتراك تتيح للمشتركين الجدد الانضمام وللمشتركين الحاليين مغادرة القائمة.

  2. عند وقوع حدث جديد، يمر الناشر على قائمة الاشتراك ويستدعي تابع الإخطار المُعلَن عنه في واجهة المشترك على كل كائن مشترك.

  3. تُعلن واجهة المشترك عن واجهة الإخطار. في معظم الحالات، تتألف من تابع update واحد. قد يحتوي التابع على عدة معاملات تتيح للناشر تمرير بعض تفاصيل الحدث مع التحديث.

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

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

  6. يُنشئ العميل كائنات الناشر والمشترك بشكل منفصل ثم يسجل المشتركين لتحديثات الناشر.

##Pseudocode

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

Structure of the Observer pattern example

إخطار الكائنات بالأحداث التي تحدث لكائنات أخرى.

تُجمَّع قائمة المشتركين بشكل ديناميكي: يمكن للكائنات البدء في الاستماع إلى الإخطارات أو التوقف عنها في وقت التشغيل، وذلك تبعاً للسلوك المطلوب لتطبيقك.

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

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

// تتضمن فئة الناشر الأساسية شفرة إدارة الاشتراك
// وتوابع الإخطار.
class EventManager is
    private field listeners: hash map of event types and listeners

    method subscribe(eventType, listener) is
        listeners.add(eventType, listener)

    method unsubscribe(eventType, listener) is
        listeners.remove(eventType, listener)

    method notify(eventType, data) is
        foreach (listener in listeners.of(eventType)) do
            listener.update(data)

// يحتوي الناشر الملموس على منطق الأعمال الحقيقي الذي
// يهم بعض المشتركين. يمكننا اشتقاق هذه الفئة من
// الناشر الأساسي، لكن هذا ليس ممكناً دائماً في الواقع
// العملي لأن الناشر الملموس قد يكون بالفعل فئة فرعية.
// في هذه الحالة، يمكنك تصحيح منطق الاشتراك باستخدام
// التركيب، كما فعلنا هنا.
class Editor is
    public field events: EventManager
    private field file: File

    constructor Editor() is
        events = new EventManager()

    // يمكن لتوابع منطق الأعمال إخطار المشتركين بالتغييرات.
    method openFile(path) is
        this.file = new File(path)
        events.notify("open", file.name)

    method saveFile() is
        file.write()
        events.notify("save", file.name)

    // ...


// هذه هي واجهة المشترك. إذا كانت لغة البرمجة تدعم
// الأنواع الدالية، يمكنك استبدال تسلسل المشترك بأكمله
// بمجموعة من الدوال.
interface EventListener is
    method update(filename)

// يتفاعل المشتركون الملموسون مع التحديثات الصادرة عن الناشر
// المرتبطين به.
class LoggingListener implements EventListener is
    private field log: File
    private field message: string

    constructor LoggingListener(log_filename, message) is
        this.log = new File(log_filename)
        this.message = message

    method update(filename) is
        log.write(replace('%s',filename,message))

class EmailAlertsListener implements EventListener is
    private field email: string
    private field message: string

    constructor EmailAlertsListener(email, message) is
        this.email = email
        this.message = message

    method update(filename) is
        system.email(email, replace('%s',filename,message))


// يمكن للتطبيق تكوين الناشرين والمشتركين في وقت التشغيل.
class Application is
    method config() is
        editor = new Editor()

        logger = new LoggingListener(
            "/path/to/log.txt",
            "Someone has opened the file: %s")
        editor.events.subscribe("open", logger)

        emailAlerts = new EmailAlertsListener(
            "admin@example.com",
            "Someone has changed the file: %s")
        editor.events.subscribe("save", emailAlerts)

##Applicability

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

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

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

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

قائمة الاشتراك ديناميكية، لذا يمكن للمشتركين الانضمام إلى القائمة أو مغادرتها في أي وقت يحتاجون إليه.

##How to Implement

  1. راجع منطق أعمالك وحاول تقسيمه إلى جزأين: ستعمل الوظائف الأساسية المستقلة عن الشفرة الأخرى ناشراً؛ أما الباقي فسيتحول إلى مجموعة من فئات المشترك.

  2. أعلن عن واجهة المشترك. في حدها الأدنى، يجب أن تُعلن عن تابع update واحد.

  3. أعلن عن واجهة الناشر وصف زوجاً من التوابع لإضافة كائن مشترك إلى القائمة وإزالته منها. تذكر أن الناشرين يجب أن يعملوا مع المشتركين فقط عبر واجهة المشترك.

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

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

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

  6. نفِّذ توابع إشعار التحديث في فئات المشترك الملموسة. سيحتاج معظم المشتركين إلى بعض بيانات السياق حول الحدث. يمكن تمريرها كوسيطة لتابع الإخطار.

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

  7. يجب على العميل إنشاء جميع المشتركين الضروريين وتسجيلهم مع الناشرين المناسبين.

##Pros & Cons

Pros
  • مبدأ الفتح/الإغلاق. يمكنك إدخال فئات مشترك جديدة دون الحاجة إلى تغيير شفرة الناشر (والعكس صحيح إذا كانت هناك واجهة ناشر).
  • يمكنك تأسيس علاقات بين الكائنات في وقت التشغيل.
Cons
  • يُخطَر المشتركون بترتيب عشوائي.

##Relations with Other Patterns

  • سلسلة المسؤولية، الأمر، الوسيط والمراقب تعالج طرقاً مختلفة لتوصيل مُرسِلي الطلبات بمستقبليها:

    • سلسلة المسؤولية تمرر الطلب بشكل متسلسل عبر سلسلة ديناميكية من المستقبلين المحتملين حتى يتعامل معه أحدهم.
    • الأمر يُنشئ اتصالات أحادية الاتجاه بين المُرسِلين والمستقبلين.
    • الوسيط يُزيل الاتصالات المباشرة بين المُرسِلين والمستقبلين، ويُجبرهم على التواصل بشكل غير مباشر عبر كائن وسيط.
    • المراقب يتيح للمستقبلين الاشتراك الديناميكي في استقبال الطلبات أو إلغاء الاشتراك.
  • الفرق بين الوسيط والمراقب كثيراً ما يكون غامضاً. في معظم الحالات، يمكنك تطبيق أي من النمطين؛ لكن أحياناً يمكنك تطبيق كليهما في آنٍ واحد. دعنا نرى كيف يمكن تحقيق ذلك.

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

    ثمة تطبيق شائع لنمط الوسيط يعتمد على المراقب. يؤدي كائن الوسيط دور الناشر، وتعمل المكونات كمشتركين يشتركون في أحداث الوسيط ويلغون اشتراكهم. عندما يُطبَّق الوسيط بهذه الطريقة، قد يبدو مشابهاً جداً لـالمراقب.

    عند الشك، تذكر أنه يمكنك تطبيق نمط الوسيط بطرق أخرى. على سبيل المثال، يمكنك ربط جميع المكونات بكائن الوسيط ذاته بشكل دائم. هذا التطبيق لن يشبه المراقب لكنه سيظل مثيلاً لنمط الوسيط.

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