Head First 設計模式(2)---觀察者(Observer)模式

本文參照《Head First 設計模式》,轉載請註明出處java

對於整個系列,咱們按照這本書的設計邏輯,使用情景分析的方式來描述,而且穿插使用一些問題,總結的方式來說述。而且全部的開發源碼,都會託管到github上。 項目地址:github.com/jixiang5200…git

前一章主要講解了設計模式入門和最經常使用的一個模式-----策略模式,並結合Joe的鴨子模型進行分析,想要了解的朋友能夠回去回看一下。 這裏咱們將繼續介紹一種能夠幫助對象知悉現狀,不會錯過該對象感興趣的事。甚至對象能夠本身決定是都要繼續接受通知。有過設計模式學習經驗的人會脫口而出-----觀察者模式。對的,接下來咱們將瞭解一個新的設計模式,也就是觀察者模式。github

1.引言

最近你的團隊獲取了一個新的合約,須要負責創建一個Weather-O-Rama公司的下一代氣象站----Internet氣象觀測站。 合約內容以下:編程

恭喜貴公司獲選爲敝公司創建下一代Internet氣象觀測站!該氣象站必須創建在咱們專利申請的WeatherData對象上,由WeatherData對象負責追蹤目前的天氣情況(溫度、溼度、氣壓)。咱們但願貴公司能創建一個應用,有三種佈告板,分別顯示目前的情況、氣象統計及簡單的預報。當WeatherData對象獲取到最新的測量數據時,三種佈告板必須實時更新。 並且,這是一個能夠拓展的氣象站,Weather-O-Rama氣象站但願公佈一組API,讓其餘開發人員能夠寫出本身的氣象佈告板,並插入此應用中咱們但願貴公司能夠提供這樣的API。 Weather-O-Rama氣象站有很好的商業運營模式:一旦客戶上鉤,他們使用每一個佈告板都要付錢最好的部分就是,爲了感謝貴公司創建此係統,咱們將以公司的認股權支付你。 咱們期待看到你的設計和應用的alpha版本。 附註:咱們正在通宵整理WeatherData源文件給大家。設計模式

1.1需求分析

根據開發的經驗,咱們首先分析Weather-O-Rama公司的需求:api

  • 此係統有三個部分組成:氣象站(獲取實際的氣象數據的物理組成),WeatherData對象(追蹤來自氣象站的數據,並更新佈告板)和佈告板(顯示目前天氣情況展現給用戶)
  • 項目應用中,開發者須要利用WeatherData去實時獲取氣象數據,而且更新三個佈告板:目前氣象,氣象統計和天氣預報。
  • 系統必須具有很高的可拓展性,讓其餘的開發人員能夠創建定製的佈告板,用戶能夠爲所欲爲地添加或刪除任何佈告板。

咱們初始設計結構以下: bash

初始設計結構

1.2WeatherData類

次日,Weather-O-Rama公司發送過來WeatherData的源碼,其結構以下圖 網絡

WeatherData數據結構

其中measurementsChanged()方法在氣象測試更新時,被調用。數據結構

1.3錯誤的編碼方式

首先,咱們從大部分不懂設計模式的開發者經常使用的設計方式開始。 根據Weather-O-Rama氣象站開發人員的需求暗示,在measurementsChanged()方法中添加相關的代碼:框架

public class WeatherData {
    private float temperature;//溫度
    private float humidity;//溼度
    private float pressure;//氣壓
    
    private CurrentConditionsDisplay currentConditionsDisplay;//目前狀態佈告板
    private StatisticsDisplay statisticsDisplay;//統計佈告板
    private ForecastDisplay forecastDisplay;//預測佈告板
    
    public WeatherData(CurrentConditionsDisplay currentConditionsDisplay
            ,StatisticsDisplay statisticsDisplay
            ,ForecastDisplay forecastDisplay){
        this.currentConditionsDisplay=currentConditionsDisplay;
        this.statisticsDisplay=statisticsDisplay;
        this.forecastDisplay=forecastDisplay;
    }
    
    
    public float getTemperature() {
        return temperature;
    }
    
    public float getHumidity(){
        return humidity;
    }
    
    public float getPressure(){
        return pressure;
    }

    //實例變量聲明
    public  void measurementsChanged(){
        //調用WeatherData的三個getter方法獲取最近的測量值
        float temp=getTemperature();
        float humidity=getHumidity();
        float pressure=getPressure();
        
        currentConditionsDisplay.update(temp,humidity,pressure);
        statisticsDisplay.update(temp,humidity,pressure);
        forecastDisplay.update(temp,humidity,pressure);
    }
    
