《Head First 設計模式》:觀察者模式

驚卻一目發表於2020-07-03

正文

一、定義

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

要點:

  • 觀察者模式定義了物件之間一對多的關係。
  • 觀察者模式讓主題(可觀察者)和觀察者之間鬆耦合。
  • 主題物件管理某些資料,當主題內的資料改變時,會以某種形式通知觀察者。
  • 觀察者可以訂閱(註冊)主題,以便在主題資料改變時能收到更新。
  • 觀察者如果不想收到主題的更新通知,可以隨時取消訂閱(註冊)。

二、實現步驟

1、建立主題父類/介面

主題父類/介面主要提供了註冊觀察者、移除觀察者、通知觀察者三個方法。

/**
 * 主題
 */
public class Subject {
    
    /**
     * 觀察者列表
     */
    private ArrayList<Observer> observers;
    
    public Subject() {
        observers = new ArrayList<>();
    }

    /**
     * 註冊觀察者
     */
    public void registerObserver(Observer o) {
        observers.add(o);
    }

    /**
     * 移除觀察者
     */
    public void removeObserver(Observer o) {
        observers.remove(o);        
    }

    /**
     * 通知所有觀察者,並推送資料(也可以不推送資料,而是由觀察者過來拉取資料)
     */
    public void notifyObservers(Object data) {
        for (Observer o : observers) {
            o.update(data);
        }
    }
}

2、建立觀察者介面

觀察者介面主要提供了更新方法,以供主題通知觀察者時呼叫。

/**
 * 觀察者介面
 */
public interface Observer {

    /**
     * 根據主題推送的資料進行更新操作
     */
    public void update(Object data);
}

3、建立具體的主題,並繼承主題父類/實現主題介面

/**
 * 主題A
 */
public class SubjectA extends Subject {
    
    /**
     * 主題資料
     */
    private String data;

    public String getData() {
        return data;
    }

    public void setData(String data) {
        this.data = data;
        // 資料發生變化時,通知觀察者
        notifyObservers(data);
    }
}

4、建立具體的觀察者,並實現觀察者介面

通過觀察者類的建構函式,註冊成為主題的觀察者。

(1)觀察者 A

/**
 * 觀察者A
 */
public class ObserverImplA implements Observer {

    private Subject subject;
    
    public ObserverImplA(Subject subject) {
        // 儲存主題引用,以便後續取消註冊
        this.subject = subject;
        // 註冊觀察者
        subject.registerObserver(this);
    }
    
    @Override
    public void update(Object data) {
        System.out.println("Observer A:" + data.toString());
    }
}

(2)觀察者 B

/**
 * 觀察者B
 */
public class ObserverImplB implements Observer {

    private Subject subject;
    
    public ObserverImplB(Subject subject) {
        // 儲存主題引用,以便後續取消註冊
        this.subject = subject;
        // 註冊觀察者
        subject.registerObserver(this);
    }
    
    @Override
    public void update(Object data) {
        System.out.println("Observer B:" + data.toString());
    }
}

5、使用主題和觀察者物件

public class Test {
    
    public static void main(String[] args) {
        // 主題
        SubjectA subject = new SubjectA();
        // 觀察者A
        ObserverImplA observerA = new ObserverImplA(subject);
        // 觀察者B
        ObserverImplB observerB = new ObserverImplB(subject);
        // 模擬主題資料變化
        subject.setData("I'm Batman!!!");
        subject.setData("Why so serious...");
    }
}

三、舉個例子

1、背景

你的團隊剛剛贏得一紙合約,負責建立 Weather-O-Rama 公司的下一代氣象站——Internet 氣象觀測站。

該氣象站建立在 WeatherData 物件上,由 WeatherData 物件負責追蹤目前的天氣狀況(溫度、溼度、氣壓)。並且具有三種佈告板,分別顯示目前的狀況、氣象統計以及簡單的預報。當 WeatherData 物件獲得最新的測量資料時,三種佈告板必須實時更新。

並且,這是一個可擴充套件的氣象站,Weather-O-Rama 氣象站希望公佈一組 API,好讓其他開發人員可以寫出自己的氣象佈告板,並插入此應用中。

2、實現

(1)建立主題父類

/**
 * 主題
 */
public class Subject {

    /**
     * 觀察者列表
     */
    private ArrayList<Observer> observers;
    
    public Subject() {
        observers = new ArrayList<>();
    }
    
    /**
     * 註冊觀察者
     */
    public void registerObserver(Observer o) {
        observers.add(o);        
    }

    /**
     * 移除觀察者
     */
    public void removeObserver(Observer o) {
        observers.remove(o);        
    }

    /**
     * 通知所有觀察者,並推送資料
     */
    public void notifyObservers(float temperature, float humidity, float pressure) {
        for (Observer o : observers) {
            o.update(temperature, humidity, pressure);
        }
    }
}

(2)建立觀察者介面

/**
 * 觀察者介面
 */
public interface Observer {

    /**
     * 更新觀測值
     */
    public void update(float temperature, float humidity, float pressure);
}

(3)建立氣象資料類,並繼承主題父類

/**
 * 氣象資料
 */
public class WeatherData extends Subject {
    
    /**
     * 溫度
     */
    private float temperature;
    /**
     * 溼度
     */
    private float humidity;
    /**
     * 氣壓
     */
    private float pressure;
    
    public void measurementsChanged() {
        // 觀測值變化時,通知所有觀察者
        notifyObservers(temperature, humidity, pressure);
    }
    
    /**
     * 設定觀測值(模擬觀測值變化)
     */
    public void setMeasurements(float temperature, float humidity, float pressure) {
        this.temperature = temperature;
        this.humidity = humidity;
        this.pressure = pressure;
        measurementsChanged();
    }
}

(4)建立佈告板,並實現觀察者介面

/**
 * 目前狀態佈告板
 */
public class CurrentConditionsDisplay implements Observer {
    
    private Subject weatherData;
    private float temperature;
    private float humidity;
    
    public CurrentConditionsDisplay(Subject weatherData) {
        this.weatherData = weatherData;
        // 註冊觀察者
        weatherData.registerObserver(this);
    }

    @Override
    public void update(float temperature, float humidity, float pressure) {
        this.temperature = temperature;
        this.humidity = humidity;
        display();
    }
    
    public void display() {
        System.out.println("Current conditions:" + temperature + "F degress and " + humidity + "% humidity");
    }
}
/**
 * 統計佈告板
 */
public class StatisticsDisplay implements Observer {

    private Subject weatherData;
    private ArrayList<Float> historyTemperatures;
    
    public StatisticsDisplay(Subject weatherData) {
        this.weatherData = weatherData;
        // 註冊觀察者
        weatherData.registerObserver(this);
        historyTemperatures = new ArrayList<>();
    }

    @Override
    public void update(float temperature, float humidity, float pressure) {
        this.historyTemperatures.add(temperature);
        display();
    }
    
    public void display() {
        if (historyTemperatures.isEmpty()) {
            return;
        }
        Collections.sort(historyTemperatures);
        float avgTemperature = 0;
        float maxTemperature = historyTemperatures.get(historyTemperatures.size() - 1);
        float minTemperature = historyTemperatures.get(0);
        float totalTemperature = 0;
        for (Float temperature : historyTemperatures) {
            totalTemperature += temperature;
        }
        avgTemperature = totalTemperature / historyTemperatures.size();
        System.out.println("Avg/Max/Min temperature:" + avgTemperature + "/" + maxTemperature + "/" + minTemperature);
    }
}
/**
 * 預測佈告板
 */
public class ForecastDisplay implements Observer {

    private Subject weatherData;
    private float temperature;
    private float humidity;
    private float pressure;
    
    public ForecastDisplay(Subject weatherData) {
        this.weatherData = weatherData;
        // 註冊觀察者
        weatherData.registerObserver(this);
    }

    @Override
    public void update(float temperature, float humidity, float pressure) {
        this.temperature = temperature;
        this.humidity = humidity;
        this.pressure = pressure;
        display();
    }
    
    public void display() {
        System.out.println("Forecast:waiting for implementation...");
    }
}

(5)測試

public class Test {
    
    public static void main(String[] args) {
        // 氣象資料
        WeatherData weatherData = new WeatherData();
        // 目前狀態佈告板
        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);
    }
}

相關文章