オブザーバーパターン
CoddyのSwiftジャーニー「オブジェクト指向プログラミング」セクションの一部。レッスン 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オブザーバーは通常、リスニングを停止する方法を必要とします。登録時に ID(またはトークン)を返すと、呼び出し側は後から自身のオブザーバーを削除できます:
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 でなければならない
}チャレンジ
簡単購読者にニュースレターを送る subject であるクラス 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]))")
}
}
このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。
オブジェクト指向プログラミングのすべてのレッスン
自分で練習してみよう: Swiftプレイグラウンド