옵저버 패턴
Coddy Swift 여정의 객체 지향 프로그래밍 섹션에 포함된 레슨. 57개 중 44번째.
observer 패턴에서는 객체(subject)가 옵저버 목록을 유지하고, 어떤 일이 발생하면 모든 옵저버에게 알립니다. subject는 옵저버가 무엇을 하는지 알 필요가 없습니다.
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 플레이그라운드