Java設計模式---觀察者模式

故事概要

概要

故事要從一個業務需求開始:

天氣監測站目前可以監測到氣溫,氣壓,溼度三種指標,有一個WeatherData類,當前面的三個數據發生變化時,就會調用WeatherData類的setWeatherData()方法,改變天氣數據.

現在要求:需要有兩個顯示裝置顯示現在的狀態和平均狀態.當監測的數據有變化時,就更新顯示.而且需要提供給第三方的人員可調用這些數據以設置想要的顯示裝置.如下圖:

業務需求圖

下面是給出的WeatherData類:

public class WeatherData {

    private float temp;
    private float humidity;
    private float pressure;

    public WeatherData(){}

    public void setWeatherData(float temp, float humidity, float pressure){
        this.temp = temp;
        this.humidity = humidity;
        this.pressure = pressure;
        dataChanged();
    }

    public void dataChanged(){
        //數據發生變化則調用此方法,可在此處填寫你的代碼
    }

    public float getTemp() {
        return temp;
    }

    public float getHumidity() {
        return humidity;
    }

    public float getPressure() {
        return pressure;
    }
}

提取已知條件

目前已知的條件:
1. WeatherData具有三個getter方法,可獲取到溫度,氣壓和溼度三個數值.
2. 當監測到新的數據時就會調用dataChanged()方法.
3. 需要實現兩個顯示器,一個顯示當前狀況(CurrentConditionsDisplay),一個顯示平均狀態(AvgStateDisplay).
4. 系統要能擴展,第三方可以組合實現顯示器.

簡單實現

根據以上前提,可以用以下方法實現:

public void dataChanged(){
    //數據發生變化則調用此方法,可在此處填寫你的代碼
    currentConditionsDisplay.update(temp, humidity, pressure);
    avgStateDisplay.update(temp, humidity, pressure);
}

其中,CurrentConditionsDisplay和AvgStateDisplay如下:

//顯示當前狀態
public class CurrentConditionsDisplay{

    private float temp;
    private float humidity;
    private float pressure;

    public void update(float temp, float humidity, float pressure) {
        this.temp = temp;
        this.humidity = humidity;
        this.pressure = pressure;
        display();
    }

    public void display(){
        //顯示當前狀態
        String displayData = "CurrentConditions[temp=" + temp + ", humidity=" + humidity + 
            ", pressure="+ pressure + "]";
        System.out.println(displayData);
    }
}


//顯示平均狀態
public class AvgStateDisplay {

    public void display() {
        System.out.println(getShowInfo());
    }

    private String getShowInfo(){
        StringBuilder displayData = new StringBuilder();
        if (tempList.size() == 0) {
            displayData.append("暫時沒有數據...");
            return displayData.toString();
        }

        float tempAve = calculatorAve(tempList);
        displayData.append("平均氣溫:" + tempAve);

        float humidityAve = calculatorAve(humidityList);
        displayData.append(",平均溼度:" + humidityAve);

        float pressureAve = calculatorAve(pressureList);
        displayData.append(",平均壓強:" + pressureAve);

        return displayData.toString();
    }

    private float calculatorAve(List<Float> dataList){
        float sum = 0F;
        for (Float float1 : dataList) {
            sum = sum + float1;
        }
        float ave = sum/dataList.size();
        return ave;
    }

    private List<Float> tempList;
    private float temp;
    private List<Float> humidityList;
    private float humidity;
    private List<Float> pressureList;
    private float pressure;

    public void update(float temp, float humidity, float pressure) {
        this.temp = temp;
        this.humidity = humidity;
        this.pressure = pressure;
        if (tempList == null) {
            tempList = new ArrayList<Float>();
        }
        tempList.add(temp);

        if (humidityList == null) {
            humidityList = new ArrayList<Float>();
        }
        humidityList.add(humidity);

        if (pressureList == null) {
            pressureList = new ArrayList<Float>();
        }
        pressureList.add(pressure);
        display();
    }
}

這不就實現了剛纔的功能:
1. 能顯示當前狀態
2. 能顯示平均狀態
3. 第三方可以通過WeatherData的dataChanged()方法獲取數據,自己任意顯示.

有沒有問題?

上一篇文章學習了Java設計的原則:

封裝變化和麪向接口編程

