نمط المراقب Observer

من موسوعة حسوب
< Design Patterns
نسخة 13:53، 4 مارس 2020 للمستخدم جميل-بيلوني (نقاش | مساهمات)
(فرق) → مراجعة أقدم | المراجعة الحالية (فرق) | مراجعة أحدث ← (فرق)
اذهب إلى: تصفح، ابحث

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

محتويات

المشكلة

تخيل أن لديك نوعين من الكائنات: العميلcustomer والمتجر store، يهتم العميل بماركة بعينها من منتج ما (لنقل أنه إصدار جديد من آيفون)، وسيتاح هذا المنتج في المتجر قريبًا. بإمكان العميل أن يذهب كل يوم إلى المتجر ليرى إن كان الهاتف قد وصل أم لا، لكن أغلب تلك الزيارات ستكون هدرًا في الغالب إن كان الهاتف لم يأت، أليس كذلك؟ (انظر ش.1)

(ش.1) زيارة المتجر في مقابل إرسال السبام.

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

ونحن الآن أمام معضلة هنا، فإما أن يهدر العميل وقته في تفقد المنتجات الجديدة ذهابًا بنفسه إلى المتجر، وإما أن يهدر المتجر موارده في تنبيه العملاء الذين قد لا يرغبون في تلك المنتجات الجديدة.

الحل

(ش.2) تسمح آلية الاشتراك للكائنات المنفردة بالاشتراك في إشعارات حدث ما.

يسمى الكائن الذي عليه الاهتمام هنا بالهدف (subject)، لكن بما أنه سينبه كائنات أخرى بالتغييرات التي ستحدث لحالته، فسنسميه الناشر (publisher)، ويسمى الكائن الذي يرغب في تتبع التغير الحاصل في حالة الناشر بالمشترك (subscriber).

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

(ش.3) ينبه الناشر المشتركين باستدعاء أسلوب تنبيه محدد إلى كائناتهم.

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

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

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

مثال واقعي

(ش.4) اشتراكات المجلات والصحف.

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

البنية

dpo.structure-indexed.png
  1. يرسل الناشر (publisher) الأحداث المهمة للكائنات الأخرى، وتقع هذه الأحداث حين يغير الناشر حالته أو ينفذ بعض الأساليب، وتحتوي الكائنات الناشرة على بنية اشتراك تحتية تسمح للمشتركين الجديد بالانضمام، وللمشتركين الحاليين بترك القائمة.
  2. عند وقوع حدث جديد فإن الناشر يذهب لقائمة الاشتراك ويستدعي ألوب التنبيه المصرح عنه في واجهة المشترك الموجودة في كل كائنِ مشترِك.
  3. تصرح واجهة المشترك (subscriber) عن واجهة التنبيه، وفي الغالب تتكون تلك الواجهة من أسلوب update واحد، وقد يحتوي الأسلوب على عدة عوامل تسمح للناشر بتمرير بعض تفاصيل الأحداث مع التحديث.
  4. ينفذ المشتركون الحقيقيون (Concrete Subscribers) بعض الإجراءات كرد على التنبيهات التي أرسلها الناشر، ويجب أن تستخدم كل تلك الفئات نفس الواجهة كي لا يُربط الناشر إلى الفئات الحقيقية.
  5. عادة ما يحتاج المشتركون إلى بعض المعلومات السياقية لمعالجة التحديث بشكل سليم، ولهذا فإن الناشرون عادة ما يمررون بعض البيانات السياقية كوسائط (arguments) لأسلوب التنبيه، ويستطيع الناشر أن يمرر نفسه كوسيط (argument)، سامحًا للمشترك بجلب أي بيانات مطلوبة مباشرة.
  6. ينشئ العميلُ (Client) الناشرَ وكائنات المشترِك بشكل منفصل ثم يسجل المشتركين في تحديثات الناشر.

مثال توضيحي

(ش.6) تنبيه الكائنات بالأحداث التي تقع للكائنات الأخرى.

يتيح نمط المراقب في هذا المثال لكائن المحرر النصي أن ينبه كائنات الخدمة الأخرى بالتغييرات في حالته. (انظر ش.6)

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

وفي هذا الاستخدام فإن فئة المحرر لا تحتفظ بقائمة الاشتراك بنفسها، بل تفوض تلك المهمة إلى كائن مساعدة خاص مختص بتلك الوظيفة حصرًا، ويمكنك ترقية ذلك الكائن ليتصرف كمرسِل أحداث مركزي، سامحًا لأي كائن أن يتصرف كناشر. أيضًا، لا تتطلب إضافة مشتركين جدد إلى البرنامج أي تغييرات في فئات الناشر الحالية، طالما أنها تعمل مع كل المشتركين من خلال نفس الواجهة.
// تحتوي فئة الناشر الأساسية على شيفرة إدارة الاشتراك وأساليب التنبيه.
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

    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

    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 TextEditor()

        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)

قابلية التطبيق

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

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

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

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