    //通知發生變化
    public void setMeasurements(float temperature,float humidity,float pressure){
        this.temperature=temperature;
        this.humidity=humidity;
        this.pressure=pressure;
        measurementsChanged();
    }
}
複製代碼

回顧第一章的三個設計原則,咱們發現這裏違反了幾個原則

第一設計原則 找出應用中可能須要變化之處,把它們獨立出來,不要和那些不須要變化的代碼混合在一塊兒。

第二設計原則 針對於接口編程,不針對實現編程

第三設計原則 多用組合,少用繼承

在這裏咱們使用了針對實現編程,而且沒有將變化部分獨立出來,這樣會致使咱們之後在增長或刪除佈告板時必須修改應用程序。並且,最重要的是,咱們犧牲了可拓展性。

分析錯誤點
既然這裏咱們提到了要使用觀察者模式來解決問題,那麼該如何下手。而且,什麼是觀察者模式?

2.觀察者模式

2.1認識觀察者模式

爲了方便理解,咱們從平常生活中常遇到的情形來理解觀察者模式,這裏咱們使用生活常見的報紙和雜誌訂閱業務邏輯來理解:

  • 報社的業務在於出版報紙
  • 訂閱報紙的用戶,只要對應報社有新的報紙出版,就會給你送來
  • 當用戶不想繼續訂閱報紙,能夠直接取消訂閱。那麼以後就算有新的報紙出版,也不會送給對應用戶了。
  • 只要報社一直存在,任何用戶均可以自由訂閱或取消訂閱報紙

從上面的邏輯咱們分析出,這裏由如下部分組成,報社,用戶,訂閱。將其抽象出來就i是:出版者,訂閱者,訂閱。這裏觀察者模式的雛形已經出來了。

出版者+訂閱者=觀察者模式

若是上面已經理解了報社報紙訂閱的邏輯,也能夠很快知道觀察者模式是什麼。只是在其中名稱會有差別,前面提到的「出版者」咱們能夠稱爲**「主題(Subject)」「被觀察者(Observable)」(後一個更加經常使用),「訂閱者」咱們稱爲「觀察者(Observer)」**,這裏咱們採用類UML的結構圖來解釋:

觀察者模式結構圖

2.2 觀察者模式註冊/取消註冊

場景1: 某一天,鴨子對象以爲本身的朋友都訂閱了主題,本身也想稱爲一個觀察者。因而告訴主題,它想當一個觀察者。完成訂閱後,鴨子也成爲一個觀察者了。

鴨子成爲觀察者後的結構圖
這樣當主題數據發生變化時,鴨子對象也能夠獲得通知了!!

場景2: 老鼠對象厭煩了天天都被主題煩,決定從觀察者序列離開,因而它告訴主題它想離開觀察者行列,主題將它從觀察者中除名。

老鼠離開觀察者後的結構圖
以後主題數據發生變化時,不會再通知老鼠對象。

上面的兩個情形分別對應了註冊和取消註冊,這也是觀察者模式最重要的兩個概念。註冊後的對象咱們才能夠稱爲觀察者。觀察者取消註冊後也不能稱爲觀察者。

2.3 觀察者模式定義

經過報紙業務和對象訂閱的例子,咱們能夠勾勒出觀察者模式的基本概念。

觀察者模式定義了對象之間的一對多的依賴,這樣一來,當一個對象改變狀態時,它全部的依賴者都會收到通知並自動更新。

主題/被觀察者和觀察者之間定義了一對多的關係。觀察者依賴於主題/被觀察者。一旦主題/被觀察者數據發生改變的時候,觀察者就會收到通知。那麼,如何實現觀察者和主題/被觀察者呢?

2.4 觀察者模式實現

因爲網絡上的實現觀察者的方式很是多,咱們這裏採起比較容易理解的方式Subject和Observer。對於更高級的使用方式,能夠百度。 接下來咱們來看看基於Subject和Observer的類圖結構:

Subject和Observer的類圖結構

3. 設計氣象站

到這裏咱們再回到當初的問題,氣象站中結構模型爲一對多模型,其中WeatherData爲氣象模型中的「一」,而「多」也就對應了這裏用來展現天氣監測數據的各類佈告板。相對於以前的針對實現的方式,使用觀察者模式來設計會更加符合需求。優先咱們給出新的氣象站模型。

氣象站數據模型

3.1實現氣象站

依照前面的設計結構圖,最終來實現具體代碼結構

1.Subject

public interface Subject {

    //註冊觀察者
    public void registerObserver(Observer o);
    
    //刪除觀察者
    public void removeObserver(Observer o);
    
