نمط Observer
جزء من قسم Object Oriented Programming في رحلة C++ على Coddy — الدرس 94 من 104.
يحدد نمط المراقب (Observer pattern) علاقة تبعية واحد إلى متعدد بين الكائنات: فعندما تتغير حالة كائن واحد (الموضوع)، يتم إخطار جميع التابعين له (المراقبين) تلقائيًا. يُعد هذا النمط مثاليًا لأنظمة الأحداث، أو تحديثات واجهة المستخدم، أو أي سيناريو تحتاج فيه كائنات متعددة إلى التفاعل مع التغييرات.
يتضمن النمط دورين أساسيين: Subject الذي يحتفظ بقائمة من المراقبين ويقوم بإخطارهم، و Observers الذين يقومون بتنفيذ واجهة تحديث:
#include <iostream>
#include <vector>
#include <algorithm>
class Observer {
public:
virtual void update(int value) = 0;
virtual ~Observer() = default;
};
class Subject {
std::vector<Observer*> observers;
int state = 0;
public:
void attach(Observer* obs) {
observers.push_back(obs);
}
void detach(Observer* obs) {
observers.erase(
std::remove(observers.begin(), observers.end(), obs),
observers.end());
}
void setState(int value) {
state = value;
notify();
}
void notify() {
for (Observer* obs : observers) {
obs->update(state);
}
}
};
class Display : public Observer {
std::string name;
public:
Display(const std::string& n) : name(n) {}
void update(int value) override {
std::cout << name << " received: " << value << "\n";
}
};
int main() {
Subject sensor;
Display screen1("Screen1"), screen2("Screen2");
sensor.attach(&screen1);
sensor.attach(&screen2);
sensor.setState(42); // تم إخطار كلتا الشاشتين
}عندما يتم استدعاء setState()، يقوم الموضوع بالتكرار عبر جميع المراقبين المسجلين ويستدعي تابع update() الخاص بهم. يمكن للمراقبين الانضمام أو الانفصال في أي وقت، مما يجعل النظام مرنًا وضعيف الارتباط.
استخدم نمط Observer عندما تتطلب التغييرات في كائن واحد تحديث كائنات أخرى، ولا تريد أن تكون هذه الكائنات مرتبطة ببعضها البعض بشكل وثيق.
التحدي
سهللنقم ببناء نظام مراقبة محطة أرصاد جوية (Weather Station) باستخدام نمط المراقب (Observer pattern). ستقوم بإنشاء محطة أرصاد تتبع بيانات درجة الحرارة وتقوم تلقائياً بإخطار وحدات عرض متعددة كلما تغيرت درجة الحرارة — وهو تطبيق كلاسيكي من واقع الحياة لهذا النمط.
ستقوم بتنظيم الكود الخاص بك عبر ثلاثة ملفات:
Observer.h: تحديد واجهة المراقب (observer interface) ومراقب عرض ملموس.قم بإنشاء فئة مجردة
Observerتحتوي على دالة افتراضية بحتة (pure virtual method) وهيupdate(double temperature)التي تنفذها الكائنات المراقبة لتلقي الإشعارات، بالإضافة إلى مدمر افتراضي (virtual destructor).ثم قم بإنشاء فئة
TemperatureDisplayترث منObserver. كل وحدة عرض لها اسم (string) يتم تعيينه من خلال المنشئ (constructor) الخاص بها. عندما يتم استدعاءupdate()، يجب أن تطبع:[DisplayName]: Temperature is [temperature] degreesWeatherStation.h: إنشاء الموضوع (subject) الذي يحتفظ بدرجة الحرارة ويخطر المراقبين.يجب أن تقوم فئة
WeatherStationالخاصة بك بـ:- تخزين قائمة من مؤشرات المراقبين (observer pointers) ودرجة الحرارة الحالية (قم بتهيئتها إلى
0.0) - امتلاك دالة
attach(Observer* obs)لتسجيل المراقبين - امتلاك دالة
detach(Observer* obs)لإزالة المراقبين - امتلاك دالة
setTemperature(double temp)تقوم بتحديث درجة الحرارة وإخطار جميع المراقبين المتصلين - امتلاك دالة خاصة
notify()تستدعيupdate()لكل مراقب مع درجة الحرارة الحالية
- تخزين قائمة من مؤشرات المراقبين (observer pointers) ودرجة الحرارة الحالية (قم بتهيئتها إلى
main.cpp: عرض نمط المراقب (Observer pattern) قيد التشغيل.اقرأ ثلاثة مدخلات:
- اسم وحدة العرض الأولى (string)
- اسم وحدة العرض الثانية (string)
- قيمة درجة الحرارة (double)
قم بإنشاء
WeatherStationوكائنين من نوعTemperatureDisplayبالأسماء المقدمة. قم بربط كلا العرضين بالمحطة، ثم اضبط درجة الحرارة على القيمة المدخلة. يجب أن يتلقى كلا العرضين التحديث تلقائياً ويطبعا رسائلهما.بعد ذلك، قم بفصل العرض الأول واضبط درجة حرارة جديدة تزيد بمقدار 5 درجات عن القيمة المدخلة. يجب أن يتلقى العرض الثاني فقط هذا التحديث.
على سبيل المثال، مع المدخلات Kitchen و Bedroom و 22.5:
Kitchen: Temperature is 22.5 degrees
Bedroom: Temperature is 22.5 degrees
Bedroom: Temperature is 27.5 degreesمع المدخلات Office و Lobby و 18.0:
Office: Temperature is 18 degrees
Lobby: Temperature is 18 degrees
Lobby: Temperature is 23 degreesلاحظ كيف ينشئ نمط المراقب نظاماً مفككاً (loosely coupled) — لا تحتاج محطة الأرصاد الجوية إلى معرفة أي شيء عن وحدات العرض المحددة، فهي تقوم فقط بإخطار كل من يستمع إليها. عندما تفصل مراقباً، فإنه يتوقف عن تلقي التحديثات تلقائياً.
ورقة مرجعية
يحدد نمط المراقب (Observer pattern) تبعية "واحد إلى متعدد" حيث يقوم الموضوع (subject) بإخطار جميع مراقبيه تلقائيًا عند تغير حالته.
يحتوي هذا النمط على دورين رئيسيين:
- الموضوع (Subject): يحتفظ بقائمة من المراقبين ويخطرهم بتغييرات الحالة
- المراقب (Observer): ينفذ واجهة تحديث لتلقي الإخطارات
التنفيذ الأساسي:
#include <iostream>
#include <vector>
#include <algorithm>
class Observer {
public:
virtual void update(int value) = 0;
virtual ~Observer() = default;
};
class Subject {
std::vector<Observer*> observers;
int state = 0;
public:
void attach(Observer* obs) {
observers.push_back(obs);
}
void detach(Observer* obs) {
observers.erase(
std::remove(observers.begin(), observers.end(), obs),
observers.end());
}
void setState(int value) {
state = value;
notify();
}
void notify() {
for (Observer* obs : observers) {
obs->update(state);
}
}
};
class Display : public Observer {
std::string name;
public:
Display(const std::string& n) : name(n) {}
void update(int value) override {
std::cout << name << " received: " << value << "\n";
}
};الأساليب (Methods) الرئيسية:
attach(): يسجل مراقبًا لتلقي الإخطاراتdetach(): يزيل مراقبًا من قائمة الإخطاراتnotify(): يمر عبر جميع المراقبين ويستدعي أسلوبupdate()الخاص بهم
استخدم نمط المراقب (Observer) عندما تتطلب التغييرات في كائن واحد تحديث كائنات أخرى دون وجود ارتباط وثيق (tight coupling) بينها.
جرّب بنفسك
#include <iostream>
#include <string>
#include "WeatherStation.h"
using namespace std;
int main() {
// قراءة المدخلات
string display1Name, display2Name;
double temperature;
cin >> display1Name;
cin >> display2Name;
cin >> temperature;
// TODO: إنشاء كائن WeatherStation
// TODO: إنشاء كائنين TemperatureDisplay بالأسماء المدخلة
// TODO: ربط كلا الشاشتين بمحطة الأرصاد الجوية
// TODO: ضبط درجة الحرارة على القيمة المدخلة
// (يجب أن تطبع كلتا الشاشتين رسائلهما)
// TODO: فصل الشاشة الأولى
// TODO: ضبط درجة الحرارة لتكون أعلى بـ 5 درجات من القيمة المدخلة
// (يجب أن تطبع الشاشة الثانية فقط)
return 0;
}
يتضمن هذا الدرس اختبارًا قصيرًا. ابدأ الدرس للإجابة عليه وتتبّع تقدمك.
جميع دروس Object Oriented Programming
1أساسيات الـ OOP
الملفات الخارجيةبناء وتجميع (Compilation) لغة C++ملفات الـ Header وملفات الـ Sourceالـ Namespaces والـ Scopeمقدمة في الـ OOP في C++الـ Classes مقابل الـ Objectsالمؤشر 'this'الـ Methods (الدوال الأعضاء)الـ Attributes (بيانات الأعضاء)أساسيات الـ Ctors والـ Dtorsمراجعة - آلة حاسبة بسيطة4خصائص الـ Class
أعضاء الـ Instance مقابل الـ Staticدوال الـ Getters والـ Settersدوال الأعضاء الثابتة (Const)الكلمة المفتاحية Mutableالدوال والمتغيرات الساكنة (Static)الدوال والفئات الصديقة (Friend)مراجعة - مدير الحساب البنكي7الوراثة
الوراثة الأساسيةمستويات الوصول في الوراثةترتيب استدعاء الـ Ctor والـ Dtorإعادة تعريف الدوال (Method Overriding)الدوال الافتراضية والـ VTableالوراثة المتعددةالوراثة الافتراضيةمراجعة - هيكلية الموظفين10نظرة عامة على STL
نظرة عامة وفلسفة STLحاويات STLالـ Iteratorsخوارزميات STLالـ Functors وتعبيرات Lambdaمراجعة - تكرار الكلمات13أنماط التصميم - الجزء الأول
مقدمة في أنماط التصميمنمط Singletonنمط Factory و Abstract Factoryنمط Builderنمط Observerنمط Strategy2إدارة الذاكرة
ذاكرة Stack مقابل Heapالمؤشرات والمراجعالذاكرة الديناميكية (new/delete)المؤشرات الذكية في C++RAII في C++مراجعة - مدير المصفوفات الديناميكية5التغليف (Encapsulation)
محددات الوصول في C++محددات الوصول بشكل متعمقإخفاء المعلوماتStruct مقابل Classالأصناف المتداخلة والداخليةمراجعة - نظام سجلات الطلاب8تعدد الأشكال (Polymorphism)
تعدد الأشكال: وقت التجميع مقابل وقت التشغيلالتحميل الزائد للدوال (Function Overloading)مراجعة الدوال الافتراضية (Virtual Functions)الدوال الافتراضية البحتة (Pure Virtual Functions)الأصناف المجردة (Abstract Classes)تصميم الواجهات (Interface) في C++التحويل الديناميكي (Dynamic Casting) و RTTIملخص - حاسبة الأشكال11مفاهيم OOP المتقدمة
التركيب مقابل الوراثةالـ Mixins عبر CRTPنمط Pimplمحو النوع (Type Erasure)أصناف Enum والأنواع القويةمعالجة الاستثناءات في OOPتسلسلات الاستثناءات المخصصة14أنماط التصميم - الجزء الثاني
نمط الأمرنمط المحولنمط المزيننمط قالب الطريقةنمط الحالةنمط التركيبRAII كنمط3المنشئات والموادم
المنشئ الافتراضيالمنشئ ذو المعاملاتمنشئ النسخمنشئ النقلقوائم تهيئة المنشئالمنشئات المفوضةتعمق في الموادمقاعدة الثلاثة / الخمسة / الصفرمراجعة - فئة String6تحميل العوامل (Operator Overloading)
مقدمة في تحميل العواملتحميل العوامل الحسابيةتحميل عوامل المقارنةعوامل Streamتحميل عامل التعيينتحميل العوامل [] و ()عوامل تحويل النوعمراجعة - Matrix Class9القوالب
قوالب الدوالقوالب الأصنافتخصيص القوالبالقوالب المتغيرةأساسيات SFINAE و Type Traitsمراجعة - الحاويات العامة