設計模式/原則

觀察者模式 (Observer Pattern)

觀察者模式 (Observer Pattern) ,GoF 23種設計模式其一,行為型
又稱為 publish-subscribe (發佈-訂閱) [註1]、dependants (家眷) 模式,
其定義:

Define a one-to-many dependency between objects so that when one object changes state,
all its dependents are notified and updated automatically.

 

定義物件之間一種一對多的依賴關係,當一個物件狀態發生改變時,
所有依賴於他的物件都將自動地得到通知且被更新

 

 
 
[註1]:
許多人認為,現今的實務開發背景,
『觀察者模式』 與 『發佈/訂閱模式』 兩者已是截然不同的名詞。
 


 

簡介

生活中,我們透過不同的『訂閱』機制,來取得最新資訊。
 
譬如:

  • 訂閱報紙/雜誌
  • 訂閱部落格 (還沒訂閱我的,快去旁邊訂 😘)
  • FB: 追蹤粉專 + 開啟通知
  • Line: 加入一些官方帳號 or Bot 為好友 (e.g., Yahoo!新聞、Line每日英文)


 
 
這種 「訂閱」後就能自動收到更新通知 的概念,
即是 觀察者模式 (Observer Pattern)
 
那些被訂閱、被追蹤、被觀察的,稱為 — 主題/目標 (Subject)
而對主題感興趣的我們,則是 — 觀察者 (Observer)
 


 

耦合 (Coupling)

一致性 vs. 緊密耦合

在實作系統時,常會遇到一個問題:

如何保持 — 相關物件之間的一致性 (consistency) ?

 
譬如:
當資料變動時,如何確保所有畫面,正確地顯示最新資料?

 
 
最簡單不外乎是,當資料一變動,便呼叫畫面進行修改:

class User {

    private String name;
    
	...
	...
	
    public void setName(String name) {
        this.name = name;
        onNameChanged(name);
    }

    public void onNameChanged(String name){
        panelA.update(name); // 更改 畫面A
        panelB.changeName(name); // 更改 畫面B
        panelC.updateName(name); // 更改 畫面C
    }
}

 
 
這樣直覺的做法,卻會產生一堆問題:
 

  • 針對具體 的畫面類別 撰寫 操作,而非抽象的介面或類別,違反了 依賴倒置原則 (DIP)
  • 因違反了 DIP,panel A、B、C 的更新 操作沒有一致規範 (如上方範例,操作名稱皆不相同)。
  • 當今天新增了畫面D,又 得修改 此處 程式,違反了 開閉原則 (Open-Closed Principle, OCP)
  • 無法在 執行期 (runtime) 動態新增、刪除畫面。

 
 

再談 依賴倒置原則

觀察者模式 (Observer Pattern) 即是透過實現 依賴倒置原則 ( DIP) 與 開閉原則 (OCP),
以防止『相關物件為保持一致性,而產生之緊密耦合』。
 
主題 (Subject) 狀態發生變化時,
不用假設 訂閱的 觀察者 (Observer) 是誰、有多少個
而是使用 相同的操作介面 通知他們,從而達到鬆散耦合 (loose coupling)
 

『不用假設是誰』– 這就是解耦的關鍵。

 

為何需要解耦?

開發時,遇到其他功能、模組 (資料庫、畫面、網路… etc.),
直接使用或建立 介面 (or 抽象類別) 來進行操作,
具體實作類別交給下屬完成 ㄏㄏ
除了方便開發分工、擴充、更 利於測試

 
才不會 User 類別寫到一半,要先去新增 畫面類別 PanelA,
寫完了 PanelA 再回到 User 類別,真的超沒效率 der~
 
主題 (Subject)觀察者 (Observer) 的依賴得到鬆綁後,
片面的改變就 不會影響對方 (只要介面不變)。
 
如此,便能夠在 執行期 (runtime),
動態 增加、修改、刪除 任意的觀察者,
主題 的程式碼也都 不用修改,滿足了封閉性,真是豪棒棒呢!
 


 

