前言:
這一節開始學習觀察者模式,開始講之前會先像第一節那樣通過一個應用場景來引入該模式。具體場景為:氣象站提供了一個WeatherData對象,該對象可以追蹤獲取天氣的溫度、氣壓、濕度信息,WeatherData對象會隨即更新三個布告板的顯示:目前狀況(溫度、濕度、氣壓)、氣象統計和天氣預報。
1. 基本需求:利用WeatherData對象獲取數據、並更新三個布告板:目前狀況、氣象統計和天氣預報
WeatherData類圖如下:
說明:
GetTemperature()、GetHumidity()、GetPressure()分別用來獲取天氣溫度、濕度氣壓,MeasurementsChanged()當氣象測量更新此方法會被調用。
分析:
- 可以通過WeatherData的Getter方法獲取三個測量值:溫度、濕度、氣壓
- 氣象測量更新時會調用MeasurementsChanged()方法
- 需要實現三個天氣數據布告板:“目前狀況”、“氣象統計”、“天氣預報”,一旦WeatherData測到新值,這些布告也馬上更新
- 此系統可擴展,可以隨意的添加或者刪除任何布告板。
實現:
public class WeatherData { public void MeasurementsChanged() { float temp = GetTemperature(); float humidity = GetHumidity(); float pressure = GetPressure(); currentConditionDisplay.update(temp, humidity, pressure); statisticsDisplay.update(temp, humidity, pressure); forecastDisplay.update(temp, humidity, pressure); } //其他方法 }
反思:
我們的這種實現有何不妥?
結合我們第一節中的一些面向對象的原則,這種實現方式會有如下問題:
l 針對具體實現編程,后續新增或者刪除布告板必須修改程序
l 未將改變的地方封裝起來
這種實現方式與面向對象的一些基本原則是相違背的,目前暫時是實現了用戶的需求,但是在后續不具備可擴展行。
2. 引入觀察者模式
2.1 出版者+訂閱者=觀察者模式
出版者:就相當於“主題”(Subject),訂閱者相當於“觀察者”(Observer)
當出版者(主題)發行新的報紙的時候,所有的觀察者(訂閱者)就可以收到最新的報紙,同時,當新的觀察者(訂閱者)加入時,也可以收到最新的報紙,當觀察者(訂閱者)退訂報紙后,就再也收不到新的報紙。
2.2 定義觀察者模式
觀察者模式定義了對象之間一對多依賴,這樣一來,當一個對象狀態改變時,它的所有依賴者都會收到通知並自動更新。
主題和觀察者定義一對多的關系。觀察者依賴於此主題,只要主題狀態一有變化,觀察者就會被通知,根據通知的風格,觀察者可能因此新值而更新。
觀察者模式:類圖
2.3設計原則:為了交互對象之間的松耦合設計而努力
松耦合的威力
l 當兩個對象之間松耦合,它們依然可以交互,但是不清楚彼此的細節。
l 觀察者模式提供了一種對象設計,讓主題和觀察者之間松耦合。
說明:
- 主題只知道觀察者實現了某個接口(IObserver接口),不需要知道觀察者是誰,或其他細節。
- 任何時候都可以增加或者刪除的觀察者,主題唯一依賴的是一個實現了IObserver接口的對象列表。
- 新的類型觀察者出現時,主題代碼不需要修改,只需要在新類型里實現觀察者接口,然后注冊為觀察者即可。
- 可以獨立的復用主題或觀察者,因為二者松耦合。
- 改變主題或者觀察者,並不會影響另一方。因為二者松耦合。
松耦合的設計之所以能讓我們建立有彈性的OO系統,能夠應對變化,是因為對象之間的相互依賴講到了最低。
3. 利用觀察者模式設計並實現氣象站
3.1 設計氣象站
類圖:
3.2具體實現
3.2.1主題、觀察者、顯示接口
/// Description:對象、觀察者、顯示接口 /// </summary> public interface ISubject { void RegisterObserver(IObserver o);//注冊觀察者 void RemoveObserver(IObserver o);//刪除觀察者 void NotifyObervers();//通知觀察者 } public interface IObserver { void Update(float temp, float humidity, float pressure); } public interface IDisplayElement { void Display(); }
3.2.2 WeatherData類:注冊、刪除、通知觀察者
/// Description:WeatherData 注冊、刪除、通知觀察者 /// </summary> public class WeatherData:ISubject { private ArrayList observers; private float temperature; private float humidity; private float pressure; public WeatherData() { observers = new ArrayList();//初始化obervers,用來存儲注冊的觀察者 } /// <summary> /// 注冊觀察者 /// </summary> /// <param name="o"></param> public void RegisterObserver(IObserver o) { observers.Add(o); } /// <summary> /// 刪除觀察者 /// </summary> /// <param name="o"></param> public void RemoveObserver(IObserver o) { int i = observers.IndexOf(o); if (i >= 0) observers.Remove(o); } /// <summary> /// 通知觀察者 /// </summary> public void NotifyObervers() { foreach (IObserver o in observers) { o.Update(temperature, humidity, pressure); } } /// <summary> /// 當從氣象站得到更新觀測值時,通知觀察者 /// </summary> public void MeasurementsChanged() { NotifyObervers(); } /// <summary> /// /// </summary> /// <param name="temperature"></param> /// <param name="humidity"></param> /// <param name="pressure"></param> public void SetMeasurements(float temperature, float humidity, float pressure) { this.temperature = temperature; this.humidity = humidity; this.pressure = pressure; MeasurementsChanged(); } }
3.2.3 布告板類,實現了IObserver、IDisplayElement接口
/// Description:創建布告板 /// </summary> public class CurrentConditionsDisplay:IObserver,IDisplayElement { private float temperature; private float humidity; private ISubject weatherData; public CurrentConditionsDisplay(ISubject weatherData) { this.weatherData = weatherData; weatherData.RegisterObserver(this); } public void Update(float temperature, float humidity, float pressure) { this.temperature = temperature; this.humidity = humidity; Display(); } public void Display() { Console.WriteLine("Current coditions: " + temperature + "F degress and " + humidity + "% humidity"); } }
3.2.4 測試
WeatherStation.WeatherData weatherData = new WeatherStation.WeatherData(); WeatherStation.CurrentConditionsDisplay currentDisplay = new WeatherStation.CurrentConditionsDisplay(weatherData); weatherData.SetMeasurements(10, 20, 30);
結果如下:
4. Java內置的觀察者模式
Java內置的觀察者模式,許多功能都已經事先准備好了,甚至可以用推(push)或拉(pull)的方式傳送數據。
使用java內置觀察者模式實現氣象站的OO設計類圖,如下:
Java內置觀察者模式與我們在3小節中明顯的差異是WeatherData繼承自Observable類,並集成了一些增加、刪除、通知觀察者的方法。
l 將對象變成觀察者
首先還是要實現Observer(觀察者)接口,其次調用Observable對象的addObserver()方法即可。
l 可觀察者(主題)送出通知
- 先調用setChanged()方法,標記狀態已經改變的事實。
- 調用notifyObservers()方法(該方法有2個,任意一個皆可):notifyObservers()或notifyObservers(Object arg)
l 觀察者接收通知
觀察者實現了Observer的接口,方法簽名如下:
update(Observable o,Object arg)
第一個參數為主題本身,讓觀察者知道是哪個主題通知它的
第二個參數是傳入notifyObservers()的數據對象
如果想用“推”的方式將數據給觀察者,則可以把數據當做數據對象的方式傳給notifyObservers(arg)
如果想用“拉”的方式將數據給觀察者,則需要在update()中,通過WeatherData的getTemperature()等方法獲取對應的氣象值。
缺陷:
Java內置的觀察者模式中Observable是一個類,你必須設計一個類去繼承它。如果某類相同時具有Observable類和另一個超類的行為,就無法實現,因為java不支持多繼承。
同時也限制了Observable的復用能力。
同時,Observable API將setChanged()方法保護了起來,除非繼承自Observable類,否則無法創建Observable實例組合到自己的對象中,也違背了面向對象設計的第二個原則:多用組合,少用繼承。
5. 總結
l OO原則:
封裝變化
多用組合,少用繼承
針對接口編程,不針對實現編程
對交互對象之間的松耦合設計而努力(新的OO原則,松耦合的設計更有彈性,更能應對變化)
l OO模式:
觀察者模式—在對象之間定義一對多的依賴,這樣一來,當一個對象改變狀態,依賴它的對象都會收到通知,並自動更新。