設計模式之——觀察者模式

前提:本文章是以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中可以見到,而且在JavaBeansSwing中,也都有觀察者模式。
——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 設計模式》*
    
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,412評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,514評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,373評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,975評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,743評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,199評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,262評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,414評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,951評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,780評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,527評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,218評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,649評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,889評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,673評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,967評論 2 374

推薦閱讀更多精彩內容