《Head First设计模式》第二章笔记 观察者模式
背景
客戶有一個WeatherData對象,負責追蹤溫度、濕度和氣壓等數據。現在客戶給我們提了個需求,讓我們利用WeatherData對象取得數據,并更新三個布告板:目前狀況、氣象統計和天氣預報。
WeatherData對象提供了4個接口:
getTemperature():獲取溫度
getHumidity():獲取濕度
getPressure():獲取氣壓
measurementsChanged():一旦氣象測量更新,此方法會被調用
我們的工作是實現measurementsChanged(),好讓它更新目前狀況、氣象統計、天氣預報的顯示布告板。
我們的工作就是建立一個應用,利用WeatherData對象取得數據,并更新三個布告板:目前狀況、氣象統計和天氣預報。
先看一個錯誤示范
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | public class WeatherData{ ????//實例變量聲明 ????public void measurementsChanged(){ ????????//獲取溫度、濕度、氣壓數據 ????????float temp=getTemperature(); ????????float humidity=getHumidity(); ????????float pressure=getPressure();? ????????//調用update()更新布告板 ????????currentConditionsDisplay.update(temp,humidity,pressure); ????????statisticsDisplay.update(temp,humidity,pressure); ????????forecastDisplay.update(temp,humidity,pressure); } ????//其他WeatherData方法??? } |
回顧第一章中的概念與原則
| 1 2 3 | currentConditionsDisplay.update(temp,humidity,pressure); statisticsDisplay.update(temp,humidity,pressure); forecastDisplay.update(temp,humidity,pressure); |
針對具體實現編程會導致以后增刪布告版時必須修改程序,
對變化的部分必須封裝起來。
認識觀察者模式
先看看報紙和雜志的訂閱是怎么回事:
觀察者模式與訂閱報紙類似,出版者改稱為“主題”(Subject),訂閱者改稱為“觀察者”(Observer)。主題對象管理某些數據。當主題內的數據改變,就會通知觀察者。已經訂閱了主題的觀察者會在主題數據發送改變時收到通知。如果觀察者不想接收新的數據,可以取消訂閱,之后主題數據改變時就不會收到通知。
定義觀察者模式
觀察者模式定義了對象之間的一對多依賴,這樣一來,當一個對象改變時,他的所有依賴者都會收到通知并自動更新。
類圖
主題與觀察者之間松耦合
當兩個對象之間松耦合,它們依然可以交互,但是不太清楚彼此的細節。
觀察者模式提供了一種對象設計,讓主題和觀察者之間松耦合。
關于觀察者的一切,主題只知道觀察者實現了某個接口(也就是Observer接口)。主題不需要知道觀察者的具體類是誰、做了些什么或其他任何細節。
任何時候我們都可以增加新的觀察者。因為主題唯一依賴的東西是一個實現Observer接口的對象列表,所以我們可以隨時增加觀察者。事實上,在運行時我們可以用新的觀察者取代現有的觀察者,主題不會受到任何影響。同樣的,也可以在任何時候刪除某些觀察者。
有新類型的觀察者出現時,主題的代碼不需要修改。我們可以獨立地復用主題或觀察者。如果我們在其他地方需要使用主題或觀察者,可以輕易地復用。因為二者并非緊耦合。
改變主題或觀察者其中一方,并不會影響另一方。因為兩者是松耦合的,所以只要他們之間的接口仍被遵守,我們就可以自由地改變他們。
松耦合的設計之所以能讓我們建立有彈性的OO系統,能夠應對變化,是因為對象之間的互相依賴降到了最低。
?
設計氣象站
思考:我們把WeatherData對象當作主題,把布告板當作觀察者,布告板為了取得信息,就必須先向WeatherData對象注冊。
我們必須記得,每個布告板都有差異,這也就是為什么我們需要一個共同的接口的原因。盡管布告板的類都不一樣,但是它們都應該實現相同的接口,好讓WeathcrData對象能夠知道如何把觀測值送給它們。所以每個布告板都應該有一個大概名為update()的方法,以供WeatherData對象調用,而這個update)方法應該在所有布告板都實現的共同接口里定義。
類圖
實現(一)
java為觀察者模式提供了內置支持。但是,我們暫時不用它,而是先自己動手。雖然,某些時候可以利用Java內置的支持,但是有許多時候,自己建立這一切會更具彈性(況且建立這一切并不是很麻煩)。
所以,讓我們從建立接口開始吧:
Subject接口
| 1 2 3 4 5 6 7 | public interface Subject { ???//這兩個方法都需要一個觀察者作為變量,該觀察者是用來注冊或者被刪除的 ???public void registerObserver(Observer o); ???public void removeObserver(Observer o); ???//主題狀態改變時,此方法被調用,以通知所有的觀察者 ???public void notifyObservers(); } |
Observer接口
| 1 2 3 4 5 | public interface Observer { ?????//當氣象觀測值改變時,主題會把這些狀態值當作方法參數傳遞給觀察者 ?????//所有方法都必須實現update()方法,以實現觀察者接口 ?????public void update(float temp ,float humidity,float pressure); } |
DisplayElement接口
| 1 2 3 4 | public interface DisplayElement { ?????//當布告板需要顯示時調用此方法 ?????public void display(); } |
在WeatherData中實現主題接口
| 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 | public class WeatherData?implements Subject { ?????//ArrayList用于記錄觀察者 ?????private ArrayList observers; ?????private float temperature; ?????private float humidity; ?????private float pressure; ?????public WeatherData() { ?????????// 在構造方法中建立ArrayList????????????????????????? ?????????observers=new ArrayList(); ?????} ?????@Override ?????public void registerObserver(Observer o) { ?????????// 當注冊注冊觀察者時,將它加到ArrayList后面即可 ?????????observers.add(o); ?????} ?????@Override ?????public void removeObserver(Observer o) { ?????????// 當觀察者取消訂閱時,則將它從Arraylist中刪除 ????????int i=observers.indexOf(o); ????????if(i>=0) ??????????????observers.remove(o); ?????} ?????@Override ?????public void notifyObservers() { ?????????// 將狀態告訴每一個觀察者,因為每個觀察者都實現了update()方法 ?????????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 presure) { ?????????//測試用方法 ?????????this.temperature=temperature; ?????????this.humidity=humidity; ?????????this.pressure=presure; ?????????measurementsChanged(); ?????} } |
布告板建立
此處只展示“目前狀況”布告板,另外兩個與此類似。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | public class CurrentConditionsDisplay?implements Observer, DisplayElement { ?????private float temperature; ?????private float humidity; ?????private Subject weatherData; ?????public CurrentConditionsDisplay(Subject weatherData) { ?????????this.weatherData=weatherData; ?????????weatherData.registerObserver(this); ?????} ?????@Override ?????public void display() { ?????????//顯示當前溫濕度狀況 ?????????System. out. println("Current conditions:"+ temperature+"F degrees and"t humidity +"% humidity"); ?????} ?????@Override ?????public void update(float temperature,?float humidity,?float pressure) { ?????????this.temperature=temperature; ?????????this.humidity=humidity; ?????????display(); ?????} } |
測試類
| 1 2 3 4 5 6 7 8 | public static void main(String[] args) { ?????// TODO 自動生成的方法存根 ?????WeatherData weatherData=new WeatherData(); ?????CurrentConditionsDisplay currentiDisplay=new CurrentConditionsDisplay(weatherData); ?????weatherData.setMeasurements(80,?65,?30.4f); ?????weatherData.setMeasurements(82,?75,?29.2f); ?????weatherData.setMeasurements(78,?90,?29.2f); ?} |
輸出
這種觀察者模式總是在數據改變時自動推送全部數據,而觀察者沒有主動獲取數據的方法,因此有時會讓觀察者很困擾,總是收到一大堆數據而觀察者想要的只是其中一個或兩個而已。但讓觀察者自己去取得數據就必須開放權限,這樣又帶來數據安全性問題,或者使用getter方法又會讓需要很多數據的觀察者多次調用才能全部取得想要的數據。
主動推送與觀察者自行獲取都有各自的優缺點,因此Java內置的Observer模式兩種方法都支持!
java.util包(package)內包含最基本的Observer接口與Observable類,這和我們的Subject接口與0bserver接口很相似。
Observer接口與0bscrvable類使用上更方便,因為許多功能都已經事先準備好了。你甚至可以使用推(push)或拉(pull)的方式傳送數據。
使用Java內置觀察者模式類圖
類圖大體和原來差不多,主題接口變為了Observerable類,WeatherData也不再提供addObserver()等方法而是繼承自Observerable.
如何把對象變成觀察者…
如同以前一樣,實現觀察者接口(java.uitl.Observer),然后調用任何Observable對象的addObserver)方法。不想再當觀察者時,調用deleteObserver()方法就可以了。
可觀察者要如何送出通知……
首先,你需要利用擴展java.util.Observable接口產生“可觀察者”類,然后,需個步驟:
①先調用setChanged()方法,標記狀態已經改變的事實。
②然后調用兩種notifyObservers0方法中的一個:
notifyobservers()或 notifyobservers(object arg)
觀察者如何接收通知……
同以前一樣,觀察者實現了更新的方法,但是方法的簽名不太一樣:
如果你想“推”(push)數據給觀察者,你可以把數據當作數據對象傳送給notifyObservers(arg)方法。否則,觀察者就必須從可觀察者對象中“拉”(pull)數據。
其中 setChanged()方法是用來標記狀態是否改變的,好讓notifyObservers()知道當它被調用時應該更新觀察者。如果調用notifyObservers)之前沒有先調用setChanged(),觀察者就“不會”被通知。讓我們看看Observable內部,以了解這一切:
這樣做有其必要性。setChanged()方法可以讓你在更新觀察者時,有更多的彈性,你可以更適當地通知觀察者。
利用內置的支持重做氣象站
首先,把WeatherData改成使用java.util.Observable
| 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 | import java.util.Observable;//導入Observerable import java.util.Observer; public class WeatherData?extends Observable { ????private float temperature; ????private float humidity; ????private float pressure; ????public WeatherData(){}//不再需要為了記住觀察者們而建立數據結構了 ????public void measurementsChanged(){ ????????setChanged(); ????????notifyObservers();//我們沒有調用 notifyObsevets()傳送數據對象,這表示我們采用的做法是“拉”?????? ????} ????public void setMeasurements(float temperature,float humidity,float pressure){ ????????this.temperature=temperature; ????????this.humidity=humidity; ????????this.pressure=pressure; ????????measurementsChanged(); ????} ????public float getTemperature(){ ????????return temperature; ????} ????public float getHumidity(){ ????????return humidity; ????} ????public float getPressure(){ ????????return pressure; ????} } |
重做CurrentConditionsDisplay
| 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 | import java.util.Observable;//導入Observerable import java.util.Observer; //實現java.util.Observer接口 public class CurrentConditionsDisplay?implements Observer, DisplayElement { ?????Observerable observable; ?????private float temperature; ?????private float humidity; ?????public CurrentConditionsDisplay(Observable observable) { ?????????// 將Observer當參數,并將CurrentConditionsDisplay對象登記為觀察者 ?????????this.observable=observable; ?????????observable.addObserver(this); ?????} ?????@Override ?????public void display() { ?????????//顯示當前溫濕度狀況 ?????????System. out. println("Current conditions:"+ temperature+"F degrees and"t humidity +"% humidity"); ?????} ?????@Override ?????public void update(Observable obs, Object arg) { ?????????// TODO 自動生成的方法存根 ?????????if(obs?instanceof WeatherData){ ?????????????WeatherData weatherData=(WeatherData)obs; ?????????????this.temperature=weatherData.getTemperature(); ?????????????this.humidity=weatherData.getHumidity(); ?????????????display(); ?????????} ????} } |
輸出
嗯!你注意到差別了嗎?再看一次……
你會看到相同的計算結果,但是奇怪的地方在于,文字輸出的次序不一樣。怎么會這樣呢?
思考一下.....
...
...
...
...
...
...
...
不要依賴于觀察者被通知的次序
java.uitl.Observable實現了它的notifyObservers()方法,這導致了通知觀察者的次序不同于我們先前的次序。誰也沒有錯,只是雙方選擇不同的方式實現罷了。但是可以確定的是,如果我們的代碼依賴這樣的次序,就是錯的。為什么呢?因為一旦觀察者/可觀察者的實現有所改變,通知次序就會改變,很可能就會產生錯誤的結果。這絕對不是我們所認為的松耦合。
java.util.Observable的弊端
如同你所發現的,可觀察者是一個“類”而不是一個“接口”,更糟的是,它甚沒有實現一個接口。不幸的是,java.util.Observable的實現有許多問題,限制了它的使用和復用。這并不是說它沒有提供有用的功能,只是想提醒大家注意一些事實。
Observable是一個,類到底會造成什么問題
首先,因為Observable是一個“類”,你必須設計一個類繼承它。如果某類想同時具有Observable類和另一個超類的行為,就會陷入兩難,畢竟Java不支持多重繼承。
這限制了Observable的復用潛力(而增加復用潛力不正是我們使用模式最原始的動機嗎?)。
再者,因為沒有Observable接口,所以你無法建立自己的實現,和Java內置的Observer API搭配使用,也無法將java.util的實現換成另一套做法的實現(比方說,Observable將關鍵的方法保護起來如果你看看ObservableAP1,你會發現setChanged)方法被保護起來了(被定義成protected)。那又怎么樣呢?這意味著:除非你繼承自Observable,否則你無法創建Observable實例并組合到你自己的對象中來。這個設計違反了第二個設計原則:“多用組合,少用繼承”。
如果你能夠擴展java.util.Observable,那么Observable“可能”可以符合你的需求。否則,你可能需要像本章開頭的做法那樣自己實現這一整套觀察者模式。
在JDK中,并非只有在java.util中才能找到觀察者模式,共實在JavaBeans和Swing中,也都實現了觀察者模式。
結束
具體實現的代碼,我覺得還是看這本書來的清晰,在本章節末尾,還說到"推模式"和"拉模式",所謂"推模式"就是主題對象傳遞數據,
"拉模式"就是觀察者對象主動得數據,我們要牢記一些設計原則:
封裝變化
多用組合,少用繼承
為交互對象之間的松耦合設計而努力
?
總結
以上是生活随笔為你收集整理的《Head First设计模式》第二章笔记 观察者模式的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Java多线程常用方法
- 下一篇: 队列【数据结构】