上述的實現中:

public void dataChanged(){
    //數據發生變化則調用此方法,可在此處填寫你的代碼
    currentConditionsDisplay.update(temp, humidity, pressure);
    avgStateDisplay.update(temp, humidity, pressure);
}

這個裏面的3,4兩句是變化的,這部分沒有封裝.而且應該面向接口編程而不是面向具體的類編程,如果增加第三方的顯示器,只能修改此處添加代碼

認識觀察者模式

從訂閱報紙開始

報紙的訂閱過程一般如下:

  • 報社的業務就是出版報紙。
  • 向某家報社訂閱報紙,只要他們有新報紙出版,就會給你送來。只要你是他們的訂戶,你就會一直收到新報紙。
  • 當你不想再看報紙的時候,取消訂閱,他們就不會再送新報紙來。
  • 只要報社還在運營,就會一直有人(或單位)向他們訂閱報紙或取消訂閱報紙。

觀察者模式就和上面的訂報紙差不多:

出版者改稱爲“主題”(Subject),訂閱者改稱爲“觀察者”(Observer)

觀察者模式

觀察者模式

觀察者模式

觀察者模式

觀察者模式

定義觀察者模式

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

觀察者模式

觀察者模式的類圖

觀察者模式類圖

從以上類圖可以看出,觀察者模式的是遵守針對接口編程原則的.

手動實現氣象站的觀察者模式

1.觀察者接口

觀察者是一個接口:Observer

public interface Observer {
    //用於更新的方法
    void update(float temp, float humidity, float pressure);
}

2.觀察對象接口

貫徹對象接口有三個作用:
1. 觀察者註冊爲觀察者
2. 觀察者解除註冊
3. 數據更新時通知觀察者

public interface Subject {

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

    //解除註冊
    void unregisterObserver(Observer observer);

    //通知觀察者
    void notifyObserver();

}

3.新的WeatherData類

其中的setter是給氣象站設置數據使用.getter是給第三方獲取數據使用

public class WeatherData implements Subject{

    private float temp;
    private float humidity;
    private float pressure;
    private List<Observer> observerList;

    public WeatherData(){
        observerList = new ArrayList<Observer>();
    }

    public void setWeatherData(float temp, float humidity, float pressure){
        this.temp = temp;
        this.humidity = humidity;
        this.pressure = pressure;
        dataChanged();
    }

    public void dataChanged(){
        notifyObserver();
    }

    public void registerObserver(Observer observer) {
        observerList.add(observer);
    }

    public void unregisterObserver(Observer observer) {
        int index = observerList.indexOf(observer);
        if (index >= 0) {
            observerList.remove(observer);
        }
    }

    public void notifyObserver() {
        for (Observer observer : observerList) {
            observer.update(temp, humidity, pressure);
        }
    }

    public float getTemp() {
        return temp;
    }

    public void setTemp(float temp) {
        this.temp = temp;
    }

    public float getHumidity() {
        return humidity;
    }

    public void setHumidity(float humidity) {
        this.humidity = humidity;
    }

    public float getPressure() {
        return pressure;
    }

    public void setPressure(float pressure) {
        this.pressure = pressure;
    }
}

4.顯示器接口

顯示器接口是用來實現各種顯示器的

public interface DisplayElement {

    void display();
}

5.顯示器的實現類:CurrentConditionsDisplay和AvgStateDisplay類

CurrentConditionsDisplay:當前狀態顯示器
AvgStateDisplay:平均狀態顯示器

public class CurrentConditionsDisplay implements Observer, DisplayElement {

    private WeatherData weatherData;

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

    public void unregister(){
        weatherData.unregisterObserver(this);
    }

    public void display() {
        String displayData = "CurrentConditions[temp=" + temp + ", humidity=" + humidity + ", pressure="
                + pressure + "]";
        System.out.println(displayData);
    }

    private float temp;
    private float humidity;
    private float pressure;
    public void update(float temp, float humidity, float pressure) {
        this.temp = temp;
        this.humidity = humidity;
        this.pressure = pressure;
        display();
    }
}

//---------------------------------------------------------------//

public class AvgStateDisplay implements Observer, DisplayElement{

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

    public void unregister(){
        weatherData.unregisterObserver(this);
    }

    public void display() {
        System.out.println(getShowInfo());
    }

