1. 程式人生 > >《Head First 設計模式》筆記2

《Head First 設計模式》筆記2

觀察者模式(Observer)

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

初識

我們先來了解一下報紙和雜誌的訂閱是怎麼回事:
1. 報社的業務就是出版報紙、雜誌等各種出版物。
2. 如果我想看報社的 A 報紙和 B 雜誌,那麼就向報社訂閱 A 報紙和 B 雜誌。
3. 當他們有新的 A 報紙或 B 雜誌出版時,就會向你派送,只要你是他們的訂戶,你就會一直收到新報紙,新雜誌。
4. 如果你不想看 B 雜誌了,取消訂閱,他們就不會再送新的 B 雜誌給你了。但不會影響你訂閱的 A 報紙。
5. 只要報社還在運營,就會一直有人向他們訂閱或取消報紙等出版物。

在觀察者模式中,出版者報社 = 主題(subject),而我們訂閱者 = 觀察者(observer)。

栗子

現在有一個系統,包括三部分:
- 氣象站:獲取實際氣象資料的物理裝置。
- WeatherData 類:追蹤來自氣象站的資料,並更新佈告板(具體怎麼追蹤的不用管)。
- 佈告板:顯示目前的天氣狀況。

現在的專案是,利用 WeatherData 類取得氣象資料,更新三個佈告板:目前狀況、氣象統計和天氣預報。

WeatherData 類:

class WeatherData {
    private float temperature; // 溫度
    private
float humidity; // 溼度 private float pressure; // 氣壓 public float getTemperature() { return this.temperature; } public float getHumidity() { return this.humidity; } public float getPressure() { return this.pressure; } /** * 一旦氣象資料更新,就會被呼叫 */
public void measurementsChanged() { // 你的程式碼 } }

而我們的工作就是實現 measurementsChanged,讓它來更新我們的三個佈告板(不用知道該方法是如何被呼叫的,我們只用知道該方法被呼叫時,我們的佈告板也被更新了)。

佈告板肯定還會新增或者刪除的,所以專案一定要支援擴充套件。

錯誤示範

public void measurementsChanged() {
    // 獲得最近的天氣資料
    float temp = getTemperature();
    float humidity = getHumidity();
    float pressure = getPressure();
    // 更新三個佈告板
    currentConditionsDisplay.update(temp, humidity, pressure);
    statisticsDisplay.update(temp, humidity, pressure);
    forecastDisplay.update(temp, humidity, pressure);
}

有什麼問題呢?
1. 如果有新增和刪除佈告板的需求,那麼就必須改動這些程式碼,不利於專案的擴充套件。(想一想每次都要修改、編譯、打包就覺得累)
2. 這些佈告板都有一個 update 方法,所以這些佈告板應該用帶有 update 方法的介面或抽象類替代而不是具體實現。

滿足需求

一個 WeatherData 類和多個佈告板有聯絡,並且佈告板需要 WeatherData 類來通知資料,所以這裡應該使用觀察者模式。

定義主題介面:

interface Subject {
    // 觀察者註冊
    void registerObserver(Observer o);
    // 刪除觀察者
    void removeObserver(Observer o);
    // 通知所有觀察者
    void notifyObservers();
}

觀察者介面:

interface Observer {
    void update(float temperature, float humidity, float pressure);
}

佈告板顯示功能:

interface DisplayElement {
    void display();
}

然後就是把 WeatherData 類改造成 Subject:

class WeatherData implements Subject {
    private float temperature; // 溫度
    private float humidity; // 溼度
    private float pressure; // 氣壓

    private List<Observer> observers; // 觀察者們

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

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

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

    @Override
    public void notifyObservers() {
        // 通知每一個觀察者更新資料
        for (Observer observer : observers) {
            observer.update(temperature, humidity, pressure);
        }
    }

    public void measurementsChanged() {
        notifyObservers();
    }

    public float getTemperature() {
        return this.temperature;
    }

    public float getHumidity() {
        return this.humidity;
    }

    public float getPressure() {
        return this.pressure;
    }

    // 模擬資料,方便測試
    public void mock(float temperature, float humidity, float pressure) {
        this.temperature = temperature;
        this.humidity = humidity;
        this.pressure = pressure;
        measurementsChanged();
    }
}

把佈告板變成觀察者:

class CurrentConditionsDisplay implements Observer, DisplayElement {
    private Subject weatherData; // 儲存主題,方便之後取消觀察
    private float temperature;
    private float humidity;

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

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

    @Override
    public void display() {
        System.out.println("目前狀況:" + temperature + " 攝氏度," + humidity + "% 溼度");
    }
}

測試

public class Main {
    public static void main(String[] args) {
        WeatherData weatherData = new WeatherData();
        CurrentConditionsDisplay display1 = new CurrentConditionsDisplay(weatherData);
        System.out.println("通知前");
        display1.display();
        System.out.println("第一次通知後");
        weatherData.mock(25, 60, 30.4f);
        System.out.println("第二次通知後");
        weatherData.mock(20, 72, 41.7f);
    }
}

輸出:
通知前
目前狀況:0.0 攝氏度,0.0% 溼度
第一次通知後
目前狀況:25.0 攝氏度,60.0% 溼度
第二次通知後
目前狀況:20.0 攝氏度,72.0% 溼度

使用 Java 內建的觀察者模式

Java 內建的 Observer 介面和 Observable 類和我們實現的 Subject 介面與 Observer 介面很相似。

這裡就將使用這兩個內建的介面和類重寫上面的天氣軟體。

WeatherData 類繼承 Observable 類:

import java.util.Observable;

class WeatherData extends Observable {
    private float temperature; // 溫度
    private float humidity; // 溼度
    private float pressure; // 氣壓

    public void measurementsChanged() {
        // 指示狀態已經改變;如果不指示的話,notifyObservers 無法發出通知
        // 詳細看原始碼實現
        setChanged();
        notifyObservers();
    }

    // 觀察者會利用這些 getter 方法取得 WeatherData 物件的狀態
    public float getTemperature() {
        return this.temperature;
    }

    public float getHumidity() {
        return this.humidity;
    }

    public float getPressure() {
        return this.pressure;
    }

    // 模擬資料,方便測試
    public void mock(float temperature, float humidity, float pressure) {
        this.temperature = temperature;
        this.humidity = humidity;
        this.pressure = pressure;
        measurementsChanged();
    }
}

佈告板實現 Observer 介面:

import java.util.Observer;

class CurrentConditionsDisplay implements Observer, DisplayElement {
    private Observable observable;
    private float temperature;
    private float humidity;

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

    @Override
    public void update(Observable observable, Object arg) {
        // 先確定接收的是來自 WeatherData 的,而不是來自其他可觀察物件的
        if (observable instanceof WeatherData) {
            WeatherData weatherData = (WeatherData) observable;
            this.temperature = weatherData.getTemperature();
            this.humidity = weatherData.getHumidity();
            display();
        }
    }

    @Override
    public void display() {
        System.out.println("目前狀況:" + temperature + " 攝氏度," + humidity + "% 溼度");
    }
}

測試

public class Main {
    public static void main(String[] args) {
        WeatherData weatherData = new WeatherData();
        CurrentConditionsDisplay display1 = new CurrentConditionsDisplay(weatherData);
        System.out.println("通知前");
        display1.display();
        System.out.println("第一次通知後");
        weatherData.mock(25, 60, 30.4f);
        System.out.println("第二次通知後");
        weatherData.mock(20, 72, 41.7f);
    }
}

輸出結果和上面一致:
通知前
目前狀況:0.0 攝氏度,0.0% 溼度
第一次通知後
目前狀況:25.0 攝氏度,60.0% 溼度
第二次通知後
目前狀況:20.0 攝氏度,72.0% 溼度

如果有多個不同的公告板,上面輸出的結果順序可能會不同,因為 Observable 類通知的先後順序不依賴於註冊的先後。比如 A、B 都訂了同一份報紙,並且 A 比 B 先訂閱,但派送新報紙時,可能 A 先收到,可能 B 先收到,與註冊先後無關,這是鬆耦合的體現。

注意:WeatherData 類是通過繼承 Observable 類來獲得可被觀察的行為的,這違背了設計原則的“多用組合,少用繼承”。