軟體設計

Swift Pub-Sub 模式和觀察者模式有何不同(事件匯流排完整整理)

製作 iOS App 時,總會遇到一次讓人卡住的地方。

閱讀 4 分鐘
Swift Pub-Sub 模式和觀察者模式有何不同(事件匯流排完整整理) 封面圖

製作 iOS App 時,總會遇到一次讓人卡住的地方。

畫面 A 發生的事情,遠處的畫面 B 需要知道;這時就會想:兩者要如何連接?

不斷傳遞 Delegate,很容易讓程式碼像義大利麵一樣糾結。這時就該拿出 Swift Pub-Sub 模式和事件匯流排。

今天就為容易搞混的人整理 Swift Pub-Sub 模式與觀察者模式的差異,以及事件匯流排到底是什麼。

先說結論:觀察者模式是「主體直接知道訂閱者」的結構;Pub-Sub 則在中間加入中介者(事件匯流排),讓雙方互不知道彼此。

理解這一句後,其他內容就會順暢許多。接下來搭配程式碼一步步看看。


先來看看觀察者模式

觀察者模式是「被觀察的主體(Subject)」與「觀察者(Observer)」直接連接的結構。

主體發生變化時,會直接通知已註冊的觀察者。

如果你做過 iOS,應該已經用過了。代表性的例子有 NotificationCenter、Combine 的 @Published,以及 KVO(Key-Value Observing)。

重點在於,被觀察的主體會直接持有自己的訂閱者清單。

// 被觀察的主體直接管理訂閱者
class Subject {
    private var observers: [Observer] = []
    func add(_ o: Observer) { observers.append(o) }
    func notify() { observers.forEach { $0.update() } }
}

結構簡單又直觀,很適合一對多的 1:N 關係。

不過,當規模變大時,要求主體知道觀察者的存在也會成為負擔。


Swift Pub-Sub 模式有什麼不同?

Pub-Sub 模式(發佈-訂閱模式)會再往前一步。

它會在發佈者(Publisher)與訂閱者(Subscriber)之間加入中介者。這個中介者就是事件匯流排,也可以稱為訊息代理程式。

發佈者只要把「發生了這件事」丟到匯流排即可,不需要知道誰會接收。

訂閱者也只要向匯流排註冊「這個事件發生時通知我」,不需要知道是誰發送的。

核心就是雙方完全不知道彼此。這稱為鬆耦合(decoupling)。

直接連接與透過匯流排連接,用圖來看就是這樣
直接連接與透過匯流排連接,用圖來看就是這樣
// 事件匯流排在發佈者與訂閱者之間進行仲介
enum AppEvent { case userLoggedIn(id: String) }

final class EventBus {
    static let shared = EventBus()
    private var handlers: [(AppEvent) -> Void] = []
    func subscribe(_ h: @escaping (AppEvent) -> Void) { handlers.append(h) }
    func publish(_ e: AppEvent) { handlers.forEach { $0(e) } }
}

發佈者只要一行EventBus.shared.publish(.userLoggedIn(id: "123"))就完成了。

登入畫面甚至不需要知道首頁存在。

一行 EventBus 就能完成發佈,就是這麼簡單
一行 EventBus 就能完成發佈,就是這麼簡單

觀察者 vs Pub-Sub:用表格一眼比較

只用文字看起來很相似,所以整理成表格比較差異。

分類 觀察者模式 Pub-Sub 模式
中介者 無(直接連接) 有(事件匯流排)
耦合度 主體知道訂閱者 彼此不知道(鬆耦合)
關係 主要是 1:N 可為 N:N
iOS 範例 KVO, @Published NotificationCenter、事件匯流排
適用情境 觀察特定物件的狀態 遠距模組之間的通訊

有趣的是,NotificationCenter其實更接近 Pub-Sub。

雖然名稱是「Notification」,但發佈者與訂閱者只會透過NotificationCenter這個匯流排相遇,不會直接參考彼此。

所以把「觀察者模式 = NotificationCenter」背起來,會有一點偏差。從概念上看,它更接近 Pub-Sub。


那麼,什麼時候該用哪一種?

分享一下我在實務上採用的判斷標準。

如果要在近距離觀察單一物件的狀態變化,觀察者類型會很方便。Combine 的 @Published 或 SwiftUI 的 @Observable 就很適合。

相反地,如果要通知遠距模組之間,或 App 全域發生的登入、付款完成、網路中斷等事件,事件匯流排會俐落得多。

不過,事件匯流排也不是萬靈丹。

濫用後會陷入「這個事件到底從哪裡來?」的困境,難以追蹤流程。換來鬆耦合的代價,就是可見性稍微降低。

因此我會將畫面內的邏輯交給觀察者(Combine),只有跨越畫面與模組的重要事件才使用事件匯流排處理。

只把重要事件集中到匯流排,流程就會變得清楚
只把重要事件集中到匯流排,流程就會變得清楚

Q. 使用 Combine 就不需要事件匯流排了嗎?

不是。只要使用 Combine 的 PassthroughSubject,就能自行建立簡單的事件匯流排。只是工具重疊,概念並沒有被取代。

Q. Pub-Sub 一定是更好的模式嗎?

不一定。如果是不需要解耦的緊密關係,觀察者更容易閱讀,也更方便除錯。


觀察者模式是直接連接,Pub-Sub 模式是透過中介者連接。差別就是這個。

兩者不是競爭關係,而是依情境選用的工具。下次設計畫面時,先想想雙方需要多了解彼此,就能很快判斷哪種模式適合。

延伸閱讀