跳至主要内容

Observer Pattern (觀察者模式)

觀察者模式是一種行為設計模式,定義物件之間的一對多依賴關係,當一個物件改變狀態時,依賴它的物件都會自動收到通知和更新。

觀察者模式結構

觀察者模式結構

觀察者模式的應用場景

  1. 當一個對象狀態改變時,需要改變其他對象,或實際對象是事先未知的或動態變化的

    圖形介面中,需要按鈕類允許客戶在按下按鈕時注入自己定義的程式碼,透過訂閱機制(addEventListener),讓用戶,這樣當用戶按下時就會觸發程式碼

  2. 應用中的一些對象必須觀察其他對象時,但僅能在有限時間內或特定情況下使用

    訂閱列表是動態的,觀察者可以隨時加入或離開列表

優缺點

優點

  1. 開放封閉原則。無須修改 Subject 程式,就可以引入新的觀察者類別(如果是 Subject 介面則可輕鬆引入 Subject 類)
  2. 可以在執行期間建立對象之間的關係

缺點

  1. 觀察者的通知順序是隨機的
  2. 觀察者很多的時候,通知所有觀察者會很花時間
  3. 如果觀察者和觀察目標之間有循環依賴,可能導致系統崩潰
  4. 觀察者不知道其他觀察者的存在,依賴準則的定義或維護不當,容易引起錯誤的更新

原始的模型架構與需求

  1. 我們有一個 weatherData,他儲存氣象局最新的測量數據(氣溫、濕度、溫度),每當他改變的時候,我們的數值就要改變
  2. 我們有多個畫面(目前的天氣、統計數據、天氣預測...)

原始的模型架構

錯誤寫法

public void measurementsChanged() {
float temperature = getTemperature();
float humidity = getHumidity();
float pressure = getPressure();

/* 看起來下面這段式會改變的,需要封裝起來 */
currentConditionsDisplay.update(temperature, humidity, pressure); // 傳入參數未來會越來越多
statisticsDisplay.update(temperature, humidity, pressure); // 不一定每個畫面都需要所有觀測資料
forecastDisplay.update(temperature, humidity, pressure);
// 寫成具體實作後,我們就無法在不修改程式的情況下,加入或移除其他元素
}
警告

傳入參數未來可能會越來越多

警告

不一定每個畫面都需要所有觀測資料

警告

違反開放封閉原則,我們就無法在不修改程式的情況下,加入或移除其他元素

對象(SUBJECT) + 觀察者(OBSERVER) = 觀察者模式

observer 有時又稱作 subscriber、listener

備註

觀察者模式定義物件之間的一對多依賴關係,當一個物件改變狀態時,依賴它的物件都會自動收到通知和更新。

發布者訂閱者

觀察者模式是鬆耦合

  1. Subject 只知道觀察者實作了某個介面(Observer 介面)
  2. 我們可以隨時加入新的觀察者
  3. 如果要加入新的觀察者類型,我們完全不需要修改 Subject
  4. 可以重複利用 Subject 或觀察者,又不會影響到對方
  5. 修改 Subject 或觀察者,都不會影響到對方
資訊

努力為彼此互動的物件做出鬆耦合的設計

使用觀察模式後的設計

設計氣象站

程式碼實作

  1. 針對 Subject 建立介面,包和 register、remove、notify
  2. 針對 Observer 建立介面,讓所有觀察者實作
public interface Subject {
public void registerObserver(Observer o); // 接收Observer引數,註冊Observer
public void removeObserver(Observer o); // 接收Observer引數,移除Observer
public void notifyObservers(); // Subject改變時,這個方法會被呼叫,藉此通知觀察者
}

// ----------------------------------------------------------------

public interface Observer {
public void update(float temp, float humidity, float pressure); // 這些測量數據改變時,Observer從Subject取得的狀態值
}

// ----------------------------------------------------------------

