觀察者模式(ObserverPattern)

張磊BARON發表於2016-09-18

轉載請註明出處:www.jianshu.com/p/d55ee6e83…
歡迎大家關注我的知乎專欄:zhuanlan.zhihu.com/baron
文章中的例子和思路均來自於《Head First》


場景

我們接到一個來自氣象局的需求:氣象局需要我們構建一套系統,這系統有兩個公告牌,分別用於顯示當前的實時天氣和未來幾天的天氣預報。當氣象局釋出新的天氣資料(WeatherData)後,兩個公告牌上顯示的天氣資料必須實時更新。氣象局同時要求我們保證程式擁有足夠的可擴充套件性,因為後期隨時可能要新增新的公告牌。

概況

這套系統中主要包括三個部分:氣象站(獲取天氣資料的物理裝置)、WeatherData(追蹤來自氣象站的資料,並更新公告牌)、公告牌(用於展示天氣資料)

WeatherStation

WeatherData知道如何跟氣象站聯絡,以獲得天氣資料。當天氣資料有更新時,WeatherData會更新兩個公告牌用於展示新的天氣資料。

錯誤示範

我們現來看看隔壁老王的實現思路:

public class WeatherData {

    //例項變數宣告
    ...

    public void measurementsChanged() {

        float temperature = getTemperature();
        float humidity = getHumidity();
        float pressure = getPressure();
        List<Float> forecastTemperatures = getForecastTemperatures();

        //更新公告牌
        currentConditionsDisplay.update(temperature, humidity, pressure);
        forecastDisplay.update(forecastTemperatures);
    }
    ...
}複製程式碼

上面這段程式碼是典型的針對實現程式設計,這會導致我們以後增加或刪除公告牌時必須修改程式。我們現在來看看觀察者模式,然後再回來看看如何將觀察者模式應用到這個程式。

####觀察者模式介紹 觀察者模式面向的需求是:A物件(觀察者)對B物件(被觀察者)的某種變化高度敏感,需要在B變化的一瞬間做出反應。舉個例子,新聞裡喜聞樂見的警察抓小偷,警察需要在小偷伸手作案的時候實施抓捕。在這個例子裡,警察是觀察者、小偷是被觀察者,警察需要時刻盯著小偷的一舉一動,才能保證不會錯過任何瞬間。程式裡的觀察者和這種真正的【觀察】略有不同,觀察者不需要時刻盯著被觀察者(例如A不需要每隔1ms就檢查一次B的狀態),二是採用註冊(Register)或者成為訂閱(Subscribe)的方式告訴被觀察者:我需要你的某某狀態,你要在它變化時通知我。採取這樣被動的觀察方式,既省去了反覆檢索狀態的資源消耗,也能夠得到最高的反饋速度。

觀察者模式通常基於SubjectObserver介面類來設計,下面是是類圖: Observer

觀察者模式的應用

結合上面的類圖,我們現在將觀察者模式應用到WeatherData專案中來。於是有了下面這張類圖: ObserverForWeatherStation

主題介面

/**
 * 主題(釋出者、被觀察者)
 */
public interface Subject {

    /**
     * 註冊觀察者
     */
    void registerObserver(Observer observer);

    /**
     * 移除觀察者
     */
    void removeObserver(Observer observer);

    /**
     * 通知觀察者
     */
    void notifyObservers(); 
}複製程式碼

觀察者介面


/**
 * 觀察者
 */
public interface Observer {
    void update();
}複製程式碼

公告牌用於顯示的公共介面

public interface DisplayElement {
    void display();
}複製程式碼

下面我們再來看看WeatherData是如何實現的

public class WeatherData implements Subject {

    private List<Observer> observers;

    private float temperature;//溫度
    private float humidity;//溼度
    private float pressure;//氣壓
    private List<Float> forecastTemperatures;//未來幾天的溫度

    public WeatherData() {
        this.observers = new ArrayList<Observer>();
    }

    @Override
    public void registerObserver(Observer observer) {
        this.observers.add(observer);
    }

    @Override
    public void removeObserver(Observer observer) {
        this.observers.remove(observer);
    }

    @Override
    public void notifyObservers() {
        for (Observer observer : observers) {
            observer.update();
        }
    }

    public void measurementsChanged() {
        notifyObservers();
    }

