Head First設計模式之觀察者模式(Observer Pattern)
前言:
這一節開始學習觀察者模式,開始講之前會先像第一節那樣通過一個應用場景來引入該模式。具體場景為:氣象站提供了一個WeatherData物件,該物件可以追蹤獲取天氣的溫度、氣壓、溼度資訊,WeatherData物件會隨即更新三個佈告板的顯示:目前狀況(溫度、溼度、氣壓)、氣象統計和天氣預報。
1. 基本需求:利用WeatherData物件獲取資料、並更新三個佈告板:目前狀況、氣象統計和天氣預報
WeatherData類圖如下:
說明:
GetTemperature()、GetHumidity()、GetPressure()分別用來獲取天氣溫度、溼度氣壓,MeasurementsChanged()當氣象測量更新此方法會被呼叫。
分析:
1. 可以通過WeatherData的Getter方法獲取三個測量值:溫度、溼度、氣壓
2. 氣象測量更新時會呼叫MeasurementsChanged()方法
3. 需要實現三個天氣資料佈告板:“目前狀況”、“氣象統計”、“天氣預報”,一旦WeatherData測到新值,這些佈告也馬上更新
4. 此係統可擴充套件,可以隨意的新增或者刪除任何佈告板。
實現:
public class WeatherData { public void MeasurementsChanged() { float temp = GetTemperature(); float humidity = GetHumidity(); float pressure = GetPressure(); currentConditionDisplay.update(temp,humidity, pressure); statisticsDisplay.update(temp,humidity, pressure); forecastDisplay.update(temp, humidity,pressure); } //其他方法 }
反思:
我們的這種實現有何不妥?
結合我們第一節中的一些面向物件的原則,這種實現方式會有如下問題:
l 針對具體實現程式設計,後續新增或者刪除佈告板必須修改程式
l 未將改變的地方封裝起來
這種實現方式與面向物件的一些基本原則是相違背的,目前暫時是實現了使用者的需求,但是在後續不具備可擴充套件行。
2. 引入觀察者模式
2.1 出版者+訂閱者=觀察者模式
出版者:就相當於“主題”(Subject),訂閱者相當於“觀察者”(Observer)
當出版者(主題)發行新的報紙的時候,所有的觀察者(訂閱者)就可以收到最新的報紙,同時,當新的觀察者(訂閱者)加入時,也可以收到最新的報紙,當觀察者(訂閱者)退訂報紙後,就再也收不到新的報紙。
2.2 定義觀察者模式
觀察者模式定義了物件之間一對多依賴,這樣一來,當一個物件狀態改變時,它的所有依賴者都會收到通知並自動更新。
主題和觀察者定義一對多的關係。觀察者依賴於此主題,只要主題狀態一有變化,觀察者就會被通知,根據通知的風格,觀察者可能因此新值而更新。
觀察者模式:類圖
2.3設計原則:為了互動物件之間的鬆耦合設計而努力
鬆耦合的威力
l 當兩個物件之間鬆耦合,它們依然可以互動,但是不清楚彼此的細節。
l 觀察者模式提供了一種物件設計,讓主題和觀察者之間鬆耦合。
說明:
1. 主題只知道觀察者實現了某個介面(IObserver介面),不需要知道觀察者是誰,或其他細節。
2. 任何時候都可以增加或者刪除的觀察者,主題唯一依賴的是一個實現了IObserver介面的物件列表。
3. 新的型別觀察者出現時,主題程式碼不需要修改,只需要在新型別裡實現觀察者介面,然後註冊為觀察者即可。
4. 可以獨立的複用主題或觀察者,因為二者鬆耦合。
5. 改變主題或者觀察者,並不會影響另一方。因為二者鬆耦合。
鬆耦合的設計之所以能讓我們建立有彈性的OO系統,能夠應對變化,是因為物件之間的相互依賴講到了最低。
3. 利用觀察者模式設計並實現氣象站
3.1 設計氣象站
類圖:
3.2具體實現
3.2.1主題、觀察者、顯示介面
/// Description:物件、觀察者、顯示介面
/// </summary>
public interface ISubject
{
void RegisterObserver(IObserver o);//註冊觀察者
void RemoveObserver(IObserver o);//刪除觀察者
void NotifyObervers();//通知觀察者
}
public interface IObserver
{
void Update(float temp, float humidity,float pressure);
}
public interface IDisplayElement
{
void Display();
}
3.2.2 WeatherData類:註冊、刪除、通知觀察者
/// Description:WeatherData 註冊、刪除、通知觀察者
/// </summary>
public class WeatherData:ISubject
{
private ArrayList observers;
private float temperature;
private float humidity;
private float pressure;
public WeatherData()
{
observers = new ArrayList();//初始化obervers,用來儲存註冊的觀察者
}
/// <summary>
/// 註冊觀察者
/// </summary>
/// <paramname="o"></param>
public void RegisterObserver(IObservero)
{
observers.Add(o);
}
/// <summary>
/// 刪除觀察者
/// </summary>
/// <paramname="o"></param>
public void RemoveObserver(IObserver o)
{
int i = observers.IndexOf(o);
if (i >= 0)
observers.Remove(o);
}
/// <summary>
/// 通知觀察者
/// </summary>
public void NotifyObervers()
{
foreach (IObserver o in observers)
{
o.Update(temperature, humidity,pressure);
}
}
/// <summary>
/// 當從氣象站得到更新觀測值時,通知觀察者
/// </summary>
public void MeasurementsChanged()
{
NotifyObervers();
}
/// <summary>
///
/// </summary>
/// <paramname="temperature"></param>
/// <paramname="humidity"></param>
/// <paramname="pressure"></param>
public void SetMeasurements(floattemperature, float humidity, float pressure)
{
this.temperature = temperature;
this.humidity = humidity;
this.pressure = pressure;
MeasurementsChanged();
}
}
3.2.3 佈告板類,實現了IObserver、IDisplayElement介面
/// Description:建立佈告板
/// </summary>
public classCurrentConditionsDisplay:IObserver,IDisplayElement
{
private float temperature;
private float humidity;
private ISubject weatherData;
public CurrentConditionsDisplay(ISubject weatherData)
{
this.weatherData = weatherData;
weatherData.RegisterObserver(this);
}
public void Update(float temperature,float humidity, float pressure)
{
this.temperature = temperature;
this.humidity = humidity;
Display();
}
public void Display()
{
Console.WriteLine("Currentcoditions: " + temperature + "F degress and " + humidity +"% humidity");
}
}
3.2.4 測試
WeatherStation.WeatherDataweatherData = new WeatherStation.WeatherData();
WeatherStation.CurrentConditionsDisplaycurrentDisplay = new WeatherStation.CurrentConditionsDisplay(weatherData);
weatherData.SetMeasurements(10,20, 30);
結果如下:
4. Java內建的觀察者模式
Java內建的觀察者模式,許多功能都已經事先準備好了,甚至可以用推(push)或拉(pull)的方式傳送資料。
使用java內建觀察者模式實現氣象站的OO設計類圖,如下:
Java內建觀察者模式與我們在3小節中明顯的差異是WeatherData繼承自Observable類,並集成了一些增加、刪除、通知觀察者的方法。
l 將物件變成觀察者
首先還是要實現Observer(觀察者)介面,其次呼叫Observable物件的addObserver()方法即可。
l 可觀察者(主題)送出通知
1. 先呼叫setChanged()方法,標記狀態已經改變的事實。
2. 呼叫notifyObservers()方法(該方法有2個,任意一個皆可):notifyObservers()或notifyObservers(Object arg)
l 觀察者接收通知
觀察者實現了Observer的介面,方法簽名如下:
update(Observable o,Object arg)
第一個引數為主題本身,讓觀察者知道是哪個主題通知它的
第二個引數是傳入notifyObservers()的資料物件
如果想用“推”的方式將資料給觀察者,則可以把資料當做資料物件的方式傳給notifyObservers(arg)
如果想用“拉”的方式將資料給觀察者,則需要在update()中,通過WeatherData的getTemperature()等方法獲取對應的氣象值。
缺陷:
Java內建的觀察者模式中Observable是一個類,你必須設計一個類去繼承它。如果某類相同時具有Observable類和另一個超類的行為,就無法實現,因為java不支援多繼承。
同時也限制了Observable的複用能力。
同時,Observable API將setChanged()方法保護了起來,除非繼承自Observable類,否則無法建立Observable例項組合到自己的物件中,也違背了面向物件設計的第二個原則:多用組合,少用繼承。
5. 總結
l OO原則:
封裝變化
多用組合,少用繼承
針對介面程式設計,不針對實現程式設計
對互動物件之間的鬆耦合設計而努力(新的OO原則,鬆耦合的設計更有彈性,更能應對變化)
l OO模式:
觀察者模式—在物件之間定義一對多的依賴,這樣一來,當一個物件改變狀態,依賴它的物件都會收到通知,並自動更新。