public interface DisplayElement {
public void display(); // 想要顯示的元素時,呼叫他
}
  1. 讓 WeatherData 實作 Subject 介面
public class WeatherData implements Subject {
private List<Observer> observers; // 保存觀察者的引用對象
private float temperature;
private float humidity;
private float pressure;

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

public void registerObserver(Observer o) {
observers.add(o); //註冊時加入到List的最後
}

public void removeObserver(Observer o) {
observers.remove(o); // 退出時從List中移除
}

public void notifyObservers() {
for (Observer observer : observers) { // 所有觀察者皆實作update方法,在這裡將最新狀態傳給每一個觀察者
observer.update(temperature, humidity, pressure);
}
}

public void measurementsChanged() {
notifyObservers(); // 當氣象站取得新的資料時,通知Observer
}

public void setMeasurements(float temperature, float humidity, float pressure) {// 測試用,假設數據更新了
this.temperature = temperature;
this.humidity = humidity;
this.pressure = pressure;
measurementsChanged();
}

public float getTemperature() {
return temperature; // pull model使用
}

public float getHumidity() {
return humidity; // pull model使用
}

public float getPressure() {
return pressure; // pull model使用
}

}

  1. 取其中一個顯示元素當作例子,他實作 Observer 介面,可以從 WeatherData 物件收到變更。
public class CurrentConditionsDisplay implements Observer, DisplayElement {
private float temperature;
private float humidity;
private WeatherData weatherData; // 未來想要退出訂閱時可以使用到(不一定要)

public CurrentConditionsDisplay(WeatherData weatherData) {
this.weatherData = weatherData;
weatherData.registerObserver(this); // 透過建構子,將畫面註冊為觀察者
}

public void update(float temperature, float humidity, float pressure) {
this.temperature = temperature;
this.humidity = humidity;
display(); // 在MVC的地方有更好的寫法
}

public void display() {
System.out.println("Current conditions: " + temperature
+ "F degrees and " + humidity + "% humidity");
}
}
  1. 寫一個測試項,確認訂閱會收到訊息,取消訂閱就不再收到訊息
public class WeatherStation {

public static void main(String[] args) {
WeatherData weatherData = new WeatherData();

CurrentConditionsDisplay currentDisplay =
new CurrentConditionsDisplay(weatherData); // 透過建構子的方式,在方法裡面對Subject註冊自己

weatherData.setMeasurements(80, 65, 30.4f); //數據更新,會收到update,並display
weatherData.removeObserver(currentDisplay); //退出訂閱,不在收到通知
weatherData.setMeasurements(62, 90, 28.1f); //數據更新,不會收到update
}
}

Pull Model / Pull Model

  • Push model
    • 推送所有資料給 Observer
    • Subject 要知道 Observer 需要什麼,彈性較差
    • Observer 會接收到不必要的資料
    • 好處是不需要保留 Subject 的引用
public class CurrentConditionsDisplay implements Observer, DisplayElement {
private float temperature;
private float humidity;

public void update(float temperature, float humidity, float pressure) { // Observer會接收到不必要的資料,例如pressure
this.temperature = temperature;
this.humidity = humidity;
display();
}

public void display() {...}
}

PushModel

  • Pull model
    • 提供必要的資料或其來源(如 data id 或 subject 本身)給 Observer,由 Observer 自行取得相對關資料
    • 每個 Observer 都要重新取得資料,效率較差
    • 壞處是需要保留 Subject 的引用
// Subject
public void notifyObservers() {
for (Observer observer : observers) {
observer.update(); // 不傳遞參數
}
}

// ----------------------------------------------------------------

// Object interface
public interface Observer {
public void update(); // 沒有參數
}

// Object
public void update() {
this.temperature = weatherData.getTemperature();
this.humidity = weatherData.getHumidity();
display();
}

PullModel

同步/非同步?

