Menu
CoddyTech

Mẫu Observer

Thuộc phần Lập trình hướng đối tượng trong hành trình Swift của Coddy. Bài học 44 trên 57.

Trong mẫu thiết kế observer, một đối tượng (subject) giữ một danh sách các observer và thông báo cho tất cả khi có chuyện xảy ra. Subject không cần biết các observer làm gì:

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

Đầu ra:

logger saw tapped
counter saw tapped

Observer cũng có thể là các đối tượng tuân theo một giao thức. Đối tượng chủ thể gọi phương thức của giao thức trên từng observer đã đăng ký:

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

Đầu ra:

AAPL above 100: 120

Các đối tượng quan sát thường cần cách ngừng lắng nghe. Trả về một id (hoặc token) khi đăng ký để bên gọi có thể xóa đối tượng quan sát đó sau này:

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

Đầu ra:

A got login
B got login
B got logout

@escaping đánh dấu một closure được lưu lại và gọi sau, khi hàm nhận closure đó đã trả về. Mẫu observer giúp đối tượng chủ thể và các observer ít phụ thuộc vào nhau: bạn có thể thêm observer mới mà không cần thay đổi đối tượng chủ thể.

func onTap(_ handler: @escaping (String) -> Void) {
    handlers.append(handler)   // được lưu lại: phải có @escaping
}
challenge icon

Thử thách

Dễ

Hoàn thiện lớp Newsletter, một đối tượng gửi các bản tin cho người đăng ký. subscribe(name:handler:) lưu một closure theo tên (nếu đăng ký lại với cùng tên, closure mới sẽ thay thế closure cũ), unsubscribe(name:) xóa closure đó, còn publish(_ title:) gọi tất cả handler theo thứ tự tên trong bảng chữ cái và trả về số người đã nhận thông báo.

Code được cung cấp đọc các lệnh sub <name>, unsub <name> hoặc pub <title>. Handler của mỗi người đăng ký in ra <name> reads <title>; sau mỗi lệnh pub, code in ra sent to 2.

Code của bạn nằm trong Newsletter.swift. main.swift chứa code đầu vào/đầu ra được cung cấp và bạn không thể chỉnh sửa tệp này.

Tự làm thử

// Đoạn code đầu vào/đầu ra đã được cung cấp: giữ nguyên
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 iconTự kiểm tra

Bài học này có một bài trắc nghiệm ngắn. Bắt đầu bài học để trả lời và theo dõi tiến độ.

Tất cả bài học trong Lập trình hướng đối tượng

Tự luyện tập: Playground Swift