
轉(zhuǎn)載請(qǐng)注明出處:http://www.itdecent.cn/p/d55ee6e83d66
文章中的例子和思路均來(lái)自于《Head First》剛剛開(kāi)通了微信公眾號(hào):BaronTalk,之前專(zhuān)欄上的文章也陸續(xù)完成了搬遷。后續(xù)會(huì)持續(xù)保質(zhì)保量的輸出,還在等什么?!關(guān)注一波吧?。?! :-)
場(chǎng)景
我們接到一個(gè)來(lái)自氣象局的需求:氣象局需要我們構(gòu)建一套系統(tǒng),這系統(tǒng)有兩個(gè)公告牌,分別用于顯示當(dāng)前的實(shí)時(shí)天氣和未來(lái)幾天的天氣預(yù)報(bào)。當(dāng)氣象局發(fā)布新的天氣數(shù)據(jù)(WeatherData)后,兩個(gè)公告牌上顯示的天氣數(shù)據(jù)必須實(shí)時(shí)更新。氣象局同時(shí)要求我們保證程序擁有足夠的可擴(kuò)展性,因?yàn)楹笃陔S時(shí)可能要新增新的公告牌。
概況
這套系統(tǒng)中主要包括三個(gè)部分:氣象站(獲取天氣數(shù)據(jù)的物理設(shè)備)、WeatherData(追蹤來(lái)自氣象站的數(shù)據(jù),并更新公告牌)、公告牌(用于展示天氣數(shù)據(jù))

WeatherData知道如何跟氣象站聯(lián)系,以獲得天氣數(shù)據(jù)。當(dāng)天氣數(shù)據(jù)有更新時(shí),WeatherData會(huì)更新兩個(gè)公告牌用于展示新的天氣數(shù)據(jù)。
錯(cuò)誤示范
我們先來(lái)看看隔壁老王的實(shí)現(xiàn):
public class WeatherData {
//實(shí)例變量聲明
...
public void measurementsChanged() {
float temperature = getTemperature();
float humidity = getHumidity();
float pressure = getPressure();
List<Float> forecastTemperatures = getForecastTemperatures();
//更新公告牌
currentConditionsDisplay.update(temperature, humidity, pressure);
forecastDisplay.update(forecastTemperatures);
}
...
}
上面這段代碼是典型的針對(duì)實(shí)現(xiàn)編程,這會(huì)導(dǎo)致我們以后增加或刪除公告牌時(shí)必須修改程序。我們現(xiàn)在來(lái)看看觀(guān)察者模式,然后再回來(lái)看看如何將觀(guān)察者模式應(yīng)用到這個(gè)程序。
觀(guān)察者模式介紹
觀(guān)察者模式面向的需求是:A對(duì)象(觀(guān)察者)對(duì)B對(duì)象(被觀(guān)察者)的某種變化高度敏感,需要在B變化的一瞬間做出反應(yīng)。舉個(gè)例子,新聞里喜聞樂(lè)見(jiàn)的警察抓小偷,警察需要在小偷伸手作案的時(shí)候?qū)嵤┳ゲ?。在這個(gè)例子里,警察是觀(guān)察者、小偷是被觀(guān)察者,警察需要時(shí)刻盯著小偷的一舉一動(dòng),才能保證不會(huì)錯(cuò)過(guò)任何瞬間。程序里的觀(guān)察者和這種真正的【觀(guān)察】略有不同,觀(guān)察者不需要時(shí)刻盯著被觀(guān)察者(例如A不需要每隔1ms就檢查一次B的狀態(tài)),二是采用注冊(cè)(Register)或者成為訂閱(Subscribe)的方式告訴被觀(guān)察者:我需要你的某某狀態(tài),你要在它變化時(shí)通知我。采取這樣被動(dòng)的觀(guān)察方式,既省去了反復(fù)檢索狀態(tài)的資源消耗,也能夠得到最高的反饋速度。
觀(guān)察者模式通常基于Subject和Observer接口類(lèi)來(lái)設(shè)計(jì),下面是是類(lèi)圖:

觀(guān)察者模式的應(yīng)用
結(jié)合上面的類(lèi)圖,我們現(xiàn)在將觀(guān)察者模式應(yīng)用到WeatherData項(xiàng)目中來(lái)。于是有了下面這張類(lèi)圖:

