نمط Observer
جزء من قسم Object Oriented Programming في رحلة GO على Coddy — الدرس 89 من 107.
يحدد نمط المراقب (Observer pattern) علاقة واحد إلى متعدد بين الكائنات. عندما تتغير حالة كائن واحد (الموضوع - subject)، يتم إخطار جميع التابعين له (المراقبين - observers) تلقائياً. هذا مثالي لأنظمة الأحداث، أو الإشعارات، أو أي سيناريو تحتاج فيه مكونات متعددة للتفاعل مع التغييرات.
في لغة Go، نقوم بتنفيذ هذا النمط باستخدام واجهة (interface) للمراقبين (observers) وهيكل موضوع (subject struct) يدير الاشتراكات:
type Observer interface {
Update(event string)
}
type Subject struct {
observers []Observer
}
func (s *Subject) Subscribe(o Observer) {
s.observers = append(s.observers, o)
}
func (s *Subject) Notify(event string) {
for _, observer := range s.observers {
observer.Update(event)
}
}يمكن لأي نوع يقوم بتنفيذ واجهة Observer الاشتراك لتلقي الإشعارات:
type EmailAlert struct{ Address string }
func (e EmailAlert) Update(event string) {
fmt.Printf("Email to %s: %s\n", e.Address, event)
}
type Logger struct{}
func (l Logger) Update(event string) {
fmt.Println("Log:", event)
}يقوم الموضوع بإخطار جميع المراقبين دون معرفة أنواعهم الملموسة:
subject := &Subject{}
subject.Subscribe(EmailAlert{Address: "user@example.com"})
subject.Subscribe(Logger{})
subject.Notify("Order placed")
// بريد إلكتروني إلى user@example.com: Order placed
// سجل: Order placedيفصل هذا النمط الموضوع عن مراقبيه، مما يجعل من السهل إضافة أنواع مراقبين جديدة دون تعديل الكود الحالي. ويُستخدم بشكل شائع في البنى القائمة على الأحداث (event-driven architectures)، وأطر عمل واجهة المستخدم الرسومية (GUI frameworks)، وأنظمة مراسلة النشر/الاشتراك (pub/sub messaging systems).
التحدي
سهللنقم ببناء نظام تنبيه لأسعار الأسهم باستخدام نمط المراقب (Observer pattern)! ستقوم بإنشاء نظام حيث تتلقى خدمات تنبيه متعددة إشعارات تلقائياً كلما تغير سعر السهم — وهو أمر مثالي لتوضيح كيف يمكن للمراقبين التفاعل مع الأحداث دون أن يعرف الموضوع (subject) أنواعهم الملموسة.
ستقوم بتنظيم الكود الخاص بك عبر ثلاثة ملفات:
observer.go: حدد واجهة المراقب (observer interface) وأنواع المراقبين الملموسة.قم بإنشاء واجهة
Observerمع طريقةUpdate(stockName string, price float64)التي تتلقى معلومات السهم عند تغير الأسعار.قم بتنفيذ نوعين من المراقبين:
PriceAlertمع حقلThreshold(float64) — عند التحديث، يقوم بإرجاعALERT: [stockName] at $[price] crossed threshold $[threshold]إذا كان السعر أعلى من العتبة، أوWATCHING: [stockName] at $[price] (threshold: $[threshold])إذا كان أقل.PriceLoggerمع حقلLogName(string) — عند التحديث، يقوم بإرجاع[logName] recorded [stockName]: $[price]
يجب أن يمتلك كلا المراقبين طريقة تعيد سلسلة الاستجابة المنسقة الخاصة بهما.
stock.go: قم بإنشاء الموضوع (subject) الذي يدير المراقبين ويخطرهم بتغيرات الأسعار.قم ببناء هيكل
Stockمع حقول لـName(string)، وPrice(float64)، وشريحة (slice) من المراقبين. أضف هذه الطرق:Subscribe(o Observer)لإضافة مراقب.SetPrice(price float64) []stringالتي تقوم بتحديث السعر وإخطار جميع المراقبين، مع إرجاع شريحة من جميع استجاباتهم.
قم أيضاً بإنشاء منشئ
NewStock(name string, initialPrice float64) *Stock.main.go: قم بإعداد السهم الخاص بك مع المراقبين ومحاكاة تغيرات الأسعار.اقرأ اسم السهم والسعر الأولي. ثم اقرأ عدد المراقبين المراد إنشاؤهم. لكل مراقب، اقرأ نوعه (
alertأوlogger) وتكوينه (العتبة للتنبيهات، واسم السجل للمسجلين). قم بضم كل مراقب إلى السهم.أخيراً، اقرأ سعراً جديداً، وقم بتحديث السهم، واطبع استجابة كل مراقب في سطر منفصل.
سيتم توفير المدخلات التالية:
- السطر 1: اسم السهم
- السطر 2: السعر الأولي
- السطر 3: عدد المراقبين
- لكل مراقب: النوع (
alertأوlogger)، ثم قيمة التكوين - السطر الأخير: السعر الجديد المراد تعيينه
قم بتنسيق جميع الأسعار بمنزلتين عشريتين.
على سبيل المثال، إذا كان المعطى:
GOOG
150.00
3
alert
155.00
logger
TradeLog
alert
140.00
160.50يجب أن يكون مخرجك:
ALERT: GOOG at $160.50 crossed threshold $155.00
TradeLog recorded GOOG: $160.50
ALERT: GOOG at $160.50 crossed threshold $140.00وإذا كان المعطى:
AAPL
180.00
2
logger
DailyLog
alert
200.00
175.25يجب أن يكون مخرجك:
DailyLog recorded AAPL: $175.25
WATCHING: AAPL at $175.25 (threshold: $200.00)لاحظ كيف أن Stock لا يعرف ما إذا كان يخطر تنبيهات أو مسجلات — فهو ببساطة يستدعي Update على كل مراقب. هذا الفصل (decoupling) يجعل من السهل إضافة أنواع مراقبين جديدة دون تعديل كود Stock!
ورقة مرجعية
يحدد نمط المراقب (Observer pattern) علاقة واحد إلى متعدد حيث يقوم الموضوع (subject) بإخطار عدة مراقبين (observers) تلقائيًا عندما تتغير حالته. هذا يفصل الموضوع عن مراقبيه، مما يجعله مثاليًا لأنظمة الأحداث والإشعارات.
الهيكل الأساسي
قم بتعريف واجهة مراقب (observer interface) وموضوع (subject) يدير الاشتراكات:
type Observer interface {
Update(event string)
}
type Subject struct {
observers []Observer
}
func (s *Subject) Subscribe(o Observer) {
s.observers = append(s.observers, o)
}
func (s *Subject) Notify(event string) {
for _, observer := range s.observers {
observer.Update(event)
}
}تنفيذ المراقبين
يمكن لأي نوع يقوم بتنفيذ واجهة Observer الاشتراك:
type EmailAlert struct{ Address string }
func (e EmailAlert) Update(event string) {
fmt.Printf("Email to %s: %s\n", e.Address, event)
}
type Logger struct{}
func (l Logger) Update(event string) {
fmt.Println("Log:", event)
}الاستخدام
قم بتسجيل المراقبين وإخطارهم بالتغييرات:
subject := &Subject{}
subject.Subscribe(EmailAlert{Address: "user@example.com"})
subject.Subscribe(Logger{})
subject.Notify("Order placed")
// Email to user@example.com: Order placed
// Log: Order placedيقوم الموضوع بإخطار جميع المراقبين دون معرفة أنواعهم المحددة (concrete types)، مما يسهل إضافة أنواع مراقبين جديدة دون تعديل الكود الحالي.
جرّب بنفسك
package main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
func main() {
reader := bufio.NewReader(os.Stdin)
// قراءة اسم السهم
stockName, _ := reader.ReadString('\n')
stockName = strings.TrimSpace(stockName)
// قراءة السعر الأولي
initialPriceStr, _ := reader.ReadString('\n')
initialPrice, _ := strconv.ParseFloat(strings.TrimSpace(initialPriceStr), 64)
// قراءة عدد المراقبين
numObserversStr, _ := reader.ReadString('\n')
numObservers, _ := strconv.Atoi(strings.TrimSpace(numObserversStr))
// TODO: إنشاء Stock جديد باستخدام منشئ NewStock
// TODO: التكرار عبر كل مراقب
for i := 0; i < numObservers; i++ {
// قراءة نوع المراقب ("alert" أو "logger")
observerType, _ := reader.ReadString('\n')
observerType = strings.TrimSpace(observerType)
// قراءة قيمة التكوين
configValue, _ := reader.ReadString('\n')
configValue = strings.TrimSpace(configValue)
// TODO: إنشاء المراقب المناسب بناءً على النوع
// - بالنسبة لـ "alert": قم بتحليل configValue كعتبة float64، وأنشئ PriceAlert
// - بالنسبة لـ "logger": استخدم configValue كـ LogName، وأنشئ PriceLogger
// TODO: تسجيل المراقب في السهم
}
// قراءة السعر الجديد
newPriceStr, _ := reader.ReadString('\n')
newPrice, _ := strconv.ParseFloat(strings.TrimSpace(newPriceStr), 64)
// TODO: تعيين السعر الجديد والحصول على الاستجابات
// TODO: طباعة كل استجابة في سطر خاص بها
}
يتضمن هذا الدرس اختبارًا قصيرًا. ابدأ الدرس للإجابة عليه وتتبّع تقدمك.
جميع دروس Object Oriented Programming
1أساسيات الـ OOP في Go
الملفات الخارجيةمساحة العمل والـ Modules في Goالـ Packages والـ Importsالأسماء المصدرة مقابل غير المصدرةمقدمة إلى الـ OOP في Goالـ Structs كـ Classesتعريف الـ Methods في الـ Structsالـ Pointer Receivers مقابل الـ Value Receiversتهيئة الـ Structدوال الـ Constructorمراجعة - آلة حاسبة بسيطة4الواجهات
مقدمة في الواجهاتالتنفيذ الضمنيالواجهة كعقدالواجهة الفارغة (any)تأكيد النوع (Type Assertion)تبديل النوع (Type Switch)تركيب الواجهاتواجهات Stringer و Errorمراجعة - حاسبة الأشكال7التغليف
الحقول المصدرة وغير المصدرةالتغليف على مستوى الحزمةدوال الـ Getter والـ Setterإخفاء المعلومات في Goملخص - سجلات الطلاب10الأنواع العامة (Go 1.18+)
مقدمة في الأنواع العامةمعاملات الأنواعقيود الأنواعالـ Structs العامةحل بديل للـ Methods العامةملخص - المجموعات العامة13أنماط التصميم - الجزء الأول
مقدمة في أنماط التصميمنمط Singletonنمط Factoryنمط Abstract Factoryنمط Observerنمط Strategy2تعمق في الأنواع و Structs
الأنواع الأساسية والمركبةتعريفات الأنواع المخصصةStruct TagsStructs مجهولةStructs متداخلةالقيم الصفرية والافتراضيةمراجعة - دفتر العناوين5التركيب بدلاً من الوراثة
لماذا لا تدعم Go الوراثةأساسيات تضمين الـ Structترقية الـ Methodتضمين عدة Structsالتضمين مقابل التجميعحجب الـ Methods المضمنةملخص - الهيكل الهرمي للموظفين8معالجة الأخطاء و OOP
واجهة errorأنواع الأخطاء المخصصةتغليف الأخطاء (fmt.Errorf)أخطاء Sentinelerrors.Is() و errors.As()Panic و Defer و Recoverملخص - File Parser11المكتبة القياسية والبرمجة كائنية التوجه (OOP)
io.Reader و io.Writerواجهة sort.Interfaceواجهة fmt.Stringerencoding/json مع Structsواجهة http.Handlerمراجعة - نماذج REST API14أنماط التصميم - الجزء الثاني
نمط الأمرنمط المحولنمط المزيننمط طريقة القالبنمط الحالةنمط التركيبMiddleware كنمط مزين3المؤشرات والذاكرة
أساسيات المؤشرات في Goالمؤشرات إلى الـ Structsالتمرير بالقيمة مقابل التمرير بالمرجعدالة ()newGarbage Collection في Goملخص - بناء Linked List6تعدد الأشكال في Go
تعدد الأشكال عبر InterfacesDuck Typing في Goقواعد استيفاء الـ Interfaceمجموعات متعددة الأشكالحقن التبعيةمراجعة - معالج الدفع9التزامن والبرمجة كائنية التوجه (OOP)
أساسيات الـ Goroutinesالقنوات (Channels) والاتصالالقنوات المخزنة (Buffered) مقابل غير المخزنةجملة Selectsync.Mutex و sync.RWMutexsync.WaitGroupتصميم الـ Structs الآمنة للخيوط (Thread-Safe)مراجعة - Worker Pool