Menu
Coddy logo textTech

オブザーバーパターン

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 でなければならない
}
challenge icon

チャレンジ

簡単

購読者にニュースレターを送る 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]))")
    }
}
quiz icon腕試し

このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。

オブジェクト指向プログラミングのすべてのレッスン

自分で練習してみよう: Swiftプレイグラウンド