場景
我們接到一個來自氣象局的需求:氣象局需要我們建構一套系統,這系統有兩個公告牌,分别用于顯示目前的實時天氣和未來幾天的天氣預報。當氣象局釋出新的天氣資料(WeatherData)後,兩個公告牌上顯示的天氣資料必須實時更新。氣象局同時要求我們保證程式擁有足夠的可擴充性,因為後期随時可能要新增新的公告牌。
概況
這套系統中主要包括三個部分:氣象站(擷取天氣資料的實體裝置)、WeatherData(追蹤來自氣象站的資料,并更新公告牌)、公告牌(用于展示天氣資料)
WeatherData知道如何跟氣象站聯系,以獲得天氣資料。當天氣資料有更新時,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不需要每隔1ms就檢查一次B的狀态),二是采用注冊(Register)或者成為訂閱(Subscribe)的方式告訴被觀察者:我需要你的某某狀态,你要在它變化時通知我。采取這樣被動的觀察方式,既省去了反複檢索狀态的資源消耗,也能夠得到最高的回報速度。
觀察者模式通常基于Subject和Observer接口類來設計,下面是是類圖:
觀察者模式的應用
結合上面的類圖,我們現在将觀察者模式應用到WeatherData項目中來。于是有了下面這張類圖:
主題接口
/**
* 主題(釋出者、被觀察者)
*/
public interface Subject {
/**
* 注冊觀察者
*/
void registerObserver(Observer observer);
/**
* 移除觀察者
*/
void removeObserver(Observer observer);
/**
* 通知觀察者
*/
void notifyObservers();
}
複制
觀察者接口
/**
* 觀察者
*/
public interface Observer {
void update();
}
複制
公告牌用于顯示的公共接口
public interface DisplayElement {
void display();
}
複制
下面我們再來看看WeatherData是如何實作的
public class WeatherData implements Subject {
private List<Observer> observers;
private float temperature;//溫度
private float humidity;//濕度
private float pressure;//氣壓
private List<Float> forecastTemperatures;//未來幾天的溫度
public WeatherData() {
this.observers = new ArrayList<Observer>();
}
@Override
public void registerObserver(Observer observer) {
this.observers.add(observer);
}
@Override
public void removeObserver(Observer observer) {
this.observers.remove(observer);
}
@Override
public void notifyObservers() {
for (Observer observer : observers) {
observer.update();
}
}
public void measurementsChanged() {
notifyObservers();
}
public void setMeasurements(float temperature, float humidity,
float pressure, List<Float> forecastTemperatures) {
this.temperature = temperature;
this.humidity = humidity;
this.pressure = pressure;
this.forecastTemperatures = forecastTemperatures;
measurementsChanged();
}
public float getTemperature() {
return temperature;
}
public float getHumidity() {
return humidity;
}
public float getPressure() {
return pressure;
}
public List<Float> getForecastTemperatures() {
return forecastTemperatures;
}
}
複制
顯示目前天氣的公告牌CurrentConditionsDisplay
public class CurrentConditionsDisplay implements Observer, DisplayElement {
private WeatherData weatherData;
private float temperature;//溫度
private float humidity;//濕度
private float pressure;//氣壓
public CurrentConditionsDisplay(WeatherData weatherData) {
this.weatherData = weatherData;
this.weatherData.registerObserver(this);
}
@Override
public void display() {
System.out.println("目前溫度為:" + this.temperature + "℃");
System.out.println("目前濕度為:" + this.humidity);
System.out.println("目前氣壓為:" + this.pressure);
}
@Override
public void update() {
this.temperature = this.weatherData.getTemperature();
this.humidity = this.weatherData.getHumidity();
this.pressure = this.weatherData.getPressure();
display();
}
}
複制
顯示未來幾天天氣的公告牌ForecastDisplay
public class ForecastDisplay implements Observer, DisplayElement {
private WeatherData weatherData;
private List<Float> forecastTemperatures;//未來幾天的溫度
public ForecastDisplay(WeatherData weatherData) {
this.weatherData = weatherData;
this.weatherData.registerObserver(this);
}
@Override
public void display() {
System.out.println("未來幾天的氣溫");
int count = forecastTemperatures.size();
for (int i = 0; i < count; i++) {
System.out.println("第" + i + "天:" + forecastTemperatures.get(i) + "℃");
}
}
@Override
public void update() {
this.forecastTemperatures = this.weatherData.getForecastTemperatures();
display();
}
}
複制
到這裡,我們整個氣象局的WeatherData應用就改造完成了。兩個公告牌
CurrentConditionsDisplay
和
ForecastDisplay
實作了
Observer
和
DisplayElement
接口,在他們的構造方法中會調用
WeatherData
的
registerObserver
方法将自己注冊成觀察者,這樣被觀察者
WeatherData
就會持有觀察者的應用,并将它們儲存到一個集合中。當被觀察者
`WeatherData
狀态發送變化時就會周遊這個集合,循環調用觀察者
公告牌
更新資料的方法。後面如果我們需要增加或者删除公告牌就隻需要新增或者删除實作了
Observer
和
DisplayElement
接口的公告牌就好了。
觀察者模式将觀察者和主題(被觀察者)徹底解耦,主題隻知道觀察者實作了某一接口(也就是Observer接口)。并不需要觀察者的具體類是誰、做了些什麼或者其他任何細節。任何時候我們都可以增加新的觀察者。因為主題唯一依賴的東西是一個實作了
Observer
接口的對象清單。
好了,我們測試下利用觀察者模式重構後的程式:
public class ObserverPatternTest {
public static void main(String[] args) {
WeatherData weatherData = new WeatherData();
CurrentConditionsDisplay currentConditionsDisplay = new CurrentConditionsDisplay(weatherData);
ForecastDisplay forecastDisplay = new ForecastDisplay(weatherData);
List<Float> forecastTemperatures = new ArrayList<Float>();
forecastTemperatures.add(22f);
forecastTemperatures.add(-1f);
forecastTemperatures.add(9f);
forecastTemperatures.add(23f);
forecastTemperatures.add(27f);
forecastTemperatures.add(30f);
forecastTemperatures.add(10f);
weatherData.setMeasurements(22f, 0.8f, 1.2f, forecastTemperatures);
}
}
複制
輸出結果:
目前溫度為:22.0℃
目前濕度為:0.8
目前氣壓為:1.2
未來幾天的氣溫
第0天:22.0℃
第1天:-1.0℃
第2天:9.0℃
第3天:23.0℃
第4天:27.0℃
第5天:30.0℃
第6天:10.0℃
複制
源碼位址:https://github.com/BaronZ88/DesignPatterns/tree/master/src/com/baron/patterns/observer
如果大家喜歡這一系列的文章,歡迎關注我的知乎專欄和GitHub。
- 知乎專欄:https://zhuanlan.zhihu.com/baron
- GitHub:https://github.com/BaronZ88