Menu
Coddy logo textTech
flag Ar iconالعربيةdown icon

نمط 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).

challenge icon

التحدي

سهل

لنقم ببناء نظام تنبيه لأسعار الأسهم باستخدام نمط المراقب (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: طباعة كل استجابة في سطر خاص بها
}
quiz iconاختبر نفسك

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

جميع دروس Object Oriented Programming