قائمة الاشتراك مرنة، لذا فإن المشتركين يستطيعون الانضمام وإلغاء الاشتراك كذلك متى شاؤوا.

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

  1. ألق نظرة على منطق العمل لديك وحاول تقسيمه إلى جزئين، أولهما سيكون الوظيفية الأساسية (Core Functionality) الذي سيكون مستقلًا عن بقية الشيفرة والذي سيتصرف على أنه الناشر، أما الباقي فسيتحول إلى مجموعة من فئات المشترِك.
  2. صرّح عن واجهة المشترك، يجب أن تصرح عن أسلوب update واحد على الأقل.
  3. صرح عن واجهة الناشر وَصِف زوجًا من الأساليب لإضافة كائن المشترك وإزالته من القائمة، تذكر أن الناشرين يجب أن يعملوا مع المشتركين من خلال واجهة المشترِك حصرًا.
  4. حدد أين ستضع قائمة الاشتراك الفعلية وتطبيق أساليب الاشتراك (Implementation of subscribtion methods)، عادة ما تبدو تلك الشيفرة متطابقة لكل أنواع الناشرين، لذا يكون المكان البديهي لها هو فئة مجردة (abstract) مشتقة مباشرة من واجهة الناشر. يوسِّع الناشرون الحقيقيون (Concrete Publishers) هذه الفئة مكتسبين سلوك الاشتراك. لكن، إن كنت تطبق النمط على هرمية فئوية موجودة مسبقًا فجرب منظورًا مبنيًا على التركيب (Composition)، بأن تضع منطق الاشتراك في كائن منفصل، وتجعل كل الناشرين يستخدمونه.
  5. أنشئ فئات ناشر حقيقية. يجب أن تنبه هذه الفئاتُ كل المشتركين عند حدوث شيء مهم لأحد الناشرين.
  6. استخدم أساليب تنبيه التحديث (update notification methods) في فئات المشترك الحقيقية، سيحتاج أغلب المشتركين بعض البيانات السياقية عن الحدث، ويمكن تمريرها كوسيط (argument) لأسلوب التنبيه. يوجد خيار آخر يقترح أن يجلب المشترك أي بيانات مباشرة من التنبيه عند استلام ذلك التنبيه، وفي تلك الحالة يجب أن يمرر الناشر نفسه من خلال أسلوب التحديث. كذلك يوجد خيار ثالث أقل مرونة، وهو أن يُربط الناشر بالمشترك مباشرة من خلال منشئ (Constructor).
  7. يجب أن ينشئ العميل كل المشتركين ويسجلهم مع الناشرين المناسبين لهم.

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

المزايا

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

العيوب

  • يُنبه المشتركون عشوائيًا.

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

  1. تمرر سلسلة المسؤولية الطلب بشكل تسلسلي مع سلسلة ديناميكية من المستقبلات المحتملة إلى أن يعالجها أحد المستقبلات.
  2. ينشئ الأمر اتصالات أحادية الاتجاه بين المرسِلات والمستقبِلات.
  3. يزيل الوسيط الاتصالات المباشرة بين المرسِلات والمستقبلات مجبرًا إياها على التواصل بشكل غير مباشر من خلال كائن وسيط.
  4. يسمح المراقب للمستقبلات بالاشتراك في استقبال الطلبات وكذلك إلغاء الاشتراك بمرونة.
  • عادة ما يكون الفرق بين نمطي الوسيط والمراقب محيرًا، ففي أغلب الحالات تستطيع استخدام نمط واحد فحسب منهما، لكن في ظروف تشغيل أخرى قد تستطيع استخدام كليهما معًا، وذلك على النحو التالي:

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

ولدينا استخدام شائع لنمط الوسيط يعتمد فيه على نمط المراقب، يلعب فيه كائن الوسيط دور الناشر (publisher)، ويتصرف كل عنصر كمشترك يستطيع الاشتراك في أحداث الوسيط (mediator events) وكذلك إلغاء الاشتراك منها، وتطبيق نمط الوسيط بهذا الشكل يجعله قريب الشبه جدًا من نمط المراقب. تذكر أنك تستطيع استخدام نمط الوسيط بطرق أخرى، فيمكنك مثلًا ربط كل العناصر إلى نفس الكائن الوسيط، ورغم أن هذا الاستخدام لا يشبه نمط المراقب لكن لا يزال صورة من نمط الوسيط.

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

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

المستوى: ★ ★ ☆

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

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

يمكن ملاحظة النمط من خلال أساليب الاشتراك التي تخزن الكائنات في قائمة وعبر الاستدعاءات إلى أسلوب التحديث المرسَل إلى الكائنات داخل تلك القائمة.

الاشتراك في الأحداث

في هذا المثال ينشئ نمط المراقب تعاونًا غير مباشر بين كائنات محرر نصي، ففي كل مرة يتغير كائن Editor، فإنه ينبه المشتركين به، ويتفاعل كل من EmailNotificationListener و LogOpenListener مع هذه التنبيهات بتنفيذ سلوكياتهم الرئيسية (primary behaviours).

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

الناشر publisher

 publisher/EventManager.java: الناشر الأساسي Basic Publisher
package refactoring_guru.observer.example.publisher;

import refactoring_guru.observer.example.listeners.EventListener;

import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class EventManager {
    Map<String, List<EventListener>> listeners = new HashMap<>();

    public EventManager(String... operations) {
        for (String operation : operations) {
            this.listeners.put(operation, new ArrayList<>());
        }
    }

    public void subscribe(String eventType, EventListener listener) {
        List<EventListener> users = listeners.get(eventType);
        users.add(listener);
    }

    public void unsubscribe(String eventType, EventListener listener) {
        List<EventListener> users = listeners.get(eventType);
        users.remove(listener);
    }

    public void notify(String eventType, File file) {
        List<EventListener> users = listeners.get(eventType);
        for (EventListener listener : users) {
            listener.update(eventType, file);
        }
    }
}

المحرر editor

 editor/Editor.java: الناشر الحقيقي، تتبَّعه الكائنات الأخرى
package refactoring_guru.observer.example.editor;

import refactoring_guru.observer.example.publisher.EventManager;

import java.io.File;

public class Editor {
    public EventManager events;
    private File file;

    public Editor() {
        this.events = new EventManager("open", "save");
    }

    public void openFile(String filePath) {
        this.file = new File(filePath);
        events.notify("open", file);
    }

    public void saveFile() throws Exception {
        if (this.file != null) {
            events.notify("save", file);
        } else {
            throw new Exception("Please open a file first.");
        }
    }
}

المستمعين listeners

 listeners/EventListener.java: واجهة المراقب المشتركة
package refactoring_guru.observer.example.listeners;

import java.io.File;

public interface EventListener {
    void update(String eventType, File file);
}
 listeners/EmailNotificationListener.java: يرسل رسائل بريدية عند استلام تنبيهات
package refactoring_guru.observer.example.listeners;

import java.io.File;

public class EmailNotificationListener implements EventListener {
    private String email;

    public EmailNotificationListener(String email) {
        this.email = email;
    }

    @Override
    public void update(String eventType, File file) {
        System.out.println("Email to " + email + ": Someone has performed " + eventType + " operation with the following file: " + file.getName());
    }
}
 listeners/LogOpenListener.java: يكتب رسالة إلى سجل (log) عند استلام تنبيهات