    //當主題發生數據變化時,通知全部觀察
    public void notifyObservers();
    
}
複製代碼

2.Observer

public interface Observer {

   /**
    * 
    * update:當氣象站的觀測數據發生改變時,這個方法會被調用
    * @param temp 溫度
    * @param hunmidity 溼度
    * @param pressure  氣壓
    * @since JDK 1.6
    */
    public void update(float temp,float hunmidity,float pressure);
}
複製代碼

3.DisplayElement

public interface DisplayElement {
    //當佈告板須要展現時,調用此方法時
    public void display();
}
複製代碼

4.新的WeatherData1

public class WeatherData1 implements Subject{
    
    private ArrayList<Observer> observers;
    
    private float temperature;
    
    private float humiditty;
    
    private float pressure;
    
    public WeatherData1(){
        observers=new ArrayList<Observer>();
    }

   //註冊
    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=observers.get(i);
            observer.update(temperature, humiditty, pressure);
        }
        
    }
    
    public void measurementsChanged(){
        notifyObservers();
    }
    
    public void setMeasurements(float temperature,float humidity,float pressure){
        this.temperature=temperature;
        this.humiditty=humidity;
        this.pressure=pressure;
        measurementsChanged();
    }

}
複製代碼

5.CurrentConditionsDisplay

public class CurrentConditionsDisplay implements Observer,DisplayElement{
    
    private float temperature;
    private float humidity;
    private float pressure;
    private Subject weatherData;
    
    public CurrentConditionsDisplay(Subject weatherData){
        this.weatherData=weatherData;
        weatherData.registerObserver(this);
    }
    
    /**
     * 
     * update:更新佈告板內容
     * @author 吉祥
     * @param temperature
     * @param humidity
     * @param pressure
     * @since JDK 1.6
     */
    public void update(float temperature,float humidity,float pressure){
        this.temperature=temperature;
        this.humidity=humidity;
        this.pressure=pressure;
        display();
    }

    /**
     * 
     * display:展現佈告板內容
     * @author 吉祥
     * @since JDK 1.6
     */
    public void display(){
        System.out.println("Current conditons:"+temperature
                +"F degrees and "+humidity+"% humidity");
    }
}
複製代碼

6.ForecastDisplay

public class ForecastDisplay implements Observer,DisplayElement{
    private float temperature;
    private float humidity;
    private float pressure;
    private Subject weatherData;
    
    public ForecastDisplay(Subject weatherData){
        this.weatherData=weatherData;
        weatherData.registerObserver(this);
    }
    
    /**
     * 
     * update:更新佈告板內容
     * @author 吉祥
     * @param temperature
     * @param humidity
     * @param pressure
     * @since JDK 1.6
     */
    public void update(float temperature,float humidity,float pressure){
        this.temperature=temperature;
        this.humidity=humidity;
        this.pressure=pressure;
        display();
    }

    /**
     * 
     * display:展現佈告板內容
     * @author 吉祥
     * @since JDK 1.6
     */
    public void display(){
        System.out.println("Forecast: More of the same");
    }

}
複製代碼

7.StatisticsDisplay public class StatisticsDisplay implements Observer,DisplayElement{ private float temperature; private float humidity; private float pressure; private Subject weatherData;

public StatisticsDisplay(SubjectweatherData){
    this.weatherData=weatherData;
    weatherData.registerObserver(this);
}

/**
 * 
 * update:更新佈告板內容
 * @author 吉祥
 * @param temperature
 * @param humidity
 * @param pressure
 * @since JDK 1.6
 */
public void update(float temperature,float humidity,float pressure){
    this.temperature=temperature;
    this.humidity=humidity;
    this.pressure=pressure;
    display();
}

/**
 * 
 * display:展現佈告板內容
 * @author 吉祥
 * @since JDK 1.6
 */
public void display(){
    System.out.println("Avg/Max/Min temperature= "+temperature
            +"/"+temperature+"/"+temperature);
}
複製代碼

}

ps:這裏在Observer中使用Subject緣由在於方便之後的取消註冊。

最後咱們創建一個測試類WeatherStation來進行測試

public class WeatherStation {
    public static void main(String[] args){
        WeatherData1 weatherData=new WeatherData1();
        
        CurrentConditionsDisplay currentConditionsDisplay=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);
    }
}
複製代碼

最終結果以下

測試結果
到這裏咱們已經講解完觀察者模式的一種實現方式。可是這咱們也提出一個問題,用來發散。

是否可以在主題中提供向外的可讓觀察者本身獲取本身想要數據,而並不是將全部的數據都推送給觀察者?也就是在Push(推)的同時咱們也能夠pull(拉)。