NotifyObserver()的實作方式通常是走訪 observers 成員,並且呼叫他們的 Update(),通知他們做事。但若 Update()會執行很久,會導致整個通知過程會很慢。因為 Subject 必須等 Observer 做事完後,才會繼續通知下一個 Observer.

AsyncOrSync

觀察者模式(Observer Pattern) vs 發布/訂閱模式(Publish/Subscribe Pattern)

比較觀察者模式(Observer Pattern)發布/訂閱模式(Publish/Subscribe Pattern)
模式類別Design PatternMessaging Pattern
知道對方觀察者知道 Subject 的,Subject 也一直保持對觀察者進行記錄發布者和訂閱者不知道對方的存在,只透過消息代理進行通訊
耦合性相對耦合組件是鬆散耦合
同步大多是同步(synchronous)大多是異步(asynchronous)(消息對列)

發布/訂閱模式

發布/訂閱模式

  1. 模組的解偶

    發佈者(Publisher)和訂閱者(Subscriber)之間,透過中間人(broker)或 Message/Event Bus 來解偶

    就像訂閱某個粉專,訂閱者不需要知道發文的小編是誰

  2. 時間的解偶

    發佈訊息時,訂閱者不一定在線上,採用先存再送(store-and-forward)的機制

觀察者模式 + 中介者模式

警告

一般的觀察者模式 Subject 仍需要保留 Observer 的引用,無法真正解偶

觀察者模式 + 中介者模式

  1. 封裝複雜的更新語意。當目標與觀察者的依賴關係特別複雜時,可能需要一個維護這些關係的對象,稱作更改管理器(ChangeManager)
  2. ChangeManager 是一個 Mediator(中介者)模式的實例,通常是一個 Singleton(單例)

ChangeManager 有三個職責:

  1. 將一個 Subject 映射到他的觀察者,並提供一個介面來維護這個映射,這就不用由 Subject 來維護對觀察者的引用
  2. 定義一個特定的更新策略
  3. 根據一個目標請求,更新所有依賴於這個目標的觀察者

ChangeManager

  • 當一個觀察者觀察多個目標時,DAGChangeManager 要更好用一些,他可以保證觀察者僅接受一個更新,而不會接受到多個冗余的更新。
  • 當不存在重複更新時,使用 SimpleChangeManager。
  1. 建立 ChangeManager 介面
public interface ChangeManager {

public abstract void register(Subject subject, Observer observer);

public abstract void unregister(Subject subject, Observer observer);

public abstract void notifyObservers(Subject subject);

}
  1. 實現 SimpleChangeManager
public class SimpleChangeManager implements ChangeManager { // 通常會做成Singleton

Map<Subject, List<Observer>> mapping; // 記錄每一個Subject對應多個的Observer

public SimpleChangeManager(){
mapping = new HashMap<Subject, List<Observer>>();
}

@Override
public void register(Subject subject, Observer observer){
if(mapping.containsKey(subject)){
mapping.get(subject).add(observer);
}
else{
mapping.put(subject, new ArrayList<Observer>(Arrays.asList(observer)));
}
}

@Override
public void unregister(Subject subject, Observer observer){
if(mapping.containsKey(subject)){
if(mapping.get(subject).contains(observer)){
mapping.get(subject).remove(observer);
}
}
}

@Override
public void notifyObservers(Subject subject){
for (Observer observer : mapping.get(subject)) {
observer.update(subject);
}
}
}
  1. 實現 Subject 類別,並保留 ChangeManager 實例的引用
public class ConcreteSubjectWithChangeManager implements Subject {

private ChangeManager changeManager; // 保留 ChangeManager 實例的引用

public ConcreteSubjectWithChangeManager(ChangeManager changeManager) {
this.changeManager = changeManager;
}

@Override
public void registerObserver(Observer observer) {
changeManager.register(this, observer);
}

@Override
public void removeObserver(Observer observer) {
changeManager.unregister(this, observer);
}

@Override
public void notifyObservers() {
changeManager.notifyObservers(this);
}
}