意圖(Intent)#

定義物件之間的一對多依賴關係,當一個物件狀態改變時,所有依賴它的物件都會自動收到通知並更新。

別名(Also Known As)#

  • Dependents
  • Publish-Subscribe

動機(Motivation)#

將系統拆分為多個協作的類別時,常需要維持相關物件之間的一致性。但不應透過緊密耦合來達成一致性,因為這會降低可重用性。

以試算表與長條圖為例:兩者都能顯示同一份應用程式資料,但彼此不認識對方。當使用者修改試算表中的數據時,長條圖立即反映變更,反之亦然。這意味著兩者都依賴於資料物件,需要在資料變動時收到通知。

Observer 模式的核心角色是 Subject(主題)和 Observer(觀察者)。Subject 可以擁有任意數量的 Observer;當 Subject 狀態變動時,所有 Observer 都會被通知。這種互動也稱為 publish-subscribe:Subject 發布通知,不需要知道觀察者是誰。

適用場景(Applicability)#

  • 一個抽象概念有兩個面向,其中一個依賴另一個,將它們封裝在不同物件中可獨立變更與重用
  • 一個物件的變更需要連帶改變其他物件,但不確定有多少物件需要改變
  • 物件需要通知其他物件,但不希望對這些物件做任何假設(避免緊密耦合)

結構(Structure)#

  • Subject 維護一個 Observer 列表,提供 Attach / Detach 介面
  • Observer 定義 Update 介面
  • ConcreteSubject 持有狀態,狀態變動時呼叫 Notify
  • ConcreteObserver 持有對 ConcreteSubject 的參照,透過 Update 同步狀態
classDiagram
    class Subject {
        +Attach(Observer)
        +Detach(Observer)
        +Notify()
    }
    class ConcreteSubject {
        -subjectState
        +GetState()
        +SetState()
    }
    class Observer {
        <<interface>>
        +Update()
    }
    class ConcreteObserver {
        -observerState
        +Update()
    }
    Subject <|-- ConcreteSubject
    Observer <|.. ConcreteObserver
    Subject o--> Observer
    ConcreteObserver --> ConcreteSubject

參與者(Participants)#

參與者範例職責
Subject-知道自己的 Observer,提供附加與解除 Observer 的介面
Observer-定義更新介面,用於接收 Subject 的變更通知
ConcreteSubject-儲存 ConcreteObserver 關心的狀態;狀態改變時通知所有 Observer
ConcreteObserver-維護對 ConcreteSubject 的參照;儲存應與 Subject 保持一致的狀態;實作 Observer 的 Update 介面

協作方式(Collaborations)#

  • ConcreteSubject 在狀態改變時通知所有 Observer
  • ConcreteObserver 收到通知後,向 Subject 查詢資訊以同步自身狀態
  • 發起變更的 Observer 會延遲自身更新,直到收到 Subject 的通知
sequenceDiagram
    participant Client
    participant ConcreteSubject
    participant ConcreteObserver1
    participant ConcreteObserver2
    Client->>ConcreteSubject: SetState()
    ConcreteSubject->>ConcreteSubject: Notify()
    ConcreteSubject->>ConcreteObserver1: Update()
    ConcreteObserver1->>ConcreteSubject: GetState()
    ConcreteSubject->>ConcreteObserver2: Update()
    ConcreteObserver2->>ConcreteSubject: GetState()

優缺點(Consequences)#

優點:

  • Subject 和 Observer 之間的抽象耦合:Subject 只知道 Observer 的抽象介面,不知道具體類別。兩者可以分屬不同抽象層,低層的 Subject 能通知高層的 Observer,維持系統分層完整性
  • 支援廣播通訊:Subject 發出的通知無需指定接收者,自動廣播給所有訂閱者。Observer 可隨時增減,Subject 不受影響

缺點:

  • 意外的連鎖更新:Observer 彼此不知道對方的存在,一個看似無害的操作可能引發 Observer 及其依賴物件的連鎖更新。未妥善定義的依賴關係容易導致多餘的更新,且難以追蹤

簡單的 Update 協定不提供「什麼改變了」的資訊。若缺乏額外協定讓 Observer 發現具體變更,Observer 可能需要花費大量資源才能推導出差異。

實作要點(Implementation)#

  • Subject 到 Observer 的映射:最簡單的方式是在 Subject 中直接儲存 Observer 列表。若 Subject 數量多但 Observer 少,可用關聯查詢(如 hash table)來節省空間
  • 觀察多個 Subject:Observer 可能依賴多個 Subject,此時 Update 介面需讓 Observer 知道是哪個 Subject 發出通知(將 Subject 作為參數傳入)
  • 誰觸發更新
    • 由 Subject 的狀態設定操作自動呼叫 Notify——客戶端不需記得呼叫,但連續操作會造成多次更新
    • 由客戶端在適當時機呼叫 Notify——可避免不必要的中間更新,但增加客戶端的責任
  • 避免懸空參照:Subject 被刪除時應通知 Observer,讓它們重置參照
  • 確保通知前狀態一致:呼叫 Notify 前必須確保 Subject 狀態已完全一致。可利用 Template Method 在抽象 Subject 中定義通知流程,確保子類別覆寫時不會在不一致狀態下觸發通知

Push vs. Pull 模型是關鍵設計決策。Push 模型由 Subject 推送詳細變更資訊,Observer 可能收到不需要的資料;Pull 模型由 Observer 主動查詢,Subject 保持對 Observer 的無知,但可能效率較低。

  • 指定感興趣的事件:擴展註冊介面讓 Observer 只訂閱特定 aspect(事件),提升更新效率
  • ChangeManager:當 Subject 與 Observer 的依賴關係特別複雜時,可引入 ChangeManager 集中管理映射與更新策略。ChangeManager 是 Mediator 模式的實例,通常也是 Singleton

已知應用(Known Uses)#

  • Smalltalk MVC:Model 扮演 Subject,View 扮演 Observer,是最早且最知名的 Observer 應用
  • ET++THINK 類別庫:將 Subject 和 Observer 介面放在所有類別的父類別中,提供通用依賴機制
  • InterViews:明確定義 Observer 和 Observable 類別
  • Andrew Toolkit:使用「view」和「data object」的術語
  • Unidraw:將圖形編輯器物件拆分為 View(Observer)和 Subject

相關模式(Related Patterns)#

  • Mediator:ChangeManager 封裝複雜的更新語義,扮演 Subject 與 Observer 之間的 Mediator
  • Singleton:ChangeManager 通常是全域唯一的,適合使用 Singleton 模式