Observer Pattern (觀察者模式)
觀察者模式是一種行為設計模式,定義物件之間的一對多依賴關係,當一個物件改變狀態時,依賴它的物件都會自動收到通知和更新。
觀察者模式結構
觀察者模式的應用場景
- 當一個對象狀態改變時,需要改變其他對象,或實際對象是事先未知的或動態變化的
圖形介面中,需要按鈕類允許客戶在按下按鈕時注入自己定義的程式碼,透過訂閱機制(addEventListener),讓用戶,這樣當用戶按下時就會觸發程式碼
- 應用中的一些對象必須觀察其他對象時,但僅能在有限時間內或特定情況下使用
訂閱列表是動態的,觀察者可以隨時加入或離開列表
優缺點
⭕優點
- 開放封閉原則。無須修改 Subject 程式,就可以引入新的觀察者類別(如果是 Subject 介面則可輕鬆引入 Subject 類)
- 可以在執行期間建立對象之間的關係
❌缺點
- 觀察者的通知順序是隨機的
- 觀察者很多的時候,通知所有觀察者會很花時間
- 如果觀察者和觀察目標之間有循環依賴,可能導致系統崩潰
- 觀察者不知道其他觀察者的存在,依賴準則的定義或維護不當,容易引起錯誤的更新
原始的模型架構與需求
- 我們有一個 weatherData,他儲存氣象局最新的測量數據(氣溫、濕度、溫度),每當他改變的時候,我們的數值就要改變
- 我們有多個畫面(目前的天氣、統計數據、天氣預測...)
錯誤寫法
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
備註
觀察者模式定義物件之間的一對多依賴關係,當一個物件改變狀態時,依賴它的物件都會自動收到通知和更新。
觀察者模式是鬆耦合
- Subject 只知道觀察者實作了某個介面(Observer 介面)
- 我們可以隨時加入新的觀察者
- 如果要加入新的觀察者類型,我們完全不需要修改 Subject
- 可以重複利用 Subject 或觀察者,又不會影響到對方
- 修改 Subject 或觀察者,都不會影響到對方
資訊
努力為彼此互動的物件做出鬆耦合的設計
使用觀察模式後的設計
程式 碼實作
- 針對 Subject 建立介面,包和 register、remove、notify
- 針對 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(); // 想要顯示的元素時,呼叫他
}
- 讓 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使用
}
}
- 取其中一個顯示元素當作例子,他實作 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");
}
}
- 寫一個測試項,確認訂閱會收到訊息,取消訂閱就不再收到訊息
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() {...}
}
- 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();
}
同步/非同步?
NotifyObserver()的實作方式通常是走訪 observers 成員,並且呼叫他們的 Update(),通知他們做事。但若 Update()會執行很久,會導致整個通知過程會很慢。因為 Subject 必須等 Observer 做事完後,才會繼續通知下一個 Observer.
觀察者模式(Observer Pattern) vs 發布/訂閱模式(Publish/Subscribe Pattern)
比較 | 觀察者模式(Observer Pattern) | 發布/訂閱模式(Publish/Subscribe Pattern) |
---|---|---|
模式類別 | Design Pattern | Messaging Pattern |
知道對方 | 觀察者知道 Subject 的,Subject 也一直保持對觀察者進行記錄 | 發布者和訂閱者不知道對方的存在,只透過消息代理進行通訊 |
耦合性 | 相對耦合 | 組件是鬆散耦合 |
同步 | 大多是同步(synchronous) | 大多是異步(asynchronous)(消息對列) |
發布/訂閱模式
-
模組的解偶
發佈者(Publisher)和訂閱者(Subscriber)之間,透過中間人(broker)或 Message/Event Bus 來解偶
就像訂閱某個粉專,訂閱者不需要知道發文的小編是誰
-
時間的解偶
發佈訊息時,訂閱者不一定在線上,採用先存再送(store-and-forward)的機制
觀察者模式 + 中介者模式
警告
一般的觀察者模式 Subject 仍需要保留 Observer 的引用,無法真正解偶
- 封裝複雜的更新語意。當目標與觀察者的依賴關係特別複雜時,可能需要一個維護這些關係的對象,稱作更改管理器(ChangeManager)。
- ChangeManager 是一個 Mediator(中介者)模式的實例,通常是一個 Singleton(單例)
ChangeManager 有三個職責:
- 將一個 Subject 映射到他的觀察者,並提供一個介面來維護這個映射,這就不 用由 Subject 來維護對觀察者的引用
- 定義一個特定的更新策略
- 根據一個目標請求,更新所有依賴於這個目標的觀察者
- 當一個觀察者觀察多個目標時,DAGChangeManager 要更好用一些,他可以保證觀察者僅接受一個更新,而不會接受到多個冗余的更新。
- 當不存在重複更新時,使用 SimpleChangeManager。
- 建立 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);
}
- 實現 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);
}
}
}
- 實現 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);
}
}