Menu
CoddyTech

观察者模式

本课属于 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

挑战

简单

完成类 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 在线编辑器