天天看點

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

觀察者模式

參考《Head First設計模式》中的觀察者模式完成。

  • 氣象監測應用需求
  • 觀察者模式介紹
  • 手寫觀察者模式
  • Java内置的觀察者模式

氣象監測應用需求

根據氣象站實時輸出的濕度、溫度和氣壓值制作三塊布告闆。第一塊布告闆實時顯示目前的溫度、濕度和氣壓;第二塊布告闆顯示當日的平均溫度、最低溫度以及最高溫度;第三塊布告闆根據天氣顯示預報資訊。

氣象站提供了WeatherData類來獲得實時測量的溫度、濕度和氣壓值。

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

氣象站提供的接口如上如所示,三個getter方法用于擷取溫度、濕度和氣壓;每當氣象測量值變化,就會調用measurementsChanged()方法,實作measurementsChanged()就是我們的工作。

分析:

考慮到需求中布告闆顯示的内容可能會發生變化,為了友善日後修改程式,希望每次隻需要添加一塊新的布告闆,而其他程式不需要變化,努力做到互動對象之間松耦合。這個需求中,天氣測量值一旦變化,三個布告闆要及時獲得改變并展示,完全符合觀察者模式。

觀察者模式介紹

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

觀察者模式中有主題和觀察者兩個對象,以報紙的訂閱為例簡述觀察者模式。

  1. 報社的任務就是出版報紙;
  2. 向某家報社訂閱報紙,隻要有新報紙出版,報社就會給你送來。隻要你訂閱了該報紙,就會一直收到新報紙;
  3. 當你不想看該報紙時,取消訂閱,他們就不會再送報紙來;
  4. 隻要報社還在營運,就一直有人向他們訂閱或取消訂閱報紙。

    把訂閱報紙類比為觀察者模式,出版者就是“主題”,訂閱者就是“觀察者”。在該需求中,氣象站就是一個“主題”,而每塊布告闆就是一個“觀察者”。每個布告闆(觀察者)向氣象站(主題)注冊,就可以在測量值變化時獲得消息;取消某布告闆類比為“觀察者”向“主題”登出;新增一塊布告闆類比為新增一個氣象“主題”的“觀察者”。

手寫觀察者模式

面向對象的設計原則:針對接口程式設計,不針對實作程式設計。
Java設計模式--觀察者模式觀察者模式

根據上述原則,把主題和觀察者分别抽象為Subject和Observer接口。并添加一個DisplayElement接口用于展示。

Observer接口中隻有一個update()方法,用于當主題變化時執行。

Subject接口中有使得觀察者訂閱的方法registerObserver();當觀察者不想接收資訊時的取消訂閱方法removeObserver();以及當主題資料發生改變時通知訂閱該主題的所有觀察者的notifyObserver()方法。實際的主題,例如本需求中的WeatherData需要實作該接口,内部維護一個觀察者數組。registerObserver()方法中隻需要把觀察者加入自身的觀察者數組;removeObserver()方法中把要取消訂閱的觀察者移除;notifyObserver()則是周遊目前的觀察者數組,依次調用每個觀察者的update()即可。

Subject接口

/**
 * Created by Janet on 2017/11/6.
 * 觀察者模式中主題的接口
 */
public interface Subject {
    public void registerObserver(Observer o);//觀察者o訂閱主題
    public void removeObserver(Observer o);//觀察者o取消訂閱主題
    public void notifyObserver();//主題通知觀察者
}
           

Observer接口

/**
 * Created by Janet on 2017/11/6.
 * 觀察者模式中觀察者的接口
 */
public interface Observer {
    public void update(float temp,float humidity,float pressure);//主題傳來通知
}
           

用于展示的接口

/**
 * Created by Janet on 2017/11/9.
 * 展示結果的接口
 */
public interface DisplayElement {
    public void display();
}
           

WeatherData主題

import java.util.ArrayList;

/**
 * Created by Janet on 2017/11/6.
 * 氣象預報的主題
 */
public class WeatherData implements Subject {

    private ArrayList observers;//觀察者清單
    private float temperature;//溫度
    private float humidity;//濕度
    private float pressure;//氣壓

    //每次設定溫濕度和氣壓,主題都會發生改變
    public void setTemperature(float temperature,float humidity,float pressure){
        this.temperature = temperature;
        this.humidity = humidity;
        this.pressure = pressure;
        measurementsChanged();
    }

    public WeatherData(){
        observers = new ArrayList();
    }

    //主題發生改變時執行的方法
    public void measurementsChanged(){
        notifyObserver();
    }