結構


 

  • Subject (主題/目標):
    也就是 被觀察的 (Observable) 的角色,
    提供增刪 觀察者 (Observer) 的操作介面,
    讓 任意數量的 Observer 均可觀察 (訂閱) 此 Subject。
  • Observer (觀察者):
    又稱為 訂閱者 (subscriber),制定 更新的操作 介面
    供 Subject 狀態改變時予以通知
  • ConcreteSubject (具體主題):
    用來 儲存與維護 ConcreteObserver 感興趣的 主題狀態 (subjectState)
    當 subjectState 改變時,通知 已訂閱它的 Observer。
  • ConcreteObserver (具體觀察者):
    持有 指向 ConcreteSubject 物件的 reference,(實務上不一定)
    儲存 觀察者狀態 (observerState)
    實作 Observer 的 更新操作 (Update),
    確保 observerState 與 subjectState 保持一致。

 


 

結構範例 (Java)

 

Subject (主題)

可以是 類別、抽象類別、介面,
除非需提供公共功能,使用上以 介面優先:

interface Subject {

    /**
     * 新增 觀察者
     * @param observer 抽象的觀察者,不需知道其具體類別為何、實作的細節...
     */
    void attach(Observer observer);

    /**
     * 移除 觀察者
     * @param observer 抽象的觀察者,不需知道其具體類別為何、實作的細節...
     */
    void detach(Observer observer);

    /**
     * 當 主題狀態 發生變化
     * 通知所有已訂閱的觀察者
     */
    void notifyObservers();

}

 
 

Observer (觀察者)

這裡模擬了原汁原味的 觀察者模式 :
update 沒有任何參數
 
因 ConcreteObserver,
已經存有 ConcreteSubject 之 reference,
可透過 GetState() 取得所需狀態
 
實務上 update 通常會有其他參數
供 Subject 提供額外的有用資訊。

interface Observer {

    /**
     * 供 Subject 狀態改變時予以通知
     * 以更新狀態
     */
    void update();

}

 
 

ConcreteSubject (具體主題)

可使用各種集合物件,
來儲存 觀察者 的 reference,
這裡使用較為簡易的 ArrayList。
 
實務上需要考慮到 執行緒安全、效能、佇列

import java.util.ArrayList;
import java.util.List;

public class ConcreteSubject implements Subject {


    private String subjectState;

    private List<Observer> observers;


    public ConcreteSubject() {
        observers = new ArrayList<>();
    }

    @Override
    public void attach(Observer observer) {
        observers.add(observer);
    }

    @Override
    public void detach(Observer observer) {
        observers.remove(observer);
    }

    @Override
    public void notifyObservers() {

        System.out.println("-------------------------------------");
        System.out.println("| " + getClass().getSimpleName() + " 通知給所有觀察者更新 |");
        System.out.println("-------------------------------------");
        System.out.println("                 ▼");
        System.out.println();

        observers.forEach(Observer::update); // 尋訪並通知所有 觀察者 進行更新
    }

    public String getState() {
        return subjectState;
    }

    public void setState(String state) {
        this.subjectState = state;
        System.out.println(getClass().getSimpleName() + " 變更狀態");
        System.out.println(subjectState);
        System.out.println();
		
//        notifyObservers(); // 不一定由此做通知
    }

}

 
 

ConcreteObserver (具體觀察者)

只要有心,人人都可以是 ConcreteObserver,
值得注意的是,實務上:
是否該存有 ConcreteSubject 的 refernece
或是僅透過 update() 參數,取得所需狀態 或其 reference?』
 
個人認為 若對 Subject 沒有其他需求,
透過 update() 傳遞較佳,見仁見智囉~

public class ConcreteObserver implements Observer {

    private String observerState;

    private ConcreteSubject concreteSubject; // 持有指向 ConcreteSubject 物件的 reference

    public ConcreteObserverA(ConcreteSubject concreteSubject) {
        this.concreteSubject = concreteSubject;
    }

    @Override
    public void update() {
        this.observerState = concreteSubject.getState();
        System.out.println(getClass().getSimpleName() + " 收到通知");
        System.out.println(observerState);
        System.out.println();
    }
}

 
 

使用範例 Main

