Menu
Coddy logo textTech

Beobachtermuster

Teil des Abschnitts Objektorientierte Programmierung der Swift-Journey von Coddy. Lektion 44 von 57.

Beim Beobachter-Muster führt ein Objekt (das Subjekt) eine Liste von Beobachtern und benachrichtigt sie alle, wenn etwas geschieht. Das Subjekt muss nicht wissen, was die Beobachter tun:

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

Ausgabe:

logger saw tapped
counter saw tapped

Beobachter können auch Objekte sein, die einem Protokoll entsprechen. Das Subjekt ruft die Protokollmethode für jeden registrierten Beobachter auf:

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

Ausgabe:

AAPL above 100: 120

Beobachter benötigen normalerweise eine Möglichkeit, das Lauschen zu beenden. Wenn bei der Registrierung eine ID (oder ein Token) zurückgegeben wird, kann der Aufrufer seinen Beobachter später entfernen:

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")

Ausgabe:

A got login
B got login
B got logout

@escaping kennzeichnet eine Closure, die gespeichert und später aufgerufen wird, nachdem die Funktion, die sie empfangen hat, zurückgekehrt ist. Das Beobachtermuster hält das Subjekt und die Beobachter lose gekoppelt: Neue Beobachter können hinzugefügt werden, ohne das Subjekt zu ändern.

func onTap(_ handler: @escaping (String) -> Void) {
    handlers.append(handler)   // gespeichert: muss @escaping sein
}
challenge icon

Aufgabe

Einfach

Vervollständige die Klasse Newsletter, ein Subjekt, das Ausgaben an Abonnenten sendet. subscribe(name:handler:) speichert eine Closure unter einem Namen (eine erneute Anmeldung mit demselben Namen ersetzt sie), unsubscribe(name:) entfernt sie, und publish(_ title:) ruft jeden Handler in alphabetischer Reihenfolge der Namen auf und gibt zurück, wie viele benachrichtigt wurden.

Der bereitgestellte Code liest die Befehle sub <name>, unsub <name> oder pub <title>. Der Handler jedes Abonnenten gibt <name> reads <title> aus; nach jedem pub gibt der Code sent to 2 aus.

Dein Code gehört in Newsletter.swift. main.swift enthält den bereitgestellten Eingabe-/Ausgabecode und darf nicht bearbeitet werden.

Probier es selbst

// Bereitgestellter Ein-/Ausgabecode: belasse ihn wie er ist
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 iconTeste dich selbst

Diese Lektion enthält ein kurzes Quiz. Starte die Lektion, um es zu beantworten und deinen Fortschritt zu speichern.

Alle Lektionen in Objektorientierte Programmierung

Übe selbstständig: Swift-Playground