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

نمط Observer

جزء من قسم البرمجة كائنية التوجه في رحلة Swift على Coddy. الدرس 44 من 57.

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

final class Button {
    private var handlers: [(String) -> Void] = []

    func onTap(_ handler: @escaping (String) -> Void) {
        handlers.append(handler)
    }

    func tap() {
        for h in handlers { h("tapped") }
    }
}

let b = Button()
b.onTap { print("logger saw \($0)") }
b.onTap { print("counter saw \($0)") }
b.tap()

الناتج:

logger saw tapped
counter saw tapped

يمكن أن تكون المراقبات أيضًا كائنات تلتزم ببروتوكول. يستدعي الكائن المراقَب طريقة البروتوكول على كل مراقب مسجَّل:

protocol StockObserver: AnyObject {
    func priceChanged(_ symbol: String, to price: Int)
}

final class Stock {
    let symbol: String
    private var observers: [StockObserver] = []
    var price: Int { didSet { observers.forEach { $0.priceChanged(symbol, to: price) } } }
    init(symbol: String, price: Int) { self.symbol = symbol; self.price = price }
    func add(_ o: StockObserver) { observers.append(o) }
}

final class Alert: StockObserver {
    func priceChanged(_ s: String, to p: Int) { if p > 100 { print("\(s) above 100: \(p)") } }
}

let apple = Stock(symbol: "AAPL", price: 90)
apple.add(Alert())
apple.price = 95
apple.price = 120

الناتج:

AAPL above 100: 120

يحتاج المراقبون عادةً إلى طريقة لإيقاف الاستماع. يتيح إرجاع معرّف (أو رمز مميّز) من التسجيل للمتصل إزالة مراقبه لاحقًا:

final class EventBus {
    private var listeners: [Int: (String) -> Void] = [:]
    private var nextId = 0

    func subscribe(_ f: @escaping (String) -> Void) -> Int {
        nextId += 1
        listeners[nextId] = f
        return nextId
    }
    func unsubscribe(_ id: Int) { listeners[id] = nil }
    func publish(_ event: String) {
        for id in listeners.keys.sorted() { listeners[id]!(event) }
    }
}

let bus = EventBus()
let a = bus.subscribe { print("A got \($0)") }
_ = bus.subscribe { print("B got \($0)") }
bus.publish("login")
bus.unsubscribe(a)
bus.publish("logout")

الناتج:

A got login
B got login
B got logout

@escaping تحدد إغلاقًا يتم تخزينه واستدعاؤه لاحقًا، بعد أن تعود الدالة التي استلمته. يحافظ نمط المراقب على ارتباط فضفاض بين الموضوع والمراقبين: يمكن إضافة مراقبين جدد دون تغيير الموضوع.

func onTap(_ handler: @escaping (String) -> Void) {
    handlers.append(handler)   // مخزّن: يجب أن يكون @escaping
}
challenge icon

التحدي

سهل

أكمل الصنف Newsletter، وهو موضوع يرسل الإصدارات إلى المشتركين. تخزّن subscribe(name:handler:) إغلاقًا تحت اسم (ويؤدي الاشتراك مرة أخرى بالاسم نفسه إلى استبداله)، وتزيله unsubscribe(name:)، بينما تستدعي publish(_ title:) كل معالج بالترتيب الأبجدي للاسم وتعيد عدد المعالجات التي تم إخطارها.

يقرأ الكود المزوّد الأوامر sub <name> أو unsub <name> أو pub <title>. يطبع معالج كل مشترك <name> reads <title>؛ وبعد كل pub يطبع الكود sent to 2.

ضع كودك في Newsletter.swift. يحتوي main.swift على كود الإدخال/الإخراج المزوّد ولا يمكن تعديله.

جرّب بنفسك

// كود الإدخال/الإخراج المُزوَّد: أبقِه كما هو
var input: [String] = []
while let line = readLine() { input.append(line) }
let news = Newsletter()
for cmd in input {
    let p = cmd.split(separator: " ", maxSplits: 1).map(String.init)
    switch p[0] {
    case "sub":
        let name = p[1]
        news.subscribe(name: name) { print("\(name) reads \($0)") }
    case "unsub": news.unsubscribe(name: p[1])
    default: print("sent to \(news.publish(p[1]))")
    }
}
quiz iconاختبر نفسك

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

جميع دروس البرمجة كائنية التوجه

تدرّب بنفسك: Playground لـ Swift