    private String getShowInfo(){
        StringBuilder displayData = new StringBuilder();
        if (tempList.size() == 0) {
            displayData.append("暫時沒有數據...");
            return displayData.toString();
        }

        float tempAve = calculatorAve(tempList);
        displayData.append("平均氣溫:" + tempAve);

        float humidityAve = calculatorAve(humidityList);
        displayData.append(",平均溼度:" + humidityAve);

        float pressureAve = calculatorAve(pressureList);
        displayData.append(",平均壓強:" + pressureAve);

        return displayData.toString();
    }

    private float calculatorAve(List<Float> dataList){
        float sum = 0F;
        for (Float float1 : dataList) {
            sum = sum + float1;
        }
        float ave = sum/dataList.size();
        return ave;
    }

    private List<Float> tempList;
    private float temp;
    private List<Float> humidityList;
    private float humidity;
    private List<Float> pressureList;
    private float pressure;
    public void update(float temp, float humidity, float pressure) {
        this.temp = temp;
        this.humidity = humidity;
        this.pressure = pressure;
        if (tempList == null) {
            tempList = new ArrayList<Float>();
        }
        tempList.add(temp);

        if (humidityList == null) {
            humidityList = new ArrayList<Float>();
        }
        humidityList.add(humidity);

        if (pressureList == null) {
            pressureList = new ArrayList<Float>();
        }
        pressureList.add(pressure);
        display();
    }
}

6.測試

測試氣象站數據變化時能否通知當前狀態顯示器和平均狀態顯示器更新數據

public class WeatherStation {

    public static void main(String[] args) {
        WeatherData weatherData = new WeatherData();

        CurrentConditionsDisplay currentDisplay = new CurrentConditionsDisplay(weatherData);
        AvgStateDisplay stateDisplay = new AvgStateDisplay(weatherData);

        weatherData.setWeatherData(25.0F, 80F, 1.01F);
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        weatherData.setWeatherData(26.2F, 76F, 1.02F);
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        weatherData.setWeatherData(27.8F, 70F, 1.01F);
    }
}

輸出結果爲:

CurrentConditions[temp=25.0, humidity=80.0, pressure=1.01]
平均氣溫:25.0,平均溼度:80.0,平均壓強:1.01
CurrentConditions[temp=26.2, humidity=76.0, pressure=1.02]
平均氣溫:25.6,平均溼度:78.0,平均壓強:1.015
CurrentConditions[temp=27.8, humidity=70.0, pressure=1.01]
平均氣溫:26.333334,平均溼度:75.333336,平均壓強:1.0133333

7.增加酷熱指數的顯示器:HeatIndexDisplay

public class HeatIndexDisplay implements Observer, DisplayElement{

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

    public void unregister(){
        weatherData.unregisterObserver(this);
    }

    @Override
    public void display() {
        System.out.println("當前酷熱指數:" + getHeatIndex());
    }

    private float getHeatIndex(){
        float heatIndex = 0F;
        //此處酷熱指數計算公式不正確,是隨意寫的
        heatIndex = (float) (temp*Math.PI + humidity*Math.E + pressure*1.01315)/5;
        return heatIndex;
    }

    private float temp;
    private float humidity;
    private float pressure;
    @Override
    public void update(float temp, float humidity, float pressure) {
        this.temp = temp;
        this.humidity = humidity;
        this.pressure = pressure;
        display();
    }
}

8.測試第三方的顯示器

public class WeatherStation {

    public static void main(String[] args) {
        WeatherData weatherData = new WeatherData();

        CurrentConditionsDisplay currentDisplay = new CurrentConditionsDisplay(weatherData);
        AvgStateDisplay stateDisplay = new AvgStateDisplay(weatherData);
        HeatIndexDisplay heatIndexDisplay = new HeatIndexDisplay(weatherData);

        weatherData.setWeatherData(25.0F, 80F, 1.01F);
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        weatherData.setWeatherData(26.2F, 76F, 1.02F);
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        weatherData.setWeatherData(27.8F, 70F, 1.01F);
    }
}

測試結果如下:

