觀察者模式可以讓物件了解資料變化的情況。
物件甚至可以在執行期間決定是否要繼續被通知,又或者是可以主動去詢問資料的狀態。
在此模式中也會了解一對多,以及物件鬆綁的意義是如何。
以氣象監測系統的概況來當做例子
假設系統中有三個組成要件:
(1) 氣象站: 獲取實際氣象的物理裝置,假設有三個:溫度,濕度,壓力感應
(2) Weather Data物件: 追蹤來自氣象站的資料,並且顯示在佈告版上
(3) 佈告版: 將Weather Data物件給予的資料呈現出來
整個例子會有,一個氣象站(產出假的氣象資料),Weather Data物件(獲取氣象資料並通知佈告版),佈告版將拿到的資料給呈現出來
沒使用觀察者模式 Observer Pattern的情況
初學者會很直覺的寫出這樣的程式架構:
佈告欄
CurrentConditionsDisplay.class
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| public class CurrentConditionsDisplay { private int temp; private int humidity; private int pressure; public void update() { this.temp = temp; this.humidity = humidity; this.pressure = pressure; } public void display() { System.out.println(temp); System.out.println(humidity); System.out.println(pressure); } }
|
WeatherData.class
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| public class WeatherData { CurrentConditionsDisplay currentConditionsDisplay; StatisticsDisplay statisticsDisplay; public WeatherData( CurrentConditionsDisplay currentConditionsDisplay, StatisticsDisplay statisticsDisplay ) { this.currentConditionsDisplay = currentConditionsDisplay; this.statisticsDisplay = statisticsDisplay; } public int getTemperature() {...} public int getHumidity() {...} public int getPressure() {...} public void measurementsChanged() { float temp = getTemperature(); float humidity = getHumidity(); float pressure = getPressure(); currentConditionsDisplay.update(temp, humidity, pressure); statisticsDisplay.update(temp, humidity, pressure); } }
|
但是以上程式結構會有耦合性的狀況:
所以接下來來了解觀察者模式的內涵
觀察者模式解析
定義了物件之間一對多關係,如此一來,當一個物件改變狀態時,其他相依者都會收到通知並自動做改變
其示意圖如下:
主題與觀察者們定義了一對多的關係
若要實踐出可以隔離主題和觀察者們的方式,以 Subject
介面和Observer
介面最為常見
在這張圖要注意一個重點是,由於現在已經針對介面實作,現在的Subject中的註冊Observer都是以註冊"介面"為主!而非是像上面一開始的新手例子是直接針對實踐而寫
如此一來如果要在新增一個佈告欄叫做ForecastDisplay,直接實踐Observer就好,這樣就不用動到實踐Subject介面的WeatherData之程式碼
以觀察者模式來重寫氣象監測系統
Subject.interface
1 2 3 4 5
| public interface Subject { void registerObserver(Observer o); void removeObserver(Observer o); void notifyObservers(); }
|
ObserverInterface
1 2 3
| public interface Observer { void update(float temperature, float humidity, float pressure); }
|
WeatherData.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| import java.util.ArrayList;
public class WeatherData implements Subject{ private ArrayList observers; private float temperature; private float humidity; private float pressure;
public WeatherData() { observers = new ArrayList(); }
public void registerObserver(Observer o) { int i = observers.indexOf(o); observers.add(o); }
public void removeObserver(Observer o) { int i = observers.indexOf(o); if(i>=0) { observers.remove(i); } }
public void notifyObservers() { for(int i=0; i<observers.size(); i++) { Observer observer = (Observer)observers.get(i); observer.update(temperature, humidity, pressure); } }
public void measurementsChanged() { notifyObservers(); }
public void setMeasurements( float temperature, float humidity, float pressure) { this.temperature = temperature; this.humidity = humidity; this.pressure = pressure; measurementsChanged(); } }
|
這裡我們只實踐一個佈告欄 CurrentConditionDisplay
CurrentConditionDisplay.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| public class CurrentConditionDisplay implements Observer { private float temperature; private float humidity; private float pressure; private Subject weatherData;
public CurrentConditionDisplay(Subject weatherData) { this.weatherData = weatherData; weatherData.registerObserver(this); }
public void update(float temperature, float humidity, float pressure) { this.temperature = temperature; this.humidity = humidity; this.pressure = pressure; display(); }
public void display() { System.out.println("Current condition: "+temperature + "F degrees and " + humidity + "% humidity and "+ pressure + " pressure"); } }
|
執行程式
1 2 3 4 5 6 7 8 9 10
| public class Main {
public static void main(String[] args) { WeatherData weatherData = new WeatherData();
CurrentConditionDisplay currentDisplay = new CurrentConditionDisplay(weatherData);
weatherData.setMeasurements(80, 64, 30.4f); } }
|
可以看到以下結果
1
| Current condition: 80.0F degrees and 64.0% humidity and 30.4 pressure
|
之後只要透過主題呼叫觀察者的update()的方法,就可以通知新的資料給觀察者
並且透過註冊的方式+只加入針對實踐Observer介面的觀察者,如此一來可以達到分離主題物件與觀察者物件的邏輯,之後新增新的佈告欄就不用動到主題的程式邏輯。
補充
java sdk也有自行提供Observer方法
其中會有setChange()
的方法,主要讓呼叫者定義什麼時候才要通知新的資料給觀察者,避免每次資料一改變就一直通知觀察者。Ex: 如果沒有setChanged的方法,WeahterData物件就會持續不斷的通知觀察者,所以若我們希望溫度差距半度才更新,溫度差距插到半度以上,主題才會呼叫觀察者的update()的方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| setChanged() { changed = ture; }
notifyObservers(Object arg) { if(changed) { for every observer on the list { call update(this. org) } changed = false; } }
notifyObsergers() { notifyObservers(null); }
|