观察者模式
本课属于 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
}挑战
简单完成类 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 在线编辑器