前提:本文章是以Java為基礎寫的
一、定義描述
什么是觀察者模式?
既然我們要探討一下觀察者模式,首先還是先說一下他的定義吧;
定義:觀察者模式定義了對象之間的一對多依賴,這樣一來,當一個對象改變狀態時,他的所有依賴者都會收到通知并自動更新。
現在看不懂定義很正常,在我們接下來的探討中你就會慢慢的了解了,在我們的探討結束之后,再回頭看這個定義,你就會有所領悟了!
接下來我們就步入正題了!
二、實例探討
例子:
現在要求我們做一個氣象監測應用,這個系統大致分為三個部分,分別是氣象站(獲取實際氣象數據的裝置)、WeatherData對象(追蹤來自氣象站的數據,并更新布告板)和布告板(顯示目前天氣狀況給用戶看)。
WeatherData對象知道如何跟物理氣象站聯系,以取得更新的數據(這個我們不需要考慮)。WeatherData對象會隨即更新三個布告板的顯示:目前狀況(溫度、濕度、氣壓)、氣象統計和天氣預報。
我們的工作:
利用WeatherData對象取得數據,并更新三個布告板:目前狀況、氣象統計和天氣預報。
已知的一些內容如下:
WeatherData類有四個方法:getTemperature();
getHumidity();
getPressure() ;前三個方法各自返回最近的氣象測量數據(溫度、濕度、氣壓)
measurementsChanged();此方法在氣象測量更新時調用
其實我們的工作就是實現measurementsChanged(),讓他更新目前狀況、氣象統計、天氣預報的顯示布告板
我們現在需要解決的的問題:
①實現三個使用天氣數據的布告板:“目前狀況”布告、“氣象統計”布告、“天氣預告”布告。只要WeatherData有新的測量,這些布告就必須馬上更新。
②系統必須可擴展,這是什么意思呢?
就是其他開發人員可以建立定制的布告板,用戶可以隨心所欲地添加或刪除任何布告板
OK,這個題我們就分析到這里,下面就要上代碼了!
代碼:
public class WeatherData{
//實例變量聲明
public void measurementChanged(){
float temp = getTemperature();
float humidity = getHumidity();
float pressure = getPressure();
currentConditionsDisplay.update(temp,humidity,pressure);//目前狀況布告板的更新
statisticsDisplay.update(temp,humidity,pressure);//氣象統計布告板的更新
forecastDisplay.update (temp,humidity,pressure);//天氣預報布告板的更新
}
//這里是其他WaetherData方法
}
What?
難道我們這樣就解決了?so easy?好嗨喲?!
很不幸的告訴你,這個是
錯誤示范!
接下來我們先探討一下上面的錯誤示范:
先上道題瞅瞅
答案是ABCE
錯誤代碼分析:
——現在回頭看一下剛才的代碼,有沒有發現三個布告板都有一個update(),并且里面的形參也是一樣的,那我們是不是可以把它搞成一個統一的接口呢?
——前面我們提到過,我們可能還要增加或刪除布告板,那這樣的話,我們豈不是要一直修改程序?
接下來我們就先來認識一下“觀察者模式”這位朋友
三、認識觀察者模式
我們還是通過一個例子來了解一下觀察者模式吧
例子:
如果你了解報紙的訂閱是怎么回事,其實就知道觀察者模式是怎么回事,只是名稱不太一樣:出版者對應“主題”(Subject),訂閱者對應“觀察者”(Observer)。
其實總的來說:
出版者+訂閱者=觀察者模式
為了讓大家看的更明白點,還是奉上圖吧:
下面是鴨子、老鼠和主題的故事:(加深一下大家對觀察者模式的理解)
四、定義觀察者模式
觀察者模式的定義我們在最開始已經說過了,經過上面我們對他的了解,現在我們再來回顧一下:
——主題和觀察者定義了一對多的關系。
——觀察者依賴于此主題,只要主題狀態一有變化,觀察者就會被通知。根據通知的風格,觀察者可能因此新值而更新。
下面大家先看一下觀察者模式的類圖:
看了這張圖你可能還會有些疑惑,不用慌,下面請欣賞我的自問自答:
問:
這和一對多的關系有啥關聯?。?/p>
答:
利用觀察者模式,主題是具有狀態的對象,并且可以控制這些狀態。意思就是,有“一個”具有狀態的主題。另外,觀察者使用這些狀態,雖然這些狀態并不屬于他們。觀察者是有很多的,他們依賴主題來告訴他們狀態何時改變。這就產生一個關系:
“一個”主題對“多個”觀察者的關系。
問:
其中的依賴是咋產生的啊?
答:
因為主題是真正擁有數據的人,觀察者是主題的依賴者,在數據變化時更新,這樣比起讓許多對象控制同一份數據來,可以得到更干凈的00設計
不知道00設計的可以點擊下面的連接:
https://www.cnblogs.com/HigginCui/p/6195318.html
說了這么多了咱們上面那個實例的問題還沒有解決,不過相信大家現在對觀察者模式了解的也差不多了,所以馬上就到解決問題的時候了
現在呢,我們再來加深一下對觀察者模式的了解:
其實觀察者模式就是提供了一種對象設計,讓主題和觀察者之間松耦合
(這里的松耦合的意思就是:主題和觀察者聯系越小越好)
五、解決實例的問題
首先我們還是要先捋一下我們的思路:
觀察者模式是定義了一對多的依賴
——WeatherData類就是“一”,而“多"就是使用天氣觀測的各種布告板。
觀察者模式中主題是具有狀態的對象
——WeatherData對象正好是有狀態的(溫度、濕度、氣壓)。
觀察者模式是主題+觀察者
——我們可以把WeatherData對象當作主題,把布告板當作觀察者。
——布告板獲取信息,需要向WeatherData注冊。
——每個布告板都有差異,我們就用一個共同的接口。
好了,差不多了,設計圖就可以畫出來了:
有了設計圖,接下來就要干正事了!代碼:
public interface Subject{
public void registerObserver(Observer o);//注冊觀察者
public void removeObserver(Observer o);//刪除觀察者
public void notifyObserver();//主題改變時,這個方法會被調用,通知所有的觀察者
}
public interface Observer{
public void update(float temp, float humidity, float pressure);//觀察者用于更新數據
}
public interface DisplayElement{
public void display();//布告板需要顯示時,調用此方法
}
小探討:
你可能會有其他想法用來實現這個問題,可能你會覺得你的方法比這個簡單,代碼很短,但是在你需要對這個氣象應用進行擴展的時候,比如添加新的布告板,添加新的功能,你用接口的話就不需要修改很多的代碼,直接寫你要添加的布告板就可以了,不需要做過多的修改,有很好的封裝性。
你可以自己寫下試試,比較一下。
接口寫好了,下面就實現我們的氣象站
代碼:
public class WeatherData implements Subject{
private ArrayList observer;//我們用ArrayList數組來記錄觀察者
private float temperature;
private float humidity;
private float pressure;
public WeatherData(){
observers=new ArrayList();
}//在構造器中建立ArrayList
public void registerObserver(Observer o){
observers.add(o);
}//注冊觀察者,直接加到ArrayList的后面就行了
public void removeObserver(Observer o){
int i = observers.indexOf(o); if(i >= 0){
observers.remove(i);
}
}//跟注冊類似,取消觀察者,直接把他從ArrayList中刪除
public void notifyObservers(){
for(int i = 0; i < observers.size(); i++){
Observer observer=(Observer)observers.get(i);
Observer.update(temprature, humidity, pressure);
}
}//我們把狀態告訴每一個觀察者,通過updata()通知他們
public void measurementsChanged(){
notifyObservers();
}//當從氣象站得到更新觀測值時,我們通知觀察者
public void setMeasurements(float temperature, float humidity, float pressure){
this.temperature = temprature;
this.humidity = humidity;
this.pressure = pressure;
measurementsChanged();
}
//WeatherData的其他方法
}
ArrayList的相關:
https://www.cnblogs.com/rickie/articles/67978.html
上面的WeatherData類已經寫好了,下面就該布告板了,三個布告板:目前狀況布告板、氣象統計布告板和天氣預報布告板(由于三個布告板基本差不多,所以這里就只寫目前狀況布告板)
代碼:
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);
}//構造器,用weatherData對象作為注冊來用
public void update(float temperature, float humidity, flaot pressure){
this.temperature = temperature;
this.humidity = humidity;
display();
}
public void display(){
System.out.println("Current conditions: " + temperature + "F degress and " + humidity + "% humidity");
}//把最近的溫度和濕度顯示出來
}
以上代碼并不是最好的,大家可以思考一下,嘗試不同的方法!
接下來就剩把這些連接起來的測試程序了,Let's go!
代碼:
(這里也只寫了目前狀況布告板)
public class WeatherStation{
public static void main(String[] args){
WeatherData weatherData = new WeatherData();//建立WeatherData對象
CurrentConditionsDisplay currentDisplay = new CurrentConditionsDisplay(weatherData);//建立布告板,傳入WeatherData對象
weatherData.setMeasurements(80,65,30.4f);
weatherData.setMeasurements(82,70,29.2f);
weatherData.setMeasurements(78,90,29.2f);這三組數據是我瞎寫的
}
}
請自行運行程序測試吧!
終于歐克了啊,觀察者模式不過如此嘛!哈哈哈!
本來到這里是應該大結局的,可是Java實力太強,他不允許?。?/p>
六、擴展內容
Java內置的觀察者模式:
——Java API 內置的
——java.util包內有Observer接口與Observable類,這跟我們的Subject接口與Observer接口是類似
——使用賊方便,許多功Java API都給你準備好了
下面我們先了解一下java.util.Observer和java.util.Observable,下面的圖是修改后的氣象站00設計:
Java內置的觀察者模式的運作
其實跟我們在氣象站中的實現差不多,就是WeatherData(我們的主題)不太一樣,現在這個是擴展自Observable類,諸如增加、刪除、通知觀察者的方法都繼承到了,直接調用就行了。
用法:
——先介紹一下我們會用到的內置方法:
setChanged()方法:標記狀態已經改變的事實;
notifyObservers()方法:數據更新時通知觀察者(或者nitifyObservers(Object arg)方法;
update(Observable o,Object arg)方法:這個就是觀察者更新數據的方法;
在實現代碼之前,先說一下這個setChanged()方法,因為之前咱沒有標記狀態改變這個步驟,所以這里講一下用它的原因。
看一下下面的代碼你就懂了:
setChanged(){
changed = true;
}
notifyObservers(Object arg){
if(changed){
for ever observer on the list{
call update(this,arg)
}
changed = false;//通知觀察者之后,把changed標為false
}
}//這里只會在setChanged()中的changed標為“true”時才會通知觀察者
notifyObservers(){
notifyObservers(null);
}
你可能會問:
這樣做有神馬必要呢?
我來告訴你:
我們用setChanged()方法可以在更新觀察者是,有更多的彈性,可以更適當地通知觀察者。還是打個比方發吧,如果我們不用setChanged()方法,我們的氣象站測量還非常靈敏,以致于溫度計讀數每 1/10 就會更新,這樣就會造成WeatherData對象不斷地通知觀察者,你想這樣嗎?是不是太煩人了?如果我們用setChanged()方法,我們就可以自行設置,比如我們想要在半度以上才更新,我們就可以在溫度度差到達半度時,調用setChanged()方法,進行有效的更新(現在主動權就掌握在我們手中了,說讓他啥時候通知就啥時候通知,真好?。?/p>
ok,接下來我們就利用Java內置的支持
重做氣象站:
import java.util.Observable;
import java.util.Observer;
public class WeatherData extends Observable{ //看好啊,我們現在正在**繼承**Observable
private float temperature;
private float humidity;
private float pressure;
public WeatherData(){ }//構造器不需要為了記住觀察者而建立ArrayList了
public void measurementsChanged(){
setChanged();//這里,在調用notifyObservers()之前,先調用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;
}
//用上面這三個方法取得WeatherData對象的狀態
}
重做目前狀況布告板:
import java.util.Observable;
import java.util.Observer;
public class CurrentConditionsDisplay implements Observer, DisplayElement{ //我們這里正在實現java.util.Observer接口
Observable observable;
private float temperature;
private float humidity;
public CurrentConditionsDisplay(Observable observable){ //構造器需要Observable當參數,并將CurrentConditionsDisplay對象登記成為觀察者
this.observable = observable;
observable.addObserver(this);
}
public void update(Observable obs, Object arg){ //改變update()方法,增加Observable和數據對象作為參數
if(obs instanceof WeatherData){ //這里的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 degress and " + humidity + "% humidity");
}
}
這里是對instanceof的介紹:
https://www.cnblogs.com/zjxynq/p/5882756.html
來看一下運行結果吧:
這是完整版的運行結果,你們記得把另外兩個布告板也寫上去!
通過看這個運行結果,我們能看出啥呢?
emmm...他這個輸出次序好像有點亂啊,本來的次序是:目前狀況—>氣象統計—>天氣預報;
那現在咋成這樣了呢?思考一下,我們繼續
不要依賴于觀察者被通知的次序
java.util.Observable實現了他的notifyObservers()方法,這導致了通知者的次序不同于我們先前的次序。誰都沒錯,只是雙方選擇不同的方式罷了。
但是,我們的依賴這樣的次序來寫,就是錯的!這樣一來,只要觀察者的實現有所變化,通知次序就會改變,很有可能就會產生錯誤的結果。所以這就不可能是我們所要的松耦合(違背了初心?。。?/p>
下面就來看一下原因吧!
現在應該明白了吧?
我在前面的代碼的注釋中就特別說明了Observable是類,而非是我們想要的接口!
除了這個內置的觀察者模式,下面還有一些擴展,繼續看下去吧
Come on?。?!
觀察者與Swing
其實觀察者模式還是比較厲害的,不僅是在java.util中可以見到,而且在JavaBeans和Swing中,也都有觀察者模式。
——JavaBeans中的觀察者模式的話,可以直接查一下PropertyChangeListener接口。
——Swing中的,我們就用例子來說一下:
一個小的、改變生活的程序
程序非常簡單,有一個按鈕,上面寫著“Should I do it ?”(我該做嗎?)。當你按下按鈕,傾聽者(觀察者)就回答問題。我們這里就實現兩個傾聽者吧,一個天使(AngelListener),一個惡魔(DevilListener)。
提示:我們這里用的是Swing API:JButton。
這個程序代碼很短,就建立一個JButton對象,把他加到JFrame,然后設置好傾聽者(觀察者)就可以了。我們這里打算用內部類作為傾聽者。
代碼:
public class SwingObserverExample{
JFrame frame;
public static void main(String[] args){
SwingObserverExample example = new SwingObserverExample();
example.go();
}
public void go(){
frame = new JFrame();
JButton button = new JButton("Should I do it ?");
button.addActionListener(new AngelListener());
button.addActionListener(new DevilListener());
frame.getContentPane().add(BorderLayout.CENTER, button);//在這里設置frame的屬性
}
class AngelListener implements ActionListener{
public void actionPerformed(ActionEvent event){
System.out.println("Don't do it, you might regret it !");
}
}//天使傾聽者(觀察者)
class DevilListener implements ActionListener{
public void actionPreformed("Come on, do it !");
}//這里的actionPerformed()對應之前的update()
}//惡魔傾聽者(觀察者)
}
最后一個板塊:
七、總結
工具
——00原則:
- 封裝變化;
- 多用組合,少用繼承;
- 針對接口編程,不針對實現編程;
- 為交互對象之間的松耦合設計而努力。
要點
觀察者模式定義了對象之間一對多的關系;
可觀察者(主題)用一個共同的接口來更新觀察者;
觀察者和可觀察者(主題)之間用松耦合方式結合,可觀察者不知道觀察者的細節,只知道觀察者實現了觀察者接口;
有多個觀察者時,不可以依賴特定的通知次序;
Java有多種觀察者模式的實現,包括了通用的java.util.Observable;
可以嘗試實現自己的Observable;
Swing大量使用觀察者模式,許多GUI框架也是如此;
-
此模式也被應用在很多地方,例如:JavaBeans、RMI
這次的觀察者模式算是真正的講完了,寫的不咋樣,大家湊合著看吧,有不懂的可以評論或者私聊!
關于代碼中的命名問題,這里推薦大家看一下《阿里Java開發手冊》
(提取碼:aviz)*本文圖片例子來源于《Head First 設計模式》*