    public void setMeasurements(float temperature, float humidity, 
    float pressure, List<Float> forecastTemperatures) {
        this.temperature = temperature;
        this.humidity = humidity;
        this.pressure = pressure;
        this.forecastTemperatures = forecastTemperatures;
        measurementsChanged();
    }

    public float getTemperature() {
        return temperature;
    }

    public float getHumidity() {
        return humidity;
    }

    public float getPressure() {
        return pressure;
    }

    public List<Float> getForecastTemperatures() {
        return forecastTemperatures;
    }
}複製程式碼

顯示當前天氣的公告牌CurrentConditionsDisplay

public class CurrentConditionsDisplay implements Observer, DisplayElement {

    private WeatherData weatherData;

    private float temperature;//溫度
    private float humidity;//溼度
    private float pressure;//氣壓

    public CurrentConditionsDisplay(WeatherData weatherData) {
        this.weatherData = weatherData;
        this.weatherData.registerObserver(this);
    }

    @Override
    public void display() {
        System.out.println("當前溫度為:" + this.temperature + "℃");
        System.out.println("當前溼度為:" + this.humidity);
        System.out.println("當前氣壓為:" + this.pressure);
    }

    @Override
    public void update() {
        this.temperature = this.weatherData.getTemperature();
        this.humidity = this.weatherData.getHumidity();
        this.pressure = this.weatherData.getPressure();
        display();
    }
}複製程式碼

顯示未來幾天天氣的公告牌ForecastDisplay

public class ForecastDisplay implements Observer, DisplayElement {

    private WeatherData weatherData;

    private List<Float> forecastTemperatures;//未來幾天的溫度

    public ForecastDisplay(WeatherData weatherData) {
        this.weatherData = weatherData;
        this.weatherData.registerObserver(this);
    }

    @Override
    public void display() {
        System.out.println("未來幾天的氣溫");
        int count = forecastTemperatures.size();
        for (int i = 0; i < count; i++) {
            System.out.println("第" + i + "天:" + forecastTemperatures.get(i) + "℃");
        }
    }

    @Override
    public void update() {
        this.forecastTemperatures = this.weatherData.getForecastTemperatures();
        display();
    }
}複製程式碼

到這裡,我們整個氣象局的WeatherData應用就改造完成了。兩個公告牌CurrentConditionsDisplayForecastDisplay實現了ObserverDisplayElement介面,在他們的構造方法中會呼叫WeatherDataregisterObserver方法將自己註冊成觀察者,這樣被觀察者WeatherData就會持有觀察者的應用,並將它們儲存到一個集合中。當被觀察者`WeatherData狀態傳送變化時就會遍歷這個集合,迴圈呼叫觀察者公告牌更新資料的方法。後面如果我們需要增加或者刪除公告牌就只需要新增或者刪除實現了ObserverDisplayElement介面的公告牌就好了。

觀察者模式將觀察者和主題(被觀察者)徹底解耦,主題只知道觀察者實現了某一介面(也就是Observer介面)。並不需要觀察者的具體類是誰、做了些什麼或者其他任何細節。任何時候我們都可以增加新的觀察者。因為主題唯一依賴的東西是一個實現了Observer介面的物件列表。

好了,我們測試下利用觀察者模式重構後的程式:

public class ObserverPatternTest {

    public static void main(String[] args) {

        WeatherData weatherData = new WeatherData();
        CurrentConditionsDisplay currentConditionsDisplay = new CurrentConditionsDisplay(weatherData);
        ForecastDisplay forecastDisplay = new ForecastDisplay(weatherData);

        List<Float> forecastTemperatures = new ArrayList<Float>();
        forecastTemperatures.add(22f);
        forecastTemperatures.add(-1f);
        forecastTemperatures.add(9f);
        forecastTemperatures.add(23f);
        forecastTemperatures.add(27f);
        forecastTemperatures.add(30f);
        forecastTemperatures.add(10f);
        weatherData.setMeasurements(22f, 0.8f, 1.2f, forecastTemperatures);
    }
}複製程式碼

輸出結果:

當前溫度為:22.0℃
當前溼度為:0.8
當前氣壓為:1.2
未來幾天的氣溫
第0天:22.0℃
第1天:-1.0℃
第2天:9.0℃
第3天:23.0℃
第4天:27.0℃
第5天:30.0℃
第6天:10.0℃複製程式碼

原始碼地址:github.com/BaronZ88/De…

如果大家喜歡這一系列的文章,歡迎關注我的知乎專欄、GitHub、簡書部落格。

相關文章