package refactoring_guru.observer.example.listeners;

import java.io.File;

public class LogOpenListener implements EventListener {
    private File log;

    public LogOpenListener(String fileName) {
        this.log = new File(fileName);
    }

    @Override
    public void update(String eventType, File file) {
        System.out.println("Save to log " + log + ": Someone has performed " + eventType + " operation with the following file: " + file.getName());
    }
}
 Demo.java: ِشيفرة البدء (Initialization code)
package refactoring_guru.observer.example;

import refactoring_guru.observer.example.editor.Editor;
import refactoring_guru.observer.example.listeners.EmailNotificationListener;
import refactoring_guru.observer.example.listeners.LogOpenListener;

public class Demo {
    public static void main(String[] args) {
        Editor editor = new Editor();
        editor.events.subscribe("open", new LogOpenListener("/path/to/log/file.txt"));
        editor.events.subscribe("save", new EmailNotificationListener("admin@example.com"));

        try {
            editor.openFile("test.txt");
            editor.saveFile();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

 OutputDemo.txt: نتائج التنفيذ

Save to log \path\to\log\file.txt: Someone has performed open operation with the following file: test.txt
Email to admin@example.com: Someone has performed save operation with the following file: test.txt

الاستخدام في لغة #C

المستوى: ★ ★ ☆

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

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

يمكن ملاحظة النمط من خلال أساليب الاشتراك التي تخزن الكائنات في قائمة وعبر الاستدعاءات إلى أسلوب التحديث المرسَل إلى الكائنات داخل تلك القائمة.

مثال تصوري

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

  • ما الفئات التي يتكون منها؟
  • ما الأدوار التي تلعبها هذه الفئات؟
  • كيف ترتبط عناصر النمط ببعضها؟

 Program.cs: مثال تصوري

using System;
using System.Collections.Generic;
using System.Threading;

namespace RefactoringGuru.DesignPatterns.Observer.Conceptual
{
    public interface IObserver
    {
        // استقبل التحديث من الهدف.
        void Update(ISubject subject);
    }

    public interface ISubject
    {
        // أرفق مراقبًا إلى الهدف.
        void Attach(IObserver observer);

        // افصل المراقب من الهدف.
        void Detach(IObserver observer);

        // نبِّه جمع المراقبين بحدث ما.
        void Notify();
    }

    // يملك الهدف حالة مهمة وينبه المراقبين حين تتغير تلك الحالة.
    public class Subject : ISubject
    {
        // سنخزن حالة الهدف اللازمة لكل المشتركين في هذا المتغير بداعي التبسيط.
        public int State { get; set; } = -0;

        // قائمة بالمشتركين. يمكن تخزين قائمة المشتركين في الأمثلة الواقعية
        // بشكل أكثر شمولية كأن تصنف وفق نوع الحدث، إلخ.
        private List<IObserver> _observers = new List<IObserver>();

        // أساليب إدارة الاشتراك.
        public void Attach(IObserver observer)
        {
            Console.WriteLine("Subject: Attached an observer.");
            this._observers.Add(observer);
        }

        public void Detach(IObserver observer)
        {
            this._observers.Remove(observer);
            Console.WriteLine("Subject: Detached an observer.");
        }

        // ابدأ تحديثًا في كل مشترك.
        public void Notify()
        {
            Console.WriteLine("Subject: Notifying observers...");

            foreach (var observer in _observers)
            {
                observer.Update(this);
            }
        }

        // عادة ما يمثل منطق الاشتراك جزءًا صغيرًا مما يستطيع الهدف أن يفعله
        // وتحتفظ الأهداف عادة ببعض منطق العمل الهام الذي يشغِّل أسلوب
        // تنبيه كلما أوشك أن يقع حدث مهم، أو بعد وقوعه.
        public void SomeBusinessLogic()
        {
            Console.WriteLine("\nSubject: I'm doing something important.");
            this.State = new Random().Next(0, 10);

            Thread.Sleep(15);

            Console.WriteLine("Subject: My state has just changed to: " + this.State);
            this.Notify();
        }
    }

    // يتفاعل المراقبون الحقيقيون مع التحديثات التي يرسلها الهدف المرفقين به.
    class ConcreteObserverA : IObserver
    {
        public void Update(ISubject subject)
        {            
            if ((subject as Subject).State < 3)
            {
                Console.WriteLine("ConcreteObserverA: Reacted to the event.");
            }
        }
    }

    class ConcreteObserverB : IObserver
    {
        public void Update(ISubject subject)
        {
            if ((subject as Subject).State == 0 || (subject as Subject).State >= 2)
            {
                Console.WriteLine("ConcreteObserverB: Reacted to the event.");
            }
        }
    }
    
    class Program
    {
        static void Main(string[] args)
        {
            // شيفرة العميل.
            var subject = new Subject();
            var observerA = new ConcreteObserverA();
            subject.Attach(observerA);

            var observerB = new ConcreteObserverB();
            subject.Attach(observerB);

            subject.SomeBusinessLogic();
            subject.SomeBusinessLogic();

            subject.Detach(observerB);

            subject.SomeBusinessLogic();
        }
    }
}

Output.txt: نتائج التنفيذ

Subject: Attached an observer.
Subject: Attached an observer.

Subject: I'm doing something important.
Subject: My state has just changed to: 2
Subject: Notifying observers...
ConcreteObserverA: Reacted to the event.
ConcreteObserverB: Reacted to the event.

Subject: I'm doing something important.
Subject: My state has just changed to: 1
Subject: Notifying observers...
ConcreteObserverA: Reacted to the event.
Subject: Detached an observer.

Subject: I'm doing something important.
Subject: My state has just changed to: 5
Subject: Notifying observers...

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

المستوى: ★ ★ ☆

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

أمثلة الاستخدام: تحتوي لغة PHP على عدة واجهات مضمنة فيها (SplSubject - SplObserver) يمكن استخدامها لجعل استخداماتك لنمط المراقب متوافقة مع بقية شيفرة PHP.

مثال تصوري

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

  • ما الفئات التي يتكون منها؟
  • ما الأدوار التي تلعبها هذه الفئات؟
  • كيف ترتبط عناصر النمط ببعضها؟

سيكون من السهل عليك استيعاب المثال التالي بعد تعلم بنية النمط، بناء على استخدام واقعي له في لغة PHP.

 index.php: مثال تصوري

<?php

namespace RefactoringGuru\Observer\Conceptual;

/**
 * بها بعض الواجهات المضمنة فيها والمتعلقة بنمط المراقب PHP لغة 
 *
 * إليك الكيفية التي تبدو عليها واجهة الهدف:
 *
 * @link http://php.net/manual/en/class.splsubject.php
 *
 *     interface SplSubject
 *     {
 *         // أرفق مراقبًا إلى الهدف.
 *         public function attach(SplObserver $observer);
 *
 *         // افصل المراقب من الهدف.
 *         public function detach(SplObserver $observer);
 *
 *         // نبِّه جمع المراقبين بحدث ما.
 *         public function notify();
 *     }
 *
 * هناك أيضًا واجهة مضمنة للمراقبين:
 *
 * @link http://php.net/manual/en/class.splobserver.php
 *
 *     interface SplObserver
 *     {
 *         public function update(SplSubject $subject);
 *     }
 */

/**
 * يملك الهدف حالة مهمة وينبه المراقبين حين تتغير تلك الحالة.
 */
class Subject implements \SplSubject
{
    /**
     * @var int سنخزن حالة الهدف اللازمة لكل المشتركين في هذا المتغير بداعي التبسيط.
     */
    public $state;

    /**
     * @var \SplObjectStorage قائمة بالمشتركين. يمكن تخزين قائمة المشتركين 
     * في الأمثلة الواقعية بشكل أشمل، كأن تصنف وفق نوع الحدث، إلخ.
     */
    private $observers;
    
    public function __construct()
    {
        $this->observers = new \SplObjectStorage;
    }

    /**
     * أساليب إدارة الاشتراك.
     */
    public function attach(\SplObserver $observer): void
    {
        echo "Subject: Attached an observer.\n";
        $this->observers->attach($observer);
    }

    public function detach(\SplObserver $observer): void
    {
        $this->observers->detach($observer);
        echo "Subject: Detached an observer.\n";
    }

    /**
     * ابدأ تحديثًا في كل مشترك.
     */
    public function notify(): void
    {
        echo "Subject: Notifying observers...\n";
        foreach ($this->observers as $observer) {
            $observer->update($this);
        }
    }

    /**
     * عادة ما يمثل منطق الاشتراك جزءًا صغيرًا مما يستطيع الهدف أن يفعله
     * وتحتفظ الأهداف عادة ببعض منطق العمل الهام الذي يشغِّل أسلوب
     * تنبيه كلما أوشك أن يقع حدث مهم، أو بعد وقوعه.
     */
    public function someBusinessLogic(): void
    {
        echo "\nSubject: I'm doing something important.\n";
        $this->state = rand(0, 10);

        echo "Subject: My state has just changed to: {$this->state}\n";
        $this->notify();
    }
}

/**
 * يتفاعل المراقبون الحقيقيون مع التحديثات التي يرسلها الهدف المرفقين به.
 */
class ConcreteObserverA implements \SplObserver
{
    public function update(\SplSubject $subject): void
    {
        if ($subject->state < 3) {
            echo "ConcreteObserverA: Reacted to the event.\n";
        }
    }
}

class ConcreteObserverB implements \SplObserver
{
    public function update(\SplSubject $subject): void
    {
        if ($subject->state == 0 || $subject->state >= 2) {
            echo "ConcreteObserverB: Reacted to the event.\n";
        }
    }
}

/**
 * شيفرة العميل.
 */

$subject = new Subject;

$o1 = new ConcreteObserverA;
$subject->attach($o1);

$o2 = new ConcreteObserverB;
$subject->attach($o2);

$subject->someBusinessLogic();
$subject->someBusinessLogic();

$subject->detach($o2);

$subject->someBusinessLogic();

 Output.txt: نتائج التنفيذ

Subject: Attached an observer.
Subject: Attached an observer.

Subject: I'm doing something important.
Subject: My state has just changed to: 2
Subject: Notifying observers...
ConcreteObserverA: Reacted to the event.
ConcreteObserverB: Reacted to the event.

Subject: I'm doing something important.
Subject: My state has just changed to: 4
Subject: Notifying observers...
ConcreteObserverB: Reacted to the event.
Subject: Detached an observer.

Subject: I'm doing something important.
Subject: My state has just changed to: 1
Subject: Notifying observers...
ConcreteObserverA: Reacted to the event.

مثال واقعي

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

 index.php: مثال واقعي

<?php

namespace RefactoringGuru\Observer\RealWorld;

/**
 * هدفًا، وتهتم عدة كائنات بتتبع حالته الداخلية UserRepository يمثل 
 * سواء كان إضافة مستخدم أو إزالته.
 */
class UserRepository implements \SplSubject
{
    /**
     * @var array قائمة المستخدمين.
     */
    private $users = [];

    // هنا تكون البنية التحتية الفعلية لإدارة المراقب، لاحظ أنها ليست كل
    // ما تكون فئتنا مسؤولة عنه.
    // المهمة الرئيسية لها ستكون أسفل هذه الأساليب.

    /**
     * @var array
     */
    private $observers = [];

    public function __construct()
    {
        // مجموعة أحداث خاصة للمراقبين الذين يريدون الاستماع إلى 
        // كل الأحداث.
        $this->observers["*"] = [];
    }

    private function initEventGroup(string $event = "*"): void
    {
        if (!isset($this->observers[$event])) {
            $this->observers[$event] = [];
        }
    }

    private function getEventObservers(string $event = "*"): array
    {
        $this->initEventGroup($event);
        $group = $this->observers[$event];
        $all = $this->observers["*"];

        return array_merge($group, $all);
    }

    public function attach(\SplObserver $observer, string $event = "*"): void
    {
        $this->initEventGroup($event);

        $this->observers[$event][] = $observer;
    }

    public function detach(\SplObserver $observer, string $event = "*"): void
    {
        foreach ($this->getEventObservers($event) as $key => $s) {
            if ($s === $observer) {
                unset($this->observers[$event][$key]);
            }
        }
    }

    public function notify(string $event = "*", $data = null): void
    {
        echo "UserRepository: Broadcasting the '$event' event.\n";
        foreach ($this->getEventObservers($event) as $observer) {
            $observer->update($this, $event, $data);
        }
    }

    // إليك الأساليب الممثلة لمنطق العمل للفئة.

    public function initialize($filename): void
    {
        echo "UserRepository: Loading user records from a file.\n";
        // ...
        $this->notify("users:init", $filename);
    }

    public function createUser(array $data): User
    {
        echo "UserRepository: Creating a user.\n";

        $user = new User;
        $user->update($data);

        $id = bin2hex(openssl_random_pseudo_bytes(16));
        $user->update(["id" => $id]);
        $this->users[$id] = $user;

        $this->notify("users:created", $user);

        return $user;
    }

    public function updateUser(User $user, array $data): User
    {
        echo "UserRepository: Updating a user.\n";

        $id = $user->attributes["id"];
        if (!isset($this->users[$id])) {
            return null;
        }

        $user = $this->users[$id];
        $user->update($data);

        $this->notify("users:updated", $user);

        return $user;
    }

    public function deleteUser(User $user): void
    {
        echo "UserRepository: Deleting a user.\n";

        $id = $user->attributes["id"];
        if (!isset($this->users[$id])) {
            return;
        }

        unset($this->users[$id]);

        $this->notify("users:deleted", $user);
    }
}

/**
 * لنترك فئة المستخدم مهمشة بما أنها ليست محور مثالنا.
 */
class User
{
    public $attributes = [];

    public function update($data): void
    {
        $this->attributes = array_merge($this->attributes, $data);
    }
}

/**
 * يسجل هذا العنصر الحقيقي أي أحداث يشترك فيها.
 */
class Logger implements \SplObserver
{
    private $filename;

    public function __construct($filename)
    {
        $this->filename = $filename;
        if (file_exists($this->filename)) {
            unlink($this->filename);
        }
    }

    public function update(\SplSubject $repository, string $event = null, $data = null): void
    {
        $entry = date("Y-m-d H:i:s") . ": '$event' with data '" . json_encode($data) . "'\n";
        file_put_contents($this->filename, $entry, FILE_APPEND);

        echo "Logger: I've written '$event' entry to the log.\n";
    }
}

/**
 * يرسل هذا العنصر الحقيقي إرشادات أولية إلى المستخدمين الجدد، ويكون
 * العميل مسؤولًا عن إرفاق هذا العنصر إلى حدث إنشاء مستخدم مناسب.
 */
class OnboardingNotification implements \SplObserver
{
    private $adminEmail;

    public function __construct($adminEmail)
    {
        $this->adminEmail = $adminEmail;
    }

    public function update(\SplSubject $repository, string $event = null, $data = null): void
    {
        // mail($this->adminEmail,
        //     "Onboarding required",
        //     "لدينا مستخدم، وها هي بياناته: " .json_encode($data));

        echo "OnboardingNotification: The notification has been emailed!\n";
    }
}

/**
 * شيفرة العميل.
 */

$repository = new UserRepository;
$repository->attach(new Logger(__DIR__ . "/log.txt"), "*");
$repository->attach(new OnboardingNotification("1@example.com"), "users:created");

$repository->initialize(__DIR__ . "/users.csv");

// ...

$user = $repository->createUser([
    "name" => "John Smith",
    "email" => "john99@example.com",
]);

// ...

$repository->deleteUser($user);

 Output.txt: نتائج التنفيذ

UserRepository: Loading user records from a file.
UserRepository: Broadcasting the 'users:init' event.
Logger: I've written 'users:init' entry to the log.
UserRepository: Creating a user.
UserRepository: Broadcasting the 'users:created' event.
OnboardingNotification: The notification has been emailed!
Logger: I've written 'users:created' entry to the log.
UserRepository: Deleting a user.
UserRepository: Broadcasting the 'users:deleted' event.
Logger: I've written 'users:deleted' entry to the log.

الاستخدام في لغة بايثون

المستوى: ★ ★ ☆

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

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

يمكن ملاحظة النمط من خلال أساليب الاشتراك التي تخزن الكائنات في قائمة وعبر الاستدعاءات إلى أسلوب التحديث المرسَل إلى الكائنات داخل تلك القائمة.

مثال تصوري

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

  • ما الفئات التي يتكون منها؟
  • ما الأدوار التي تلعبها هذه الفئات؟
  • كيف ترتبط عناصر النمط ببعضها؟

 main.py: مثال تصوري

from __future__ import annotations
from abc import ABC, abstractmethod
from random import randrange
from typing import List


class Subject(ABC):
    """
    تصرح واجهة الهدف عن مجموعة من الأساليب لإدارة المشتركين.
    """

    @abstractmethod
    def attach(self, observer: Observer) -> None:
        """
        أرفق مراقبًا إلى الهدف.
        """
        pass

    @abstractmethod
    def detach(self, observer: Observer) -> None:
        """
        افصل المراقب من الهدف.
        """
        pass

    @abstractmethod
    def notify(self) -> None:
        """
        افصل المراقب من الهدف.
        """
        pass


class ConcreteSubject(Subject):
    """
    يملك الهدف حالة مهمة وينبه المراقبين حين تتغير تلك الحالة.
    """

    _state: int = None
    """
    سنخزن حالة الهدف اللازمة لكل المشتركين في هذا المتغير بداعي التبسيط.
    """

    _observers: List[Observer] = []
    """
    قائمة بالمشتركين. يمكن تخزين قائمة المشتركين في الأمثلة الواقعية
    بشكل أشمل كأن تصنف وفق نوع الحدث، إلخ.
    """

    def attach(self, observer: Observer) -> None:
        print("Subject: Attached an observer.")
        self._observers.append(observer)

    def detach(self, observer: Observer) -> None:
        self._observers.remove(observer)

    """
    أساليب إدارة الاشتراك.
    """

    def notify(self) -> None:
        """
        ابدأ تحديثًا في كل مشترك.
        """

        print("Subject: Notifying observers...")
        for observer in self._observers:
            observer.update(self)

    def some_business_logic(self) -> None:
        """
        عادة ما يمثل منطق الاشتراك جزءًا صغيرًا مما يستطيع الهدف أن يفعله
        وتحتفظ الأهداف عادة ببعض منطق العمل الهام الذي يشغِّل أسلوب
        تنبيه كلما أوشك أن يقع حدث مهم، أو بعد وقوعه.
        """

        print("\nSubject: I'm doing something important.")
        self._state = randrange(0, 10)

        print(f"Subject: My state has just changed to: {self._state}")
        self.notify()


class Observer(ABC):
    """
    يتفاعل المراقبون الحقيقيون مع التحديثات التي يرسلها الهدف المرفقين به.
    """

    @abstractmethod
    def update(self, subject: Subject) -> None:
        """
        استقبل التحديث من الهدف.
        """
        pass


"""
يتفاعل المراقبون الحقيقيون مع التحديثات التي يرسلها الهدفُ المرفقين به.
"""


class ConcreteObserverA(Observer):
    def update(self, subject: Subject) -> None:
        if subject._state < 3:
            print("ConcreteObserverA: Reacted to the event")


class ConcreteObserverB(Observer):
    def update(self, subject: Subject) -> None:
        if subject._state == 0 or subject._state >= 2:
            print("ConcreteObserverB: Reacted to the event")


if __name__ == "__main__":
    # The client code.

    subject = ConcreteSubject()

    observer_a = ConcreteObserverA()
    subject.attach(observer_a)

    observer_b = ConcreteObserverB()
    subject.attach(observer_b)

    subject.some_business_logic()
    subject.some_business_logic()

    subject.detach(observer_a)

    subject.some_business_logic()

 Output.txt: نتائج التنفيذ

Subject: Attached an observer.
Subject: Attached an observer.

Subject: I'm doing something important.
Subject: My state has just changed to: 0
Subject: Notifying observers...
ConcreteObserverA: Reacted to the event
ConcreteObserverB: Reacted to the event

Subject: I'm doing something important.
Subject: My state has just changed to: 5
Subject: Notifying observers...
ConcreteObserverB: Reacted to the event

Subject: I'm doing something important.
Subject: My state has just changed to: 0
Subject: Notifying observers...
ConcreteObserverB: Reacted to the event

الاستخدام في لغة روبي

المستوى: ★ ★ ☆

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

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

يمكن ملاحظة النمط من خلال أساليب الاشتراك التي تخزن الكائنات في قائمة وعبر الاستدعاءات إلى أسلوب التحديث المرسَل إلى الكائنات داخل تلك القائمة.

مثال تصوري

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

  • ما الفئات التي يتكون منها؟
  • ما الأدوار التي تلعبها هذه الفئات؟
  • كيف ترتبط عناصر النمط ببعضها؟

 main.rb: مثال تصوري

# تصرح واجهة الهدف عن مجموعة من الأساليب لإدارة المشتركين.
class Subject
  # أرفق مراقبًا إلى الهدف.
  def attach(observer)
    raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'"
  end

  # افصل المراقب من الهدف.
  def detach(observer)
    raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'"
  end

  # نبه كل المراقبين بالحدث.
  def notify
    raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'"
  end
end

# يملك الهدف حالة مهمة وينبه المراقبين حين تتغير تلك الحالة.
class ConcreteSubject < Subject
  # سنخزن حالة الهدف اللازمة لكل المشتركين في هذا المتغير بداعي التبسيط.
  attr_accessor :state

  # @!attribute observers
  # @return [Array<Observer>] attr_accessor :observers private :observers

  def initialize
    @observers = []
  end

  # قائمة بالمشتركين. يمكن تخزين قائمة المشتركين في الأمثلة الواقعية
  # بشكل أشمل كأن تصنف وفق نوع الحدث، إلخ.

  # @param [Obserser] observer
  def attach(observer)
    puts 'Subject: Attached an observer.'
    @observers << observer
  end

  # @param [Obserser] observer
  def detach(observer)
    @observers.delete(observer)
  end

  # أساليب إدارة الاشتراك.

  # ابدأ تحديثًا في كل مشترك.
  def notify
    puts 'Subject: Notifying observers...'
    @observers.each { |observer| observer.update(self) }
  end

  # عادة ما يمثل منطق الاشتراك جزءًا صغيرًا مما يستطيع الهدف أن يفعله
  # وتحتفظ الأهداف عادة ببعض منطق العمل الهام الذي يشغِّل أسلوب
  # تنبيه كلما أوشك أن يقع حدث مهم، أو بعد وقوعه.
  def some_business_logic
    puts "\nSubject: I'm doing something important."
    @state = rand(0..10)

    puts "Subject: My state has just changed to: #{@state}"
    notify
  end
end

# يتفاعل المراقبون الحقيقيون مع التحديثات التي يرسلها الهدفُ المرفقين به.
class Observer
  # استقبل التحديث من الهدف.
  def update(_subject)
    raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'"
  end
end

# يتفاعل المراقبون الحقيقيون مع التحديثات التي يرسلها الهدفُ المرفقين به.

class ConcreteObserverA < Observer
  # @param [Subject] subject
  def update(subject)
    puts 'ConcreteObserverA: Reacted to the event' if subject.state < 3
  end
end

class ConcreteObserverB < Observer
  # @param [Subject] subject
  def update(subject)
    return unless subject.state.zero? || subject.state >= 2

    puts 'ConcreteObserverB: Reacted to the event'
  end
end

# شيفرة العميل.

subject = ConcreteSubject.new

observer_a = ConcreteObserverA.new
subject.attach(observer_a)

observer_b = ConcreteObserverB.new
subject.attach(observer_b)

subject.some_business_logic
subject.some_business_logic

subject.detach(observer_a)

subject.some_business_logic

 output.txt: نتائج التنفيذ

Subject: Attached an observer.
Subject: Attached an observer.

Subject: I'm doing something important.
Subject: My state has just changed to: 2
Subject: Notifying observers...
ConcreteObserverA: Reacted to the event
ConcreteObserverB: Reacted to the event

Subject: I'm doing something important.
Subject: My state has just changed to: 10
Subject: Notifying observers...
ConcreteObserverB: Reacted to the event

Subject: I'm doing something important.
Subject: My state has just changed to: 2
Subject: Notifying observers...
ConcreteObserverB: Reacted to the event

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

المستوى: ★ ★ ☆

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

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

يمكن ملاحظة النمط من خلال أساليب الاشتراك التي تخزن الكائنات في قائمة وعبر الاستدعاءات إلى أسلوب التحديث المرسَل إلى الكائنات داخل تلك القائمة.

مثال تصوري

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

  • ما الفئات التي يتكون منها؟
  • ما الأدوار التي تلعبها هذه الفئات؟
  • كيف ترتبط عناصر النمط ببعضها؟

بعد تعلم بنية النمط سيكون من السهل عليك استيعاب المثال التالي المبني على حالة واقعية في لغة Swift.

 Example.swift: مثال تصوري

import XCTest

/// يملك الهدف حالة مهمة وينبه المراقبين حين تتغير تلك الحالة.
class Subject {

    /// سنخزن حالة الهدف اللازمة لكل المشتركين في هذا المتغير بداعي التبسيط.
    var state: Int = { return Int(arc4random_uniform(10)) }()

    /// @var array قائمة بالمشتركين. يمكن تخزين قائمة المشتركين في الأمثلة الواقعية
    /// بشكل أشمل كأن تصنف وفق نوع الحدث، إلخ.
    private lazy var observers = [Observer]()

    /// أساليب إدارة الاشتراك.
    func attach(_ observer: Observer) {
        print("Subject: Attached an observer.\n")
        observers.append(observer)
    }

    func detach(_ observer: Observer) {
        if let idx = observers.index(where: { $0 === observer }) {
            observers.remove(at: idx)
            print("Subject: Detached an observer.\n")
        }
    }

    /// ابدأ تحديثًا في كل مشترك.
    func notify() {
        print("Subject: Notifying observers...\n")
        observers.forEach({ $0.update(subject: self)})
    }

    /// عادة ما يمثل منطق الاشتراك جزءًا صغيرًا مما يستطيع الهدف أن يفعله
    /// وتحتفظ الأهداف عادة ببعض منطق العمل الهام الذي يشغِّل أسلوب
    /// تنبيه كلما أوشك أن يقع حدث مهم، أو بعد وقوعه.
    func someBusinessLogic() {
        print("\nSubject: I'm doing something important.\n")
        state = Int(arc4random_uniform(10))
        print("Subject: My state has just changed to: \(state)\n")
        notify()
    }
}

/// يصرح بروتوكول المراقب عن أسلوب تحديث تستخدمه الأهداف.
protocol Observer: class {

    func update(subject: Subject)
}

/// يتفاعل المراقبون الحقيقيون مع التحديثات التي يرسلها الهدفُ المرفقين به.
class ConcreteObserverA: Observer {

    func update(subject: Subject) {

        if subject.state < 3 {
            print("ConcreteObserverA: Reacted to the event.\n")
        }
    }
}

class ConcreteObserverB: Observer {

    func update(subject: Subject) {

        if subject.state >= 3 {
            print("ConcreteObserverB: Reacted to the event.\n")
        }
    }
}

/// لنرى الآن كيف سيعمل كل ذلك.
class ObserverConceptual: XCTestCase {

    func testObserverConceptual() {

        let subject = Subject()

        let observer1 = ConcreteObserverA()
        let observer2 = ConcreteObserverB()

        subject.attach(observer1)
        subject.attach(observer2)

        subject.someBusinessLogic()
        subject.someBusinessLogic()
        subject.detach(observer2)
        subject.someBusinessLogic()
    }
}

 Output.txt: نتائج التنفيذ

Subject: Attached an observer.

Subject: Attached an observer.


Subject: I'm doing something important.

Subject: My state has just changed to: 4

Subject: Notifying observers...

ConcreteObserverB: Reacted to the event.


Subject: I'm doing something important.

Subject: My state has just changed to: 2

Subject: Notifying observers...

ConcreteObserverA: Reacted to the event.

Subject: Detached an observer.


Subject: I'm doing something important.

Subject: My state has just changed to: 8

Subject: Notifying observers...

مثال واقعي

Example.swift: مثال واقعي

import XCTest

class ObserverRealWorld: XCTestCase {

    func test() {

        let cartManager = CartManager()

        let navigationBar = UINavigationBar()
        let cartVC = CartViewController()

        cartManager.add(subscriber: navigationBar)
        cartManager.add(subscriber: cartVC)

        let apple = Food(id: 111, name: "Apple", price: 10, calories: 20)
        cartManager.add(product: apple)

        let tShirt = Clothes(id: 222, name: "T-shirt", price: 200, size: "L")
        cartManager.add(product: tShirt)

        cartManager.remove(product: apple)
    }
}

protocol CartSubscriber: CustomStringConvertible {

    func accept(changed cart: [Product])
}

protocol Product {

    var id: Int { get }
    var name: String { get }
    var price: Double { get }

    func isEqual(to product: Product) -> Bool
}

extension Product {

    func isEqual(to product: Product) -> Bool {
        return id == product.id
    }
}

struct Food: Product {

    var id: Int
    var name: String
    var price: Double

    /// خصائص للطعام تحديدًا.
    var calories: Int
}

struct Clothes: Product {

    var id: Int
    var name: String
    var price: Double

    /// خصائص للملابس تحديدًا
    var size: String
}

class CartManager {

    private lazy var cart = [Product]()
    private lazy var subscribers = [CartSubscriber]()

    func add(subscriber: CartSubscriber) {
        print("CartManager: I'am adding a new subscriber: \(subscriber.description)")
        subscribers.append(subscriber)
    }

    func add(product: Product) {
        print("\nCartManager: I'am adding a new product: \(product.name)")
        cart.append(product)
        notifySubscribers()
    }

    func remove(subscriber filter: (CartSubscriber) -> (Bool)) {
        guard let index = subscribers.index(where: filter) else { return }
        subscribers.remove(at: index)
    }

    func remove(product: Product) {
        guard let index = cart.index(where: { $0.isEqual(to: product) }) else { return }
        print("\nCartManager: Product '\(product.name)' is removed from a cart")
        cart.remove(at: index)
        notifySubscribers()
    }

    private func notifySubscribers() {
        subscribers.forEach({ $0.accept(changed: cart) })
    }
}

extension UINavigationBar: CartSubscriber {

    func accept(changed cart: [Product]) {
        print("UINavigationBar: Updating an appearance of navigation items")
    }

    open override var description: String { return "UINavigationBar" }
}

class CartViewController: UIViewController, CartSubscriber {

    func accept(changed cart: [Product]) {
        print("CartViewController: Updating an appearance of a list view with products")
    }

    open override var description: String { return "CartViewController" }
}

 Output.txt: نتائج التنفيذ

CartManager: I'am adding a new subscriber: UINavigationBar
CartManager: I'am adding a new subscriber: CartViewController

CartManager: I'am adding a new product: Apple
UINavigationBar: Updating an appearance of navigation items
CartViewController: Updating an appearance of a list view with products

CartManager: I'am adding a new product: T-shirt
UINavigationBar: Updating an appearance of navigation items
CartViewController: Updating an appearance of a list view with products

CartManager: Product 'Apple' is removed from a cart
UINavigationBar: Updating an appearance of navigation items
CartViewController: Updating an appearance of a list view with products

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

المستوى: ★ ★ ☆

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

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

يمكن ملاحظة النمط من خلال أساليب الاشتراك التي تخزن الكائنات في قائمة وعبر الاستدعاءات إلى أسلوب التحديث المرسَل إلى الكائنات داخل تلك القائمة.

مثال تصوري

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

  • ما الفئات التي يتكون منها؟
  • ما الأدوار التي تلعبها هذه الفئات؟
  • كيف ترتبط عناصر النمط ببعضها؟

 index.ts: مثال تصوري

/**
 * تصرح واجهة الهدف عن مجموعة من الأساليب لإدارة المشتركين.
 */
interface Subject {
    // أرفق مراقبًا إلى الهدف.
    attach(observer: Observer): void;

    // افصل المراقب من الهدف.
    detach(observer: Observer): void;

    // نبه كل المراقبين بالحدث.
    notify(): void;
}

/**
 * يملك الهدف حالة مهمة وينبه المراقبين حين تتغير تلك الحالة.
 */
class ConcreteSubject implements Subject {
    /**
     * @type {number} سنخزن حالة الهدف اللازمة لكل المشتركين في هذا المتغير بداعي التبسيط.
     */
    public state: number;

    /**
     * @type {Observer[]} قائمة بالمشتركين. يمكن تخزين قائمة المشتركين في الأمثلة الواقعية
     * بشكل أشمل كأن تصنف وفق نوع الحدث، إلخ.
     */
    private observers: Observer[] = [];

    /**
     * أساليب إدارة الاشتراك.
     */
    public attach(observer: Observer): void {
        console.log('Subject: Attached an observer.');
        this.observers.push(observer);
    }

    public detach(observer: Observer): void {
        const observerIndex = this.observers.indexOf(observer);
        this.observers.splice(observerIndex, 1);
        console.log('Subject: Detached an observer.');
    }

    /**
     * ابدأ تحديثًا في كل مشترك.
     */
    public notify(): void {
        console.log('Subject: Notifying observers...');
        for (const observer of this.observers) {
            observer.update(this);
        }
    }

    /**
     * عادة ما يمثل منطق الاشتراك جزءًا صغيرًا مما يستطيع الهدف أن يفعله
     * وتحتفظ الأهداف عادة ببعض منطق العمل الهام الذي يشغِّل أسلوب
     * تنبيه كلما أوشك أن يقع حدث مهم، أو بعد وقوعه.
     * happen (or after it).
     */
    public someBusinessLogic(): void {
        console.log('\nSubject: I\'m doing something important.');
        this.state = Math.floor(Math.random() * (10 + 1));

        console.log(`Subject: My state has just changed to: ${this.state}`);
        this.notify();
    }
}

/**
 * تصرح واجهة المراقب عن أسلوب التحديث الذي تستخدمه الأهداف.
 */
interface Observer {
    // Receive update from subject.
    update(subject: Subject): void;
}

/**
 * يتفاعل المراقبون الحقيقيون مع التحديثات التي يرسلها الهدفُ المرفقين به.
 */
class ConcreteObserverA implements Observer {
    public update(subject: Subject): void {
        if (subject.state < 3) {
            console.log('ConcreteObserverA: Reacted to the event.');
        }
    }
}

class ConcreteObserverB implements Observer {
    public update(subject: Subject): void {
        if (subject.state === 0 || subject.state >= 2) {
            console.log('ConcreteObserverB: Reacted to the event.');
        }
    }
}

/**
 * شيفرة العميل.
 */

const subject = new ConcreteSubject();

const observer1 = new ConcreteObserverA();
subject.attach(observer1);

const observer2 = new ConcreteObserverB();
subject.attach(observer2);

subject.someBusinessLogic();
subject.someBusinessLogic();

subject.detach(observer2);

subject.someBusinessLogic();

 Output.txt: نتائج التنفيذ

Subject: Attached an observer.
Subject: Attached an observer.

Subject: I'm doing something important.
Subject: My state has just changed to: 6
Subject: Notifying observers...
ConcreteObserverB: Reacted to the event.

Subject: I'm doing something important.
Subject: My state has just changed to: 1
Subject: Notifying observers...
ConcreteObserverA: Reacted to the event.
Subject: Detached an observer.

Subject: I'm doing something important.
Subject: My state has just changed to: 5
Subject: Notifying observers...

انظر أيضًا

مصادر