4.Java內置的觀察者模式

剛纔的問題,其實熟悉Java語言的開發者會發現,在Java中已經有相應的模式,若是熟悉的能夠直接跳過本章。 在java.util包下有Observer和Observable類,這兩個類的結構跟咱們遇到的Subject和Observer模型有些相似。甚至但是隨意使用push(推)或者pull(拉) 這裏咱們使用在線的Java API網站在線Java API文檔 首先查詢Observer的API

Observer API文檔

這個與咱們所寫的Observer結構幾乎相同,只是在推送是把Observable類一塊兒推送,這樣用戶既能夠push也可使用pull的方式。那麼Observable的結構呢

Observable API 介紹

Observable API 方法介紹

咱們發現這裏Observable是類與咱們以前Subject做爲接口的方式稍微有區別;而且Observable類其餘方法更全。那麼使用類的方式和使用接口的影響咱們在後面會繼續講。而且這裏咱們關注setChanged()方法告訴被觀察者的數據發生改變 那麼,若是要使用Java中自帶的觀察者模式來修改原有氣象站業務會如何。

首先,咱們來分析更改後氣象站的模型:

Java內置觀察者模式 氣象站

4.1Java內置觀察者模式運做模式

相對於於以前Subject和Observer的模式,Java內置自帶的觀察者模式運行稍微有些差別。

  • 將對象變成觀察者只須要實現Observer(java.util.Observer)接口,而後調用任何Observable的addObserver()方法便可。若是要刪除觀察者,調用deleteObserver()便可。

  • 被觀察者若要推送通知,須要對象繼承Observable(java.util.Observable)類,並先調用setChanged(),首先標記狀態已經改變。而後調用notifyObservers()方法中的一個:notifyObservers()(通知觀察者pull數據)或notifyObserers(Object object)(通知觀察者push數據)

那麼做爲觀察者如何處理被觀察者推送出的數據呢。 這裏邏輯以下:

  • 觀察者(Observer)必須在update(Observable o,Object object).前一個參數用來讓觀察者知道是哪一個被觀察者推送數據。後一個object爲推送數據,容許爲null。

4.2 setChanged()

在Observable類中setChanged()方法一開始我也有疑惑,爲什麼在推送以前須要調用該方法。後來查閱資料和Java API發現它很好的一個用處。咱們先來查看java的源碼

Observable類中setChanged()方法
Observable類中notifyObservers()方法
這裏必須標記爲true纔會推送消息,那麼這個到底有何好處,咱們拿氣象站模型來分析。 若是沒有setChanged方法,也是以前的Subject和Observer模型裏,一旦數據發生細微的變化,咱們都會對全部的觀察者進行推送。若是咱們須要在溫度變化1攝氏度以上才發送推送,調用setChanged()方法更加有效。固然,這個功能使用場景不多,可是也不排除會用到。固然更改Object和Observer模型也是能夠作到這個效果的!!!

4.3 Java內置觀察者更改氣象站

那麼利用氣象站模型來實際操做一下,依照以前的模型咱們代碼應該以下 1.WeatherData2

public class WeatherData2 extends Observable{
    
    private float temperature;
    
    private float humidity;
    
    private float pressure;
    
    //構造器不須要爲了記住觀察者創建數據模型
    public WeatherData2(){
        
    }
    
    
    public void measurementsChanged(){
        //在調用notifyObserver()須要指示狀態已經更改了
        setChanged();
       //這裏未使用notifyObserver(object),因此數據採用拉的邏輯
        notifyObservers(this);
    }
    
    public void setMeasurements(float temperature,float humidity,float pressure){
        this.temperature=temperature;
        this.humidity=humidity;
        this.pressure=pressure;
        measurementsChanged();
    }
    
    //如下方法爲pull操做提供
    public float getTemperature() {
        return temperature;
    }
    
    public float getHumidity() {
        return humidity;
    }

    public float getPressure() {
        return pressure;
    }
}
複製代碼

2.CurrentConditionsDisplay1

public class CurrentConditionsDisplay1 implements Observer,DisplayElement{
    
    private Observable observable;
    
    private float temperature;
    
    private float humidity;
    
    private float pressure;
    
    //構造器須要傳入Observable參數,並登記成爲觀察者
    public CurrentConditionsDisplay1(Observable observable){
        this.observable=observable;
        observable.addObserver(this);
    }
    