主題接口
/**
* 主題(發(fā)布者、被觀(guān)察者)
*/
public interface Subject {
/**
* 注冊(cè)觀(guān)察者
*/
void registerObserver(Observer observer);
/**
* 移除觀(guān)察者
*/
void removeObserver(Observer observer);
/**
* 通知觀(guān)察者
*/
void notifyObservers();
}
觀(guān)察者接口
/**
* 觀(guān)察者
*/
public interface Observer {
void update();
}
公告牌用于顯示的公共接口
public interface DisplayElement {
void display();
}
下面我們?cè)賮?lái)看看WeatherData是如何實(shí)現(xiàn)的
public class WeatherData implements Subject {
private List<Observer> observers;
private float temperature;//溫度
private float humidity;//濕度
private float pressure;//氣壓
private List<Float> forecastTemperatures;//未來(lái)幾天的溫度
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;
}
}
顯示當(dāng)前天氣的公告牌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("當(dāng)前溫度為:" + this.temperature + "℃");
System.out.println("當(dāng)前濕度為:" + this.humidity);
System.out.println("當(dāng)前氣壓為:" + this.pressure);
}
@Override
public void update() {
this.temperature = this.weatherData.getTemperature();
this.humidity = this.weatherData.getHumidity();
this.pressure = this.weatherData.getPressure();
display();
}
}
顯示未來(lái)幾天天氣的公告牌ForecastDisplay
public class ForecastDisplay implements Observer, DisplayElement {
private WeatherData weatherData;
private List<Float> forecastTemperatures;//未來(lái)幾天的溫度
public ForecastDisplay(WeatherData weatherData) {
this.weatherData = weatherData;
this.weatherData.registerObserver(this);
}
@Override
public void display() {
System.out.println("未來(lái)幾天的氣溫");
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();
}
}
到這里,我們整個(gè)氣象局的WeatherData應(yīng)用就改造完成了。兩個(gè)公告牌CurrentConditionsDisplay和ForecastDisplay實(shí)現(xiàn)了Observer和DisplayElement接口,在他們的構(gòu)造方法中會(huì)調(diào)用WeatherData的registerObserver方法將自己注冊(cè)成觀(guān)察者,這樣被觀(guān)察者WeatherData就會(huì)持有觀(guān)察者的應(yīng)用,并將它們保存到一個(gè)集合中。當(dāng)被觀(guān)察者``WeatherData狀態(tài)發(fā)送變化時(shí)就會(huì)遍歷這個(gè)集合,循環(huán)調(diào)用觀(guān)察者公告牌更新數(shù)據(jù)的方法。后面如果我們需要增加或者刪除公告牌就只需要新增或者刪除實(shí)現(xiàn)了Observer和DisplayElement`接口的公告牌就好了。
觀(guān)察者模式將觀(guān)察者和主題(被觀(guān)察者)徹底解耦,主題只知道觀(guān)察者實(shí)現(xiàn)了某一接口(也就是Observer接口)。并不需要觀(guān)察者的具體類(lèi)是誰(shuí)、做了些什么或者其他任何細(xì)節(jié)。任何時(shí)候我們都可以增加新的觀(guān)察者。因?yàn)橹黝}唯一依賴(lài)的東西是一個(gè)實(shí)現(xiàn)了Observer接口的對(duì)象列表。
好了,我們測(cè)試下利用觀(guān)察者模式重構(gòu)后的程序:
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);
}
}
輸出結(jié)果:
當(dāng)前溫度為:22.0℃
當(dāng)前濕度為:0.8
當(dāng)前氣壓為:1.2
未來(lái)幾天的氣溫
第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
- 模塊化示例項(xiàng)目 ModularizationProject 源碼地址:https://github.com/BaronZ88/ModularizationProject
- 路由框架 Router 源碼地址:https://github.com/BaronZ88/Router
如果你喜歡我的文章,就關(guān)注下我的公眾號(hào) BaronTalk 、 知乎專(zhuān)欄 或者在 GitHub 上添個(gè) Star 吧!
- 微信公眾號(hào):BaronTalk
- 知乎專(zhuān)欄:https://zhuanlan.zhihu.com/baron
- GitHub:https://github.com/BaronZ88
- 個(gè)人博客:http://baronzhang.com