public class Main {

    public static void main(String[] args) {
        
        // 實例 具體主題
        ConcreteSubject concreteSubject = new ConcreteSubject();
		
		// 模擬 設置初始訊息 (此時尚無 觀察/訂閱者)
        concreteSubject.setState("消息 1: 年輕人終究還是年輕人"); 


        /*
         * 下方的 concreteSubject.attach(XXX);
         * 需要的參數為 Observer
         * 因此,實例 抽象 或 具體觀察者皆可 (多型 polymorphism)
         */
        ConcreteObserverA observerA = new ConcreteObserverA(concreteSubject); // 實例 具體觀察者 A
        Observer observerB = new ConcreteObserverB(concreteSubject); // 實例 抽象觀察者 B
        Observer observerC = new ConcreteObserverC(concreteSubject); // 實例 抽象觀察者 C

        concreteSubject.attach(observerA); // 觀察者A 訂閱 主題
        concreteSubject.attach(observerB); // 觀察者B 訂閱 主題
        concreteSubject.attach(observerC); // 觀察者C 訂閱 主題

        concreteSubject.setState("消息 2: 太衝動了"); // 變更主題狀態
        concreteSubject.notifyObservers(); // 通知所有 觀察者

    }
}


/*
 *
 * Result:
 *
 * ConcreteSubject 變更狀態
 * 消息 1: 年輕人終究還是年輕人
 *
 * ConcreteSubject 變更狀態
 * 消息 2: 太衝動了
 *
 * -------------------------------------
 * | ConcreteSubject 通知給所有觀察者更新 |
 * -------------------------------------
 *                  ▼
 *
 * ConcreteObserverA 收到通知
 * 消息 2: 太衝動了
 *
 * ConcreteObserverB 收到通知
 * 消息 2: 太衝動了
 *
 * ConcreteObserverC 收到通知
 * 消息 2: 太衝動了
 *
 */

 


 

合作方式 (Collaborations)
& 實作 (Implementation)

 
 

1. 訂閱主題

欲使用 觀察者模式 (Observer Pattern)
首先得進行 訂閱 (註冊、觀察)。
 

一個 Observer 可訂閱許多不同 Subject,
一個 Subject 可以有很多 Observer。

 
譬如,有好幾萬個粉絲專頁 (Subject),
動漫、網美、資訊、旅遊…. etc.
 
身為一個專業的資訊人員
當然會訂閱 動漫 與 網美 粉專囉~
 
可透過此種方式訂閱:

網美粉專.attach(me);

 
 

2. 指定感興趣的變動

大家都知道,
許多粉專的文字內容不是重點,圖片才是重點 😍。
因此,我只想收到有 po 圖片的動態。
 
想實現此種效果,
只要 擴充 Subject 的 登記操作 (Attach)
引進 層面 (aspect) 的觀念。
 
ex:

void attach(Observer observer, Aspect aspect){
...

 
一旦有事件發生,
此主題只會通知對此『層面』感興趣的 Observer 物件。
而 Aspect 要使用何種資料結構,全看主題的複雜度。
 
譬如,可以這樣使用:

粉專.attach(me, 網美粉專.EXIST_IMAGE);

 
 

3. 觸發更新


 
觸發更新 (notifyObservers) 的物件,有兩種可能:

  • Subject 改變狀態 (SetState),自行呼叫 Notify
  • 客戶端,選擇適當的時機呼叫

 
前者的缺點是: 當有一連串狀態設定動作,
就會 引發一連串的 Notify,非常沒有效率。
 
後者雖能完成一連串狀態設定動作 再進行呼叫 (較為彈性),
不過容易忘記,或者在錯誤的時機呼叫。
 
因此,無論採取何種方式,最好都得 註記那些有引發 Notify 的操作
上方範例中,我則使用了後者的觸發機制。
 
 

4. 通知觀察者

範例中, ConcreteSubject 使用 forEach迴圈,
尋訪所有觀察者,並呼叫其更新方法 update()。
 
但是,若 觀察者較多 或是處理時間較長
一個觀察者卡住,剩下的觀察者就得等它 -.-
 
這時,通常會使用 異步 方式處理。
 
 

5. 更新協定: 推&拉模型 (push & pull models)

觀察者 需要維護的資訊 不多,那還 OK~
 
若不是呢?
 
只用簡單的更新協定,
我們 看不出 主題 (Subject) 是哪裡有變化
到底是 「海棠小姐禿頭了?」、「船到公海了?」…,
我們無從得知,只能 自行推測,容易產生許多問題。
 
因此實務上皆會 在 Update() 方法增加參數
說明此次變化的內容,以利觀察者使用。
 
有兩種極端的方式:

『推送模型、索取模型』

 

推送模型 (Push Model)

推送模型 強調 「主題 (Subject) 或多或少知道 觀察者 (Observer) 的個別需求」,
也就是 不管 Observer 要不要,Subject 都會送出最詳盡的資訊、提示。
 
ex:

void update(int id, String content, Image[] imgs, Date date)

 
可以清楚地知道, 某個 粉專 or Blog 發佈了新的動態 (文章),
包含動態的 id、內容、上傳的圖片、時間。
 


 

索取模型 (Pull Model)

索取模型 則是指 Subject 只送出 最少量 的資訊,
不夠的話 Observer 再自行去向 Subject『索取』。
 
ex:

void update(int id)

 
得知某個粉專 發佈了新的動態,
Subject 通知 Observer 此篇動態的 id,
Observer 若還需要其他資料,就透過類似以下方式,自行索取:

concreteSubject.getContentById(id) // 索取內容
concreteSubject.getImagesById(id) // 索取圖片
.....

 
 
p.s. 若好奇 ConcreteSubject 的實例 怎麼來的,上面 具體觀察者 範例提過:
是否該存有 ConcreteSubject 的 refernece
或是僅透過 update() 參數,取得所需狀態 或其 reference?』
見仁見智~
 


 

模型比較:

推送模型 (Push Model),可能會 降低 Observer 的再利用淺力
因為 Subject 對 Observer 參數的假設未必永遠是對的
當需求產生變化,原本的 Update 參數可能會不敷使用,或有多餘的資訊。
 
索取模型 (Pull Model)彈性較高,但 效率較差
若面對的是海量的資料,
Observer 要自力救濟,自行發覺有變動的地方。
 


 

觀察者模式 (Observer Pattern) 總結

 

Java API

Java 本身提供了 觀察者模式 供使用,
其 Subject 為 java.util.Observable;
Observer 為 java.util.Observer
 
不過,你會發現 Observable 是一個『類別』而非『介面』,
想要使用它,必須在設計一個類別繼承它,限制了其使用性,
違反了『多用合成,少用繼承』,因此 實務上鮮少使用!
 
欸…不是啊…不要誤會,我不是針對它,
我是說在座的各位都..
還是有些值得借鑑的地方.. 吧?
 
 

其他

其他知名應用:
java.util.EventListener 的各種 XxxListener,
可說是整個 Swing GUI 的精髓。
 
Android 開發中隨處可見的 ListViewRecyclerView …等 ViewGroup,
各種 Web 前端框架的 Data Binding,例如 Vue.js:
VueJS
 
Android 四大元件之一的 BroadcastReceiver
將訊息透過廣播,傳遞給 已註冊此行為 的接收器,
這也是典型的 觀察者模式。
 
許多程式語言也提供了現成的 API,
例如 C# 的 觀察器設計模式 – MSDN – Microsoft
 
暸解模式的原理,除了使我們更好地使用這些 API,
更幫助我們能依據自身需求,實作出有彈性的系統。
 
開發系統時,只要發現 一個抽象觀念,有 兩種層面
其中 一方的操作 依賴於 另一方的狀態變化
且一發生變化,得進行通知,以 保持物件之間的一致性
不清楚數量有多少個、是誰。
 
就是使用 觀察者模式 (Observer Pattern) 的最佳時機!
 
範例原始檔
 
 

作者: 鄭中勝
喜愛音樂,但不知為何總在打程式 ? 期許能重新審視、整理自身所學,幫助有需要的人。

發表迴響