基本介紹
觀察者模式(Observer Design Pattern)也被稱為發布訂閱模式(Publish-Subscribe Design Pattern)
意圖:當一個對象的狀態發生改變時,所有依賴於它的對象都會得到通知並被自動更新。
觀察者模式屬於行為型模式, 大多應用於一些事件驅動模型(Spring涉及)或者游戲開發領域。
假設有一家氣象局,姑且就叫神盾氣象局吧,該氣象局委托我們構建一套系統,這個系統有兩個公告牌,需要我們顯示當前的實時天氣和未來的天氣預報。 當神盾氣象局發布新的天氣數據(WeatherData)后,兩個公告牌上顯示的天氣數據務必實時更新。神盾氣象局同時要求我們保證程序擁有足夠的可擴展性,因為以后隨時要新增其他的公告牌(如緊急公告等)。
他們最初始的設計如下:
public class WeatherData {
//實例變量聲明
...
public void measurementsChanged() {
float temperature = getTemperature();
float humidity = getHumidity();
float pressure = getPressure();
List<Float> forecastTemperatures = getForecastTemperatures();
//更新公告牌
currentConditionsDisplay.update(temperature, humidity, pressure);
forecastDisplay.update(forecastTemperatures);
}
...
}
然鵝每當我需要刪除或者新增公告牌時,我就必須得修改 核心邏輯代碼,如此看來擴展性極差,違背了開閉原則(對擴展開放,對修改關閉)
從上述方案,我們已經大致了解該方案有很多問題
- 擴展性較差
- 代碼功能耦合嚴重
- 核心功能代碼改來改去容易出問題
觀察者模式
觀察者模式通常情況所解決的需求場景:A對象(觀察者)被 B對象(被觀察者)的某種變化高度敏感,需要在B變化的那一刻A及時得到反饋。
舉個例子:小明過馬路,小明需要在紅綠燈由紅燈變成綠燈后到達馬路另一側,這個場景中,小明是觀察者,紅綠燈是被觀察者,當紅綠燈發生顏色改變時,小明需要得到反饋。
但是程序中的觀察和現實中所說的【觀察】有些許差異:
- 觀察者不需要時刻盯着被觀察者(小明不需要每一秒盯着紅綠燈)
- 采用注冊或者訂閱(Subscribe)的方式告訴被觀察者(紅綠燈變色后會廣播,小明可以聽到)
采取這樣被動的觀察方式,省去了反復檢索狀態的資源消耗,也能得到最快的反饋速度,其實就是由拉變成了推。
觀察者模式通常基於 Subject(主題)和 Observer(觀察者)而設計,類圖如下
既然我們說了神盾氣象局最初的設計 多么的糟糕,那么我們現在就用觀察者模式來重構它吧!
首先我們基於上述的通用類圖,重新構建神盾氣象局的結構類圖以及編碼實現
主題接口
/**
* 主題
*/
public interface Subject {
void registerObserver(Observer observer);
void removeObserver(Observer observer);
void notifyObserver();
}
觀察者接口
/**
* 觀察者
*/
public interface Observer {
//反向更新
void update(WeatherDetail weatherDetail);
}
公告牌接口
public interface DisplayElement {
void display();
}
氣象數據Entity
@Data
@AllArgsConstructor
public class WeatherDetail {
private double temperature; //當前溫度
private double humidity; //當前濕度
private double pressure; //當前氣壓
private List<WeatherDetail> forecastDetails;//未來幾天的氣象數據詳情
}
氣象數據(被觀察者)- 核心
@Data
public class WeatherData implements Subject {
private List<Observer> observers;
private WeatherDetail weatherDetail;
public WeatherData() {
observers = new ArrayList<>();
}
@Override
public void registerObserver(Observer observer) {
observers.add(observer);
}
@Override
public void removeObserver(Observer observer) {
if (!observers.isEmpty()) {
observers.remove(observer);
}
}
@Override
public void notifyObserver() {
for (Observer observer : observers) {
observer.update(weatherDetail);
}
}
public void setMeasurements(WeatherDetail weatherDetail){
this.weatherDetail = weatherDetail;
notifyObserver();
}
}
顯示當前天氣的公告牌(CurrentConditionDisplay)
/**
* 當前展板
*/
public class CurrentConditionDisplay implements Observer, DisplayElement {
private double temperature; //當前溫度
private double humidity; //當前濕度
private double pressure; //當前氣壓
public CurrentConditionDisplay(Subject weatherData) {
//天氣數據
weatherData.registerObserver(this);
}
@Override
public void display() {
System.out.println("current-[temperature:" + temperature + ",humidity:" + humidity + ",pressure:" + pressure + "]");
}
@Override
public void update(WeatherDetail weatherDetail) {
this.temperature = weatherDetail.getTemperature();
this.humidity = weatherDetail.getHumidity();
this.pressure = weatherDetail.getPressure();
}
}
顯示未來幾天天氣的公告牌(ForecastConditionDisplay)
ppublic class ForecastConditionDisplay implements Observer, DisplayElement {
private List<WeatherDetail> forecastDetails;//未來幾天的氣象數據詳情
public ForecastConditionDisplay(Subject weatherData) {
weatherData.registerObserver(this);
}
@Override
public void display() {
if (forecastDetails != null) {
for (WeatherDetail weatherDetail : forecastDetails) {
System.out.println("forecast-[temperature:" + weatherDetail.getTemperature()
+ ",humidity" + weatherDetail.getHumidity() + ",pressure" + weatherDetail.getPressure() + "]");
}
}
}
@Override
public void update(WeatherDetail weatherDetail) {
forecastDetails = weatherDetail.getForecastDetails();
}
}
到這里,我們整個神盾氣象局的WeatherData應用就改造完成了。
兩個公告牌 CurrentConditionsDisplay
和 ForecastConditionDisplay
實現了 Observer
和 DisplayElement
接口。在他們的構造方法中會調用 WeatherData
的 registerObserver
方法將自己注冊成觀察者,這樣被觀察者 WeatherData
就會持有觀察者的應用,並將它們保存到一個集合中。當被觀察者 WeatherData
狀態發生變化時就會遍歷這個集合,循環調用觀察者更新公告牌數據的方法。后面如果我們需要增加或者刪除公告牌,就只需要新增或者刪除實現了 Observer
和 DisplayElement
接口的公告牌就好了。
好,我們接下來測試一下利用觀察者模式改進后的程序.....
public class Client {
public static void main(String[] args) {
List<WeatherDetail> forecastDetail = new ArrayList<>();
forecastDetail.add(new WeatherDetail(23.0, 0.9, 1.1, null));
forecastDetail.add(new WeatherDetail(17.0, 0.5, 1.3, null));
WeatherDetail weatherDetail = new WeatherDetail(22.0, 0.8, 1.2, forecastDetail);
WeatherData weatherData = new WeatherData();
DisplayElement current = new CurrentConditionDisplay(weatherData);
DisplayElement forecast = new ForecastConditionDisplay(weatherData);
weatherData.setMeasurements(weatherDetail);
current.display();
forecast.display();
}
}
輸出結果
觀察者模式(JDK版)
我們還是用神盾氣象局的例子來實現JDK版本的觀察者模式,這樣便於比較兩者之間的差別
公告牌接口
public interface DisplayElement {
void display();
}
氣象數據Entity
@Data
@AllArgsConstructor
public class WeatherDetail {
private double temperature; //當前溫度
private double humidity; //當前濕度
private double pressure; //當前氣壓
private List<WeatherDetail> forecastDetails;//未來幾天的氣象數據詳情
}
氣象數據(被觀察者)- 核心
/**
* 天氣數據
*/
@Data
public class WeatherData extends Observable {
private WeatherDetail weatherDetail;
public WeatherData() {}
public void setMeasurements(WeatherDetail weatherDetail){
this.weatherDetail = weatherDetail;
this.setChanged();
notifyObservers(weatherDetail);
}
}
顯示當前天氣的公告牌(CurrentConditionDisplay)
/**
* 當前展板
*/
public class CurrentConditionDisplay implements Observer, DisplayElement {
private double temperature; //當前溫度
private double humidity; //當前濕度
private double pressure; //當前氣壓
public CurrentConditionDisplay(Observable weatherData) {
//天氣數據
weatherData.addObserver(this);
}
@Override
public void display() {
System.out.println("current-[temperature:" + temperature + ",humidity:" + humidity + ",pressure:" + pressure + "]");
}
@Override
public void update(Observable observable, Object arg) {
WeatherDetail weatherDetail = (WeatherDetail) arg;
this.temperature = weatherDetail.getTemperature();
this.humidity = weatherDetail.getHumidity();
this.pressure = weatherDetail.getPressure();
}
}
顯示未來幾天天氣的公告牌(ForecastConditionDisplay)
public class ForecastConditionDisplay implements Observer, DisplayElement {
private List<WeatherDetail> forecastDetails;//未來幾天的氣象數據詳情
public ForecastConditionDisplay(Observable weatherData) {
//天氣數據
weatherData.addObserver(this);
}
@Override
public void display() {
if (forecastDetails != null) {
for (WeatherDetail weatherDetail : forecastDetails) {
System.out.println("forecast-[temperature:" + weatherDetail.getTemperature()
+ ",humidity:" + weatherDetail.getHumidity() + ",pressure:" + weatherDetail.getPressure() + "]");
}
}
}
@Override
public void update(Observable observable, Object arg) {
WeatherDetail weatherDetail = (WeatherDetail) arg;
this.forecastDetails = weatherDetail.getForecastDetails();
}
}
到這里,我們使用JDK自帶的API實現了觀察者模式,下面我們開始測試
public class Client {
public static void main(String[] args) {
List<WeatherDetail> forecastDetail = new ArrayList<>();
forecastDetail.add(new WeatherDetail(23.0, 0.9, 1.1,null));
forecastDetail.add(new WeatherDetail(17.0, 0.5, 1.3,null));
WeatherDetail weatherDetail = new WeatherDetail(22.0, 0.8, 1.2, forecastDetail);
WeatherData weatherData = new WeatherData();
DisplayElement current = new CurrentConditionDisplay(weatherData);
DisplayElement forecast = new ForecastConditionDisplay(weatherData);
weatherData.setMeasurements(weatherDetail);
current.display();
forecast.display();
}
}
輸出結果
總結:使用JDK自帶的API去實現觀察者模式固然方便,但是由於需要繼承 Observable
接口,會對被觀察者類造成限制(單繼承的局限性),其次 Observable
的代碼 從屬JDK1.0,底層還是用的相關的Vector去做安全的集合容器,個人感覺還是有點過時了,個人還是傾向於自實現觀察者模式。