java设计模式(六)--观察者模式
轉載:設計模式(中文-文字版)
目錄:
觀察者模式是JDK中使用最多的模式之一,非常有用。我們也會一并介紹一對多關系,以及松耦合(對,沒錯,我們說耦合)。有了觀察者,你將會消息靈通。
文章首先從一個案例入手開始介紹。這個案例是這樣的,團隊承包了一個氣象站的氣象發布系統。
工作合約 恭喜貴公司獲選為敝公司建立下一代Internet氣象觀測站! 該氣象站必須建立在我們專利申請中的W eatherD ata對象 上,由W eatherD ata對象負責追蹤目前的天氣狀況(溫度、 濕度、氣壓)。我們希望貴公司能建立一個應用,有三種 布告板,分別顯示目前的狀況、氣象統計及簡單的預報。 當WeatherObject對象獲得最新的測量數據時,三種布告板 必須實時更新。 而且,這是一個可以擴展的氣象站,Weather-O-Rama氣象 站希望公布一組API,好讓其他開發人員可以寫出自己的 氣象布告板,并插入此應用中。我們希望貴公司能提供這 樣的API。 Weathe r - O-Ra m a 氣象站有很好的商業營運模式:一旦客 戶上鉤,他們使用每個布告板都要付錢。最好的部分就是, 為了感謝貴公司建立此系統,我們將以公司的認股權支付 你。 我們期待看到你的設計和應用的alpha版本。 真摯的 Johnny Hurricane——Weather-O-Rama氣象站執行長 附注:我們正通宵整理WeatherData源文件給你們。此系統中的三個部分是氣象站(獲取實際氣象數據的物理裝置)、WeatherData對象(追蹤來自氣象站的數據,并更新布告板)和布告板(顯示目前天氣狀況給用戶看)。如下圖:
1.簡單的實現
再次理清思路:WeatherData對象可以獲取天氣信息(溫度,濕度,氣壓等),然后將這些天氣信息發送給布告板。那么我們可以很簡單的實現:
public class WeatherData { // 實例變量聲明 public void measurementsChanged() { float temp = getTemperature(); float humidity = getHumidity(); float pressure = getPressure(); currentConditionsDisplay.update(temp, humidity, pressure); statisticsDisplay.update(temp, humidity, pressure); forecastDisplay.update(temp, humidity, pressure); } // 這里是其他WeatherData方法 }很直觀,很容易理解,currentConditionsDisplay等代表三塊布告板,通過measurementsChanged方法告訴三個布告板天氣信息。然而,有以下幾個問題:
A. 我們是針對具體實現編程,而非針對接
B. 對于每個新的布告板,我們都得修改代碼。
C. 我們無法在運行時動態地增加(或刪除)布告板
D. 布告板沒有實現一個共同的接口。
E. 我們尚未封裝改變的部分。
F. 我們侵犯了WeatherData類的封裝。
?2.觀察者模式
簡單的認識下觀察者模式。比如報紙,客戶訂閱報紙,然后報社發送報紙到訂閱的用戶手里。報社就是一個主題,而訂閱的客戶就是觀察者。
定義:
觀察者模式定義了對象之間的一對多依賴,這樣一來,當一個對象改變狀態時,它的所有依賴者都會收到通知并自動更新。
主題和觀察者定義了一對多的關系。觀察者依賴于此主題,只要主題狀態一有變化,觀察者就會被通知。根據通知的風格,觀察者可能因此而更新。稍后你會看到,實現觀察者模式的方法不只一種,但是以包含Subject與Observer接口的類設計的做法最常見。
那么,氣象數據更新便可以這樣設計為觀察者模式:
利用觀察者模式,主題是具有狀態的對象,并且可以控制這些狀態。也就是說,有“一個”具有狀態的主題。另一方面,觀察者使用這些狀態,雖然這些狀態并不屬于他們。有許多的觀察者,依賴主題來告訴他們狀態何時改變了。這就產生一個關系:“一個”主題對“多個”觀察者的關系。
從結果往前看很容易理解觀察者模式,但如果接到了需求怎么適配到觀察者模式呢?首先,明顯的是一對多的訂閱模式,主題天氣更新后,觀察者們布告板因此而更新天氣。需要考慮的是布告板是不同的,風格不同,但主題只有一個或者說主題的通知方式只有一個,怎樣才能使布告板統一接受規則呢?那就是接口,只要所有的布告板實現了接口的update方法,通過update方法便可以獲取天氣信息。由此可以獲得最終設計:
剛開始看書看到這些類圖可能有點頭暈,別擔心,那只是因為對內容還不熟悉,只要跟著敲一遍代碼,一切就很容易理解了。下面就是代碼實現了。
3.代碼實現
3.1Subject接口
先定義主題的接口,氣象站有多個,這些氣象站都是一個個的主題,每個主題都可以發送天氣信息給它的訂閱者。為了規范信息發送格式,我們需要規定這些主題必須實現一些固定格式的方法。
/*** 主題:天氣變化管理接口* Created by mrf on 2016/3/1.*/ public interface Subject {//注冊觀察者public void registerObserver(Observer o);//移除觀察者public void removeObserver(Observer o);//當天氣改變時,這個方法會被調用,以通知所有的觀察者public void notifyObservers(); }3.2Observer接口
同樣,觀察者有多個,這些觀察者各不相同,但都要以相同的方式接收天氣信息,那么就必須實現一個觀察者的接口:
/*** 觀察者接口* Created by mrf on 2016/3/1.*/ public interface Observer {/*** 天氣更新通知* @param temp 氣象觀測值:溫度* @param humidity 氣象觀測值:濕度* @param pressure 氣象觀測值:壓強*/public void update(float temp, float humidity, float pressure); }3.3Diplay接口
根據得到的信息展示可以統一成一個方式,即實現Display的顯示方法,當然,各自也可以自己添加自己的方法。
/*** 顯示信息接口:當布告板需要顯示時調用此方法* Created by mrf on 2016/3/1.*/ public interface DisplayElement {public void display(); }3.4WeatherData主題
接下來就是天氣主題,天氣主題需要實現Subject接口以統一信息管理標準
/*** 觀察者模式--天氣更新* 角色:主題發布者* Created by mrf on 2016/3/1.*/ 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) {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();} // WeatherData的其他方法 }3.5一個布告板的實現CurrentConditionsDisplay
那么,最重要的觀察者來了。布告板實現了接受標準和顯示標準兩個接口:
/*** 顯示天氣信息的布告板* 角色:觀察者,訂閱者* 實現Observer接口以從WeatherData中獲取天氣信息* 實現DisplayElement以顯示信息* Created by mrf on 2016/3/1.*/ 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);}public void update(float temperature, float humidity, float pressure) {this.temperature = temperature;this.humidity = humidity;display();}public void display() {System.out.println("Current conditions: " + temperature+ "F degrees and " + humidity + "% humidity");} }3.6測試
萬事俱備,就來測試一下結果
/*** 觀察者模式測試* Created by mrf on 2016/3/1.*/ public class WeatherStationTest {public static void main(String[] args) {WeatherData weatherData = new WeatherData();CurrentConditionsDisplay currentDisplay =new CurrentConditionsDisplay(weatherData);//其他布告板 // StatisticsDisplay statisticsDisplay = new StatisticsDisplay(weatherData); // ForecastDisplay forecastDisplay = new ForecastDisplay(weatherData);weatherData.setMeasurements(80, 65, 30.4f);weatherData.setMeasurements(82, 70, 29.2f);weatherData.setMeasurements(78, 90, 29.2f);} }結果:
Current conditions: 80.0F degrees and 65.0% humidity Current conditions: 82.0F degrees and 70.0% humidity Current conditions: 78.0F degrees and 90.0% humidity4.松耦合的威力
設計原則:為了交互對象之間的松耦合設計而努力。
當兩個對象之間松耦合,它們依然可以交互,但是不太清楚彼此的細節。觀察者模式提供了一種對象設計,讓主題和觀察者之間松耦合。為什么呢?
關于觀察者的一切,主題只知道觀察者實現了某個接口(也就是Observer接口)。主題不需要知道觀察者的具體類是誰、做了些什么或其他任何細節。任何時候我們都可以增加新的觀察者。因為主題唯一依賴的東西是一個實現Observer接口的對象列表,所以我們可以隨時增加觀察者。事實上,在運行時我們可以用新的觀察者取代現有的觀察者,主題不會受到任何影響。同樣的,也可以在任何時候刪除某些觀察者。
有新類型的觀察者出現時,主題的代碼不需要修改。假如我們有個新的具體類需要當觀察者,我們不需要為了兼容新類型而修改主題的代碼,所有要做的就是在新的類里實現此觀察者接口,然后注冊為觀察者即可。主題不在乎別的,它只會發送通知給所有實現了觀察者接口的對象。
我們可以獨立地復用主題或觀察者。如果我們在其他地方需要使用主題或觀察者,可以輕易地復用,因為二者并非緊耦合。改變主題或觀察者其中一方,并不會影響另一方。因為兩者是松耦合的,所以只要他們之間的接口仍被遵守,我們就可以自由地改變他們。
松耦合的設計之所以能讓我們建立有彈性的OO系統,能夠應對變化,是因為對象之間的互相依賴降到了最低。
?5.使用java內置的觀察者模式
java.util下包含了Observer接口和Observable類,這和我們之前的Subject、Observer接口很類似。使用java內置的只要做些簡單修改:
?
這里要注意,主題要繼承內置實現的Observable類,觀察者實現Observer接口,而且如果主題要通知觀察者必須設置setChanged()來告訴系統需要通知,這一設置的意思是為了避免某些不想發送通知的情況。比如溫都每0.1更新一次,但觀察者不需要這么頻繁,只需要到1度以上才更新等。控制通知很重要。這里的get方法是為了實現讓觀察者自己拉取數據。由此,數據可以通知,也可以拉取。下面看代碼實現,注意導入的包的類型:
5.1WeatherData修改
/*** 觀察者模式* 角色:主題* 通過繼承java內置的對象來實現* Created by mrf on 2016/3/1.*/ public class WeatherData extends Observable {private float temperature;private float humidity;private float pressure;public WeatherData() { }public void measurementsChanged() {setChanged();//設置通知標識notifyObservers();//通知}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;} }5.2接收通知的觀察者
/*** 觀察者模式* 角色:觀察者* 通過實現java內置的方法實現觀察功能* Created by mrf on 2016/3/1.*/ public class CurrentConditionsDisplay implements Observer, DisplayElement {Observable observable;private float temperature;private float humidity;public CurrentConditionsDisplay(Observable observable) {this.observable = observable;observable.addObserver(this);//注冊為觀察者}/*** 更新* @param obs 主題* @param arg 這里沒有用到*/public void update(Observable obs, Object arg) {//通過主題對象來更新if (obs instanceof WeatherData) {WeatherData weatherData = (WeatherData)obs;this.temperature = weatherData.getTemperature();this.humidity = weatherData.getHumidity();display();}}public void display() {System.out.println("Current conditions: " + temperature+ "F degrees and " + humidity + "% humidity");} }5.3自己拉取數據的觀察者
/*** Created by mrf on 2016/3/1.*/ public class ForecastDisplay implements Observer, DisplayElement {Observable observable;private float lastPresure;private float currentPresure =12.5f;public ForecastDisplay(Observable observable) {this.observable =observable;}@Overridepublic void display() {System.out.println("當前壓強:"+currentPresure+"; 上一次壓強:"+lastPresure);}@Overridepublic void update(Observable obs, Object arg) {if (obs instanceof WeatherData) {lastPresure = currentPresure;currentPresure = ((WeatherData) obs).getPressure();display();}} }5.4測試
/*** 測試拉取數據* Created by mrf on 2016/3/1.*/ public class ForecastDisplayTest {public static void main(String[] args) {WeatherData weatherData = new WeatherData();CurrentConditionsDisplay currentDisplay =new CurrentConditionsDisplay(weatherData);ForecastDisplay forecastDisplay = new ForecastDisplay(weatherData);weatherData.setMeasurements(80, 65, 30.4f);//拉取forecastDisplay.update(weatherData,forecastDisplay);currentDisplay.update(weatherData,forecastDisplay);weatherData.setMeasurements(82, 70, 29.2f);forecastDisplay.update(weatherData,forecastDisplay);weatherData.setMeasurements(78, 90, 39.2f);forecastDisplay.update(weatherData,forecastDisplay);} }結果:
Current conditions: 80.0F degrees and 65.0% humidity 當前壓強:30.4; 上一次壓強:12.5 Current conditions: 80.0F degrees and 65.0% humidity Current conditions: 82.0F degrees and 70.0% humidity 當前壓強:29.2; 上一次壓強:30.4 Current conditions: 78.0F degrees and 90.0% humidity 當前壓強:39.2; 上一次壓強:29.26.java.util.Observable的黑暗面
Observable是一個類
是的,你注意到了!如同你所發現的,可觀察者是一個“類”而不是一個“接口”,更糟的是,它甚至沒有實現一個接口。不幸的是,java.util.Observable的實現有許多問題,限制了它的使用和復用。這并不是說它沒有提供有用的功能,我們只
是想提醒大家注意一些事實。
你已經從我們的原則中得知這不是一件好事,但是,這到底會造成什么問題呢?首先,因為Observable是一個“類”,你必須設計一個類繼承它。如果某類想同時具有Observable類和另一個超類的行為,就會陷入兩難,畢竟Java不支持多重繼承。這限制了Observable的復用潛力(而增加復用潛力不正是我們使用模式最原始的動機嗎?)。再者,因為沒有Observable接口,所以你無法建立自己的實現,和Java內置的Observer API搭配使用,也無法將java.util的實現換成另一套做法的實現(比方說,
Observable將關鍵的方法保護起來
如果你能夠擴展java.util.Observable,那么Observable“可能”可以符合你的需求。否則,你可能需要像本章開頭的做法那樣自己實現這一整套觀察者模式。不管用哪一種方法,反正你都已經熟悉觀察者模式了,應該都能善用它們。如果你看看Observable API,你會發現setChanged()方法被保護起來了(被定義成protected)。那又怎么樣呢?這意味著:除非你繼承自Observable,否則你無法創建Observable實例并組合到你自己的對象中來。這個設計違反了第二個設計原則:“多用組合,少用繼承”。
?
唯有不斷學習方能改變! -- Ryan Miao
總結
以上是生活随笔為你收集整理的java设计模式(六)--观察者模式的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Python3.7 Scrapy安装(W
- 下一篇: JavaScript 01