    //update方法增長Observable和數據對象做爲參數
    public void update(Observable o, Object arg) {
        if(arg instanceof WeatherData2){
            WeatherData2 weatherData2=(WeatherData2) arg;
            this.temperature=weatherData2.getTemperature();
            this.humidity=weatherData2.getHumidity();
            this.pressure=weatherData2.getPressure();
            display();
        }
        
    }
    
    /**
     * 
     * display:展現佈告板內容
     * @author 吉祥
     * @since JDK 1.6
     */
    public void display(){
        System.out.println("Current conditons:"+temperature
                +"F degrees and "+humidity+"% humidity");
    }
}
複製代碼

3.ForecastDisplay1

public class ForecastDisplay1 implements Observer,DisplayElement{
    private float temperature;
    private float humidity;
    private float pressure;
    private Observable observable;
    
    public ForecastDisplay1(Observable observable){
        this.observable=observable;
        observable.addObserver(this);
    }
    
   
    public void update(Observable o,Object arg){
        if(arg instanceof WeatherData2){
            WeatherData2 weatherData2=(WeatherData2) arg;
            this.temperature=weatherData2.getTemperature();
            this.humidity=weatherData2.getHumidity();
            this.pressure=weatherData2.getPressure();
            display();
        }
    }
        

    /**
     * 
     * display:展現佈告板內容
     * @author 吉祥
     * @since JDK 1.6
     */
    public void display(){
        System.out.println("Forecast: More of the same");
    }

}
複製代碼

4.StatisticsDisplay1

public class StatisticsDisplay1 implements Observer,DisplayElement{

    private float temperature;
    private float humidity;
    private float pressure;
    private Observable observable;
    
    public StatisticsDisplay1(Observable observable){
        this.observable=observable;
        observable.addObserver(this);
    }
    
   
    public void update(Observable o,Object arg){
        if(arg instanceof WeatherData2){
            WeatherData2 weatherData2=(WeatherData2) arg;
            this.temperature=weatherData2.getTemperature();
            this.humidity=weatherData2.getHumidity();
            this.pressure=weatherData2.getPressure();
            display();
        }
    }
        

    /**
     * 
     * display:展現佈告板內容
     * @author 吉祥
     * @since JDK 1.6
     */
    public void display(){
        System.out.println("Avg/Max/Min temperature= "+temperature
                +"/"+temperature+"/"+temperature);
    }
}
複製代碼

最後進行測試: WeatherStation1

public class WeatherStation1 {
    public static void main(String[] args){
        WeatherData2 weatherData=new WeatherData2();
        
        CurrentConditionsDisplay1 currentConditionsDisplay=new CurrentConditionsDisplay1(weatherData);
        StatisticsDisplay1 statisticsDisplay=new StatisticsDisplay1(weatherData);
        ForecastDisplay1 forecastDisplay=new ForecastDisplay1(weatherData);
        
        weatherData.setMeasurements(80, 65, 30.4f);
        weatherData.setMeasurements(82, 70, 29.2f);
        weatherData.setMeasurements(78, 90, 29.2f);
       
    }
}
複製代碼

結果最終以下:

Java 內置觀察者模式

咱們對比以前Subject和Observer的觀察者模式會發現二者輸出順序不同,這是爲何?

其實java.util.Observable不依賴於觀察者被通知的順序的,而且實現了他的notifyObserver()方法,這會致使通知觀察者的順序不一樣於Subject和Observer模型在具體類實現notifyObserver()方法。其實二者都沒有任何的代碼偏差,只是實現的方式不一樣致使不一樣的結果。

可是java.util.Observable類卻違背了以前第一章中針對接口編程,而非針對實現編程。恐怖的是,它也沒有接口實現,這就致使它的使用具備很高的侷限性和低複用性。若是一個對象不只僅是被觀察者,同時仍是另外一個超類的子類的時候,咱們沒法使用多繼承的方式來實現。咱們若是自行拓展的話,你會發現setChanged()方法是protected方法,這就表示只有java.util.Observable自身和其子類纔可使用這個方法。這就違反了第二個設計原則---------"多用組合,少用繼承"。這也是我通常不會使用Java自帶的設計者模式的緣由。

如今比較流行的觀察者模式,也就是RxJava,可是因爲這個框架涉及不只僅有觀察這模式,在以後整個設計模式整理玩不後,我會集中再講。

5.總結

到此,觀察者模式的講解已經所有講解完成。總結一下。

第四設計原則 爲交互對象之間的鬆耦合涉及而努力

觀察者模式 在對象之間定義一對多的依賴,這樣一來,當一個對象改變狀態,依賴它的對象都會收到通知,並自動更新。

相應的資料和代碼託管地址github.com/jixiang5200…

相關文章
相關標籤/搜索