CurrentConditions[temp=25.0, humidity=80.0, pressure=1.01]
平均氣溫:25.0,平均溼度:80.0,平均壓強:1.01
當前酷熱指數:59.40513
CurrentConditions[temp=26.2, humidity=76.0, pressure=1.02]
平均氣溫:25.6,平均溼度:78.0,平均壓強:1.015
當前酷熱指數:57.98651
CurrentConditions[temp=27.8, humidity=70.0, pressure=1.01]
平均氣溫:26.333334,平均溼度:75.333336,平均壓強:1.0133333
當前酷熱指數:55.727856

小結

經過以上測試,我們使用了觀察者模式實現了:
1. 氣象站測試到數據–>發佈數據變更—>更新各個不同類型的顯示器.
2. 實現了當前狀況的顯示和平均狀態的顯示
3. 開發者可是自行實現多種形式的顯示器

Java內置的觀察者模式

Java內置的主題

Java內部的主題存在java.util包下,其源碼如下:

public class Observable {
    private boolean changed = false;
    private Vector<Observer> obs;

    public Observable() {
        obs = new Vector<>();
    }

    public synchronized void addObserver(Observer o) {
        if (o == null)
            throw new NullPointerException();
        if (!obs.contains(o)) {
            obs.addElement(o);
        }
    }

    public synchronized void deleteObserver(Observer o) {
        obs.removeElement(o);
    }

    public void notifyObservers() {
        notifyObservers(null);
    }

    public void notifyObservers(Object arg) {

        Object[] arrLocal;

        synchronized (this) {
            if (!changed)
                return;
            arrLocal = obs.toArray();
            clearChanged();
        }

        for (int i = arrLocal.length-1; i>=0; i--)
            ((Observer)arrLocal[i]).update(this, arg);
    }

    public synchronized void deleteObservers() {
        obs.removeAllElements();
    }

    protected synchronized void setChanged() {
        changed = true;
    }

    protected synchronized void clearChanged() {
        changed = false;
    }

    public synchronized boolean hasChanged() {
        return changed;
    }

    public synchronized int countObservers() {
        return obs.size();
    }
}

內置的主題類提供:增加觀察者,刪除觀察者,刪除所有觀察者,提醒某個觀察者,提醒所有觀察者,獲取觀察者的數量,設置數據變化等方法.

與自定義的主題不同的是:

  1. java內置的主題是一個類不是一個接口.
  2. java內置的主題必須手動設置數據變化,即setChanged().否則主題不會通知觀察者.

Java內置的觀察者

java內置的主題在java.util包下,其源碼如下:

public interface Observer {
    void update(Observable o, Object arg);
}

與自定義的觀察者一致.

使用Java內置的觀察者模式實現氣象站

WeatherData類

public class WeatherData extends Observable{

    private float temp;
    private float humidity;
    private float pressure;

    public WeatherData(){}

    public void setWeatherData(float temp, float humidity, float pressure){
        this.temp = temp;
        this.humidity = humidity;
        this.pressure = pressure;
        dataChanged();
    }

    public void dataChanged(){
        setChanged();
        notifyObservers();
    }

    public float getTemp() {
        return temp;
    }

    public float getHumidity() {
        return humidity;
    }

    public float getPressure() {
        return pressure;
    }
}

CurrentConditionsDisplay

public class CurrentConditionsDisplay implements Observer, DisplayElement {

    private Observable observable;
    public CurrentConditionsDisplay(Observable observable){
        this.observable = observable;
        observable.addObserver(this);
    }

    public void unregister(){
        observable.deleteObserver(this);
    }

    public void display() {
        String displayData = "CurrentConditions[temp=" + temp + ", humidity=" + humidity + ", pressure="
                + pressure + "]";
        System.out.println(displayData);
    }

    private float temp;
    private float humidity;
    private float pressure;

    @Override
    public void update(Observable o, Object arg) {
        if (o instanceof WeatherData) {
            WeatherData weatherData = (WeatherData) o;
            this.temp = weatherData.getTemp();
            this.humidity = weatherData.getHumidity();
            this.pressure = weatherData.getPressure();
            display();
        }
    }
}

AvgStateDisplay

public class AvgStateDisplay implements Observer, DisplayElement{

    private Observable observable;
    public AvgStateDisplay(Observable observable){
        observable.addObserver(this);
        this.observable = observable;
    }

    public void unregister(){
        observable.deleteObserver(this);
    }

    public void display() {
        System.out.println(getShowInfo());
    }

    private String getShowInfo(){
        StringBuilder displayData = new StringBuilder();
        if (tempList.size() == 0) {
            displayData.append("暫時沒有數據...");
            return displayData.toString();
        }

        float tempAve = calculatorAve(tempList);
        displayData.append("平均氣溫:" + tempAve);

        float humidityAve = calculatorAve(humidityList);
        displayData.append(",平均溼度:" + humidityAve);

        float pressureAve = calculatorAve(pressureList);
        displayData.append(",平均壓強:" + pressureAve);

        return displayData.toString();
    }

    private float calculatorAve(List<Float> dataList){
        float sum = 0F;
        for (Float float1 : dataList) {
            sum = sum + float1;
        }
        float ave = sum/dataList.size();
        return ave;
    }

    private List<Float> tempList;
    private float temp;
    private List<Float> humidityList;
    private float humidity;
    private List<Float> pressureList;
    private float pressure;

    @Override
    public void update(Observable o, Object arg) {
        if (o instanceof WeatherData) {
            WeatherData weatherData = (WeatherData) o;
            this.temp = weatherData.getTemp();
            this.humidity = weatherData.getHumidity();
            this.pressure = weatherData.getPressure();

            if (tempList == null) {
                tempList = new ArrayList<Float>();
            }
            tempList.add(temp);

            if (humidityList == null) {
                humidityList = new ArrayList<Float>();
            }
            humidityList.add(humidity);

            if (pressureList == null) {
                pressureList = new ArrayList<Float>();
            }
            pressureList.add(pressure);

            display();
        }
    }
}

HeatIndexDisplay

public class HeatIndexDisplay implements Observer, DisplayElement{

    private Observable observable;
    public HeatIndexDisplay(Observable observable){
        observable.addObserver(this);
        this.observable = observable;
    }

    public void unregister(){
        observable.deleteObserver(this);
    }

    @Override
    public void display() {
        System.out.println("當前酷熱指數:" + getHeatIndex());
    }

    private float getHeatIndex(){
        float heatIndex = 0F;
        heatIndex = (float) (temp*Math.PI + humidity*Math.E + pressure*1.01315)/5;
        return heatIndex;
    }

    private float temp;
    private float humidity;
    private float pressure;

    @Override
    public void update(Observable o, Object arg) {
        if (o instanceof WeatherData) {
            WeatherData weatherData = (WeatherData) o;
            this.temp = weatherData.getTemp();
            this.humidity = weatherData.getHumidity();
            this.pressure = weatherData.getPressure();
            display();
        }
    }
}

測試類

public class WeatherStation {

    public static void main(String[] args) {
        WeatherData weatherData = new WeatherData();

        CurrentConditionsDisplay currentDisplay = new CurrentConditionsDisplay(weatherData);
        AvgStateDisplay stateDisplay = new AvgStateDisplay(weatherData);
        HeatIndexDisplay heatIndexDisplay = new HeatIndexDisplay(weatherData);

        weatherData.setWeatherData(25.0F, 80F, 1.01F);
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        weatherData.setWeatherData(26.2F, 76F, 1.02F);
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        weatherData.setWeatherData(27.8F, 70F, 1.01F);
    }
}

測試結果如下:

當前酷熱指數:59.40513
平均氣溫:25.0,平均溼度:80.0,平均壓強:1.01
CurrentConditions[temp=25.0, humidity=80.0, pressure=1.01]
當前酷熱指數:57.98651
平均氣溫:25.6,平均溼度:78.0,平均壓強:1.015
CurrentConditions[temp=26.2, humidity=76.0, pressure=1.02]
當前酷熱指數:55.727856
平均氣溫:26.333334,平均溼度:75.333336,平均壓強:1.0133333
CurrentConditions[temp=27.8, humidity=70.0, pressure=1.01]

從上面的結果可以看出與自定義的觀察者模式顯示的一致.

總結

觀察者模式的使用過程如下:

  1. 數據變化的對象繼承Observable類.在數據變化後調用setChanged()方法.
  2. 觀察者類實現Observer接口,並重新update()方法.
  3. 使用時用觀察者內部持有的可觀察接口註冊與解除註冊爲觀察者.

源代碼在這:使勁戳我

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章