    //觀察者o訂閱主題
    @Override
    public void registerObserver(Observer o) {
        observers.add(o);
    }

    //觀察者o取消訂閱主題
    @Override
    public void removeObserver(Observer o) {
        int index = observers.indexOf(o);
        if( index >=  ){
            observers.remove(o);
        }
    }

    //主題通知所有訂閱者
    @Override
    public void notifyObserver() {
        for(int i = ;i<observers.size();i++){
            Observer o = (Observer) observers.get(i);
            o.update(temperature,humidity,pressure);
        }
    }
}
           

觀察者1–展示溫度濕度氣壓的布告闆

/**
 * Created by Janet on 2017/11/6.
 * 第一塊展示溫濕度的布告闆
 */
public class CurrentConditionDisplay implements Observer,DisplayElement {

    private Subject weatherData;
    private float temperature;
    private float humidity;

    //一建立就向主題注冊
    public CurrentConditionDisplay(Subject weatherData){
        this.weatherData = weatherData;
        weatherData.registerObserver(this);
    }

    @Override
    public void display() {
        System.out.println("Current condition : "+temperature+"F degrees and "+humidity+"% humidity");
    }

    //主題變化時執行的函數
    @Override
    public void update(float temp, float humidity, float pressure) {
        this.temperature = temp;
        this.humidity = humidity;
        display();
    }
}
           

觀察者2–展示溫度最大最小及平均值的布告闆

/**
 * Created by Janet on 2017/11/6.
 * 第二塊展示最小,平均,最大溫度的布告闆
 */
public class StatisticsDisplay implements Observer,DisplayElement {

    private float minTemperature = Float.MAX_VALUE;//最小溫度
    private float avgTemperature;//平均溫度
    private float maxTemperature = Float.MIN_VALUE;//最大溫度
    private int num = ;//用于計算平均溫度
    private Subject weatherData;

    //建立觀察者時要向主題注冊
    public StatisticsDisplay(Subject weatherData){
        this.weatherData = weatherData;
        weatherData.registerObserver(this);
    }

    @Override
    public void display() {
        System.out.println("Avg/Max/Min temperature = "+avgTemperature+"/"+maxTemperature+"/"+minTemperature);
    }

    //主題變化時執行的函數
    @Override
    public void update(float temp, float humidity, float pressure) {

        if( minTemperature > temp ){
            minTemperature = temp;
        }
        if( maxTemperature < temp ){
            maxTemperature = temp;
        }
        avgTemperature = (avgTemperature * num + temp)/(num+);
        num++;
        display();
    }
}
           

測試函數

/**
 * Created by Janet on 2017/11/6.
 */
public class WeatherStation {
    public static void main(String[] args){
        WeatherData weatherData = new WeatherData();
        CurrentConditionDisplay o1 = new CurrentConditionDisplay(weatherData);//第一塊布告闆
        StatisticsDisplay o2 = new StatisticsDisplay(weatherData);
        weatherData.setTemperature(,,);
        weatherData.setTemperature(,,);
        weatherData.setTemperature(,,);
    }
}
           

執行結果如下:

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

Java内置的觀察者模式

java.util包中的Observable是主題的超類(注意,java内置的主題是類不是接口),Observer是觀察者的接口。

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

實際主題需要繼承超類Observeable,已經寫好了觀察者訂閱,取消訂閱以及通知的方法。其中setChanged()方法用于當主題資料修改時,用來标記狀态已經改變。WeatherData内部的notifyObservers()方法會首先判斷标志位是否更改,再通知各觀察者。

觀察者擷取主題變化的資料實際上有“推”和“拉”兩種方式。“推”表示主題資料發生變化時,主動把變化的資料推送給訂閱的觀察者們;“拉”表示當觀察者需要時主動向主題索取資料。上文中我們隻是自己實作了主題“推資料”的方法。Java内置的觀察者模式支援“推”和“拉”兩種擷取資料的模式。

利用Java内置實作WeatherData

注意,使用Java内置主題時,notifyObservers()有兩種重載方法:

notifyObservers()和notifyObservers(Object arg);notifyObservers(Object arg)可以傳送指定資料給觀察者。notifyObservers()用于拉資料的模式,notifyObservers(Object arg)用于推的模式。

import java.util.Observable;

/**
 * Created by Janet on 2017/11/9.
 * 使用java内置類實作主題
 */
public class WeatherData extends Observable{//繼承Observable,内部實作了主題的建立觀察者清單等方法
    private float temperature;
    private float humidity;
    private float pressure;

    public WeatherData(){}//此處無需自行建立觀察者清單,超類已經建立

    public void setMeasurements(float temperature,float humidity,float pressure){
        this.pressure = pressure;
        this.humidity = humidity;
        this.temperature = temperature;
        measurementsChanged();//資料改變後調用該方法
    }

    private void measurementsChanged() {
        setChanged();//設定改變标志位
        notifyObservers();//沒有參數傳入,說明是觀察者向主題索取資料
    }

    public float getTemperature(){
        return temperature;
    }
    public float getHumidity(){
        return humidity;
    }
    public float getPressure(){
        return pressure;
    }

}
           

利用Java内置實作觀察者

注意,Java内置的觀察者擷取主題動态有“推”和“拉”兩種模式。推即為主題

import java.util.Observable;
import java.util.Observer;

/**
 * Created by Janet on 2017/11/9.
 * 使用java内置類實作觀察者
 */
public class CurrentConditionsDisplay implements Observer,DisplayElement {
    private Observable observable;//觀察者内部記錄主題
    private float temperature;
    private float humidity;

    //構造函數中把觀察者加入到主題中
    public CurrentConditionsDisplay(Observable observable){
        this.observable = observable;
        observable.addObserver(this);//主題把觀察者加入到清單
    }

    @Override
    public void display() {
        System.out.println("Current conditions: "+temperature+" F degrees and "+humidity+"% humidity");
    }

    //不同的觀察者索取的資料由update方法展示
    @Override
    public void update(Observable o, Object arg) {
        if( o instanceof WeatherData ){
            WeatherData weatherData = (WeatherData) o;
            this.temperature = weatherData.getTemperature();
            this.humidity = weatherData.getHumidity();
            display();
        }
    }
}
           
import java.util.Observable;
import java.util.Observer;

/**
 * Created by Janet on 2017/11/9.
 */
public class StatisticsDisplay implements Observer,DisplayElement {
    private Observable observable;
    private float minTemperature = Float.MAX_VALUE;//最小溫度
    private float avgTemperature;//平均溫度
    private float maxTemperature = Float.MIN_VALUE;//最大溫度
    private int num = ;//用于計算平均溫度

    public StatisticsDisplay(Observable observable){
        this.observable = observable;
        observable.addObserver(this);
    }
    @Override
    public void display() {
        System.out.println("Avg/Max/Min temperature = "+avgTemperature+"/"+maxTemperature+"/"+minTemperature);
    }

    @Override
    public void update(Observable o, Object arg) {
        if( o instanceof WeatherData ){
            WeatherData weatherData = (WeatherData) o;
            float temp = weatherData.getTemperature();
            if( minTemperature > temp ){
                minTemperature = temp;
            }
            if( maxTemperature < temp ){
                maxTemperature = temp;
            }
            avgTemperature = (avgTemperature * num + temp)/(num+);
            num++;
            display();
        }
    }
}
           
import java.util.Observable;
import java.util.Observer;

/**
 * Created by Janet on 2017/11/9.
 */
public class ForecastDisplay implements Observer,DisplayElement {
    private Observable observable;//主題
    private float currentPressure = f;
    private float lastPressure;

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

    @Override
    public void display() {
        if( this.currentPressure < this.lastPressure ){
            System.out.println("氣壓變小");
        }else if( this.currentPressure > this.lastPressure ){
            System.out.println("氣壓變大");
        }else{
            System.out.println("氣壓不變");
        }

    }

    @Override
    public void update(Observable o, Object arg) {
        if( o instanceof WeatherData ){
            WeatherData weatherData = (WeatherData) o;
            this.lastPressure = currentPressure;
            this.currentPressure = weatherData.getPressure();
            display();
        }
    }
}
           

測試函數

/**
 * Created by Janet on 2017/11/9.
 */
public class WeatherStation {
    public static void main(String[] args){
        WeatherData weatherData = new WeatherData();
        CurrentConditionsDisplay currentConditionsDisplay = new CurrentConditionsDisplay(weatherData);
        ForecastDisplay forecastDisplay = new ForecastDisplay(weatherData);
        StatisticsDisplay statisticsDisplay = new StatisticsDisplay(weatherData);
        weatherData.setMeasurements(,,);
        weatherData.setMeasurements(,,);
        weatherData.setMeasurements(,,);
    }
}
           

運作結果如下:

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

Java内置和手寫觀察者模式的差別

  1. Java内置的主題采用類的形式,擴充性不如接口;
  2. Java内置觀察者建立的順序不等同于主題改變時通知的順序,在上例子中可見,而自寫的主題内部維護觀察者數組采用有序的ArrayList,可以保證順序。

繼續閱讀