《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 類來獲得可被觀察的行為的,這違背了設計原則的“多用組合,少用繼承”。