現在我們要為一家氣象站開發一套氣象監控系統,按照客戶的要求,這個監控系統必須可以實時跟蹤目前的天氣狀況(溫度、濕度、大氣壓力),并且可以在三種不同裝置上顯示出來(目前天氣狀況、天氣統計、天氣預測)。客戶還希望這個系統可以對外提供一個API接口,以便任何開發者都可以開發自己的顯示裝置,然後無縫挂接到系統中,系統可以統一更新所有顯示裝置的資料。客戶還會提供一個可以通路氣象站的硬體裝置的元件,如下圖所示:
它提供了三個方法(get開頭),可以分别取得實時的溫度、濕度和大氣壓力,還有一個MeasurementsChanged()方法,當任何天氣狀況發生變化的時候,這個方法都會自動被觸發,目前這個方法隻是一個空函數,擴充的代碼還需要我們自己去擴充。至于WeatherData是如何取得天氣狀況的,還有MeasurementsChanged()方法是如何被自動觸發的這些事情都不需要我們去考慮,我們隻管考慮如果做好跟顯示裝置有關的事情就好了。
OK!讓我們來考慮一下這個系統的實作,先重新理一下思路:
1. 客戶提供了擷取實時的天氣狀況的方法。
2. MeasurementsChanged()方法會在天氣狀況變化時被自動調用。
3. 系統要實作三種顯示模式,分别顯示天氣狀況、天氣統計和天氣預測,而且這些顯示的資訊必須跟目前最新的天氣狀況實時同步。
4. 系統還必須支援在顯示方式上的擴充性,而且使用者可以任意添加和移除不同的顯示模式。
基于上面這些資訊,我們大概都會想到可以象下面這樣來實作這個系統:
//僞代碼
public class WeatherData
{
//執行個體化顯示裝置(省略)
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); //同步顯示天氣預報資訊
}
}
因為客戶已經給我們提供了實時的資料,還提供了資料更新時候的觸發機制,那麼我們要做的就是把最新的資料提供給不同的顯示裝置就OK了,上面的代碼好象已經可以基本解決問題啦。哈哈!
真的就這麼簡單就搞定了嗎?讓我們用上一篇【政策模式】裡學習到的原則來審視一下這個實作。首先,xxxDisplay 這幾個對象都是具體的類執行個體,也就是說我們在這裡違背了“面向接口程式設計,而不要面向實作程式設計。”的原則,這樣實作會帶來的問題是系統無法滿足在不修改代碼的情況下動态添加或移除不同的顯示裝置。換句話說,顯示裝置相關的部分是系統中最不穩定的部分,應該将其單獨隔離開,也就是前面學過的另一個原則:“找到系統中變化的部分,将變化的部分同其它穩定的部分隔開。”那麼我們到底該怎麼辦呢?呵呵,既然這篇文章是講觀察者模式的,當然要用它來結束戰鬥!下面我們先來認識一下觀察者模式~
我們還是先看一下官方的定義:
The Observer Pattern defines a one-to-many dependency between objects so that when one object changes state, all of its dependents are notified and updated automatically. (觀察者模式定義了對象間的一種一對多依賴關系,使得每當一個對象改變狀态,則所有依賴于它的對象都會得到通知并被自動更新)
咋樣?這是超級經典的标準定義,如假抱換的!不懂?那再看看下面的類圖吧~
Subject(被觀察的對象接口)
l 規定ConcreteSubject的統一接口;
l 每個Subject可以有多個Observer;
ConcreteSubject(具體被觀察對象)
l 維護對所有具體觀察者的引用的清單;
l 狀态發生變化時會發送通知給所有注冊的觀察者。
Observer(觀察者接口)
l 規定ConcreteObserver的統一接口;
l 定義了一個update()方法,在被觀察對象狀态改變時會被調用。
ConcreteObserver(具體觀察者)
l 維護一個對ConcreteSubject的引用;
l 特定狀态與ConcreteSubject同步;
l 實作Observer接口,通過update()方法接收ConcreteSubject的通知。
怎麼樣,現在總該有點感覺了吧?下面還有一個順序圖,再體會體會~
呵呵!還沒想明白,為什麼官方的東西總是看不懂,看來是沒當官的命啦!其實觀察者模式十分簡單,現實生活中的例子更是随處可見,就比如看電視:某個觀衆就是一個标準的ConcreteObserver(具體觀察者,都符合統一的Observer接口,即都要通過電視收看節目的觀衆),電視節目就是Subject(被觀察對象接口,這裡展現為無線電視信号)了,不同的頻道的節目是不同的ConcreteSubject(不同頻道有不同的節目),觀衆可以自由決定看電視(registerObserver)或不看電視(removeObserver),而電視節目的變化也會在自動更新(notifyObservers)所有觀衆的收看内容。怎麼樣?這回明白了吧!
另外觀察者模式也叫釋出-訂閱模式(Publishers + Subscribers = Observer Pattern),跟看電視一樣,訂閱報紙也是一個很直覺的例子,有人釋出(Publish = Subject)報紙,有人訂閱(Subscribe = Observer)報紙,訂閱的人可以定期收到最新釋出的報紙,訂閱人也可以随時退訂。
現在大家應該對觀察者模式基本都了解了,我們來用這個模式來解決氣象站哪個問題。就氣象站問題的應用場景來說,WeatherData可以作為ConcreteSubject來看待,而不同的顯示裝置則可以作為ConcreteObserver來看待,也就是說顯示裝置觀察WeatherData對象,如果WeatherData對象有任何狀态變化,則立刻更新顯示裝置的資料資訊。這麼說似乎很靠譜了,下面我們再來具體實作一下吧,先從整體結構開始,如下類圖:
跟前面說的實作方式完全一樣,隻是這裡為所有顯示裝置又定義了一個統一的接口,這個接口裡定義了一個display()方法,也就是說未來所有實作Observer和DisplayElement接口的對象應該都可以作為氣象監控系統的終端顯示裝置,不同使用者可以在display()方法裡任意自定義自己的顯示模式。因為為了防止混亂,圖4中隻畫了一個具體顯示裝置對象,即CurrentConditionsDisplay,跟它同級别的還有StatisticsDisplay和ForcastDisplay,它們在結構上完全相同。下面我們通過具體的代碼再進一步了解一下基于觀察者模式的氣象監控系統的實作。
ISubject:
1using System;
2
3namespace DesignPatterns.Observer.WeatherData
4{
5 public interface ISubject
6 {
7 void RegisterObserver(IObserver o);
8 void RemoveObserver(IObserver o);
9 void NotifyObserver();
10 }
11}
12
關于這段代碼,似乎沒什麼好說的了,因為上面已經反複說了很多啦。
IObserver:
5 public interface IObserver
7 void Update(float temperature, float humidity, float pressure);
8 }
9}
10
這裡我們給update()方法定義了三個對應不同氣象資料的參數。
IDisplayElement:
5 public interface IDisplayElement
7 object Display();
這個類也是超級簡單,沒什麼可解釋的。
WeatherData:
2using System.Collections;
3
4namespace DesignPatterns.Observer.WeatherData
5{
6 public class WeatherData : ISubject
7 {
8 private ArrayList observers;
9 private float temperature;
10 private float humidity;
11 private float pressure;
13 public WeatherData()
14 {
15 observers = new ArrayList();
16 }
17
18 ISubject Members#region ISubject Members
19
20 public void RegisterObserver(IObserver o)
21 {
22 observers.Add(o);
23 }
24
25 public void RemoveObserver(IObserver o)
26 {
27 int i = observers.IndexOf(o);
28 if(i >= 0)
29 {
30 observers.Remove(o);
31 }
32 }
33
34 public void NotifyObserver()
35 {
36 foreach(IObserver observer in observers)
37 {
38 observer.Update(temperature,humidity,pressure);
39 }
40 }
41
42 #endregion
43
44 public void MeasurementsChanged()
45 {
46 NotifyObserver();
47 }
48
49 public void SetMeasurements(float temperature, float humidity,
50 float pressure)
51 {
52 this.temperature = temperature;
53 this.humidity = humidity;
54 this.pressure = pressure;
55 MeasurementsChanged();
56 }
57 }
58}
59
這個類是ISubject的具體實作,内部使用ArrayList來記錄所有注冊的觀察者,SetMeasurements() 方法是用來模拟前面提到的在天氣狀況改變的時候自動觸發MeasurementsChanged()方法的機制。
CurrentConditionsDisplay:
5 public class CurrentConditionsDisplay : IObserver, IDisplayElement
7 private float temperature;
8 private float humidity;
9 private float pressure;
10 private ISubject weatherData;
11
12 public CurrentConditionsDisplay(ISubject weatherData)
13 {
14 this.weatherData = weatherData;
15 weatherData.RegisterObserver(this);
18 IObserver Members#region IObserver Members
20 public void Update(float temperature, float humidity, float pressure)
22 this.temperature = temperature;
23 this.humidity = humidity;
24 this.pressure = pressure;
25 }
26
27 #endregion
28
29 IDisplayElement Members#region IDisplayElement Members
30
31 public object Display()
32 {
33 return "Current conditions: " + temperature +
34 "F degrees and " + humidity + "% humidity";
35 }
36
37 #endregion
38 }
39}
40
這個類是IObserver和IDisplayElement的具體實作,代表顯示目前天氣狀況的具體顯示裝置對象,其内部維護了一個ISubject類型的變量,該變量在CurrentConditionsDisplay的構造函數中被初始化,同時調用ISubject.registerObserver()方法,實作訂閱ISubject。
StatisticsDisplay和ForcastDisplay:
2using System.Text;
6 public class StatisticsDisplay : IObserver, IDisplayElement
8 Members#region Members
9 private float maxTemp = 0.0f;
10 private float minTemp = 200;
11 private float temperatureSum = 0.0f;
12 private int numReadings = 0;
13 private ISubject weatherData;
14 #endregion//Members
15
16 NumberOfReadings Property#region NumberOfReadings Property
17 public int NumberOfReadings
18 {
19 get
20 {
21 return numReadings;
22 }
24 #endregion//NumberOfReadings Property
25
26 Constructor#region Constructor
27 public StatisticsDisplay(ISubject weatherData)
28 {
29 this.weatherData = weatherData;
30 weatherData.RegisterObserver(this);
31 }
32 #endregion//Constructor
34 IObserver Members#region IObserver Members
35
36 public void Update(float temperature, float humidity, float pressure)
37 {
38 temperatureSum += temperature;
39 numReadings++;
41 if (temperature > maxTemp)
42 {
43 maxTemp = temperature;
44 }
45
46 if (temperature < minTemp)
47 {
48 minTemp = temperature;
49 }
50 }
51
52 #endregion
53
54 IDisplayElement Members#region IDisplayElement Members
55
56 public object Display()
57 {
58 return "Avg/Max/Min temperature = " + RoundFloatToString(temperatureSum / numReadings) +
59 "F/" + maxTemp + "F/" + minTemp + "F";
60 }
61
62 #endregion
63
64 RoundFloatToString#region RoundFloatToString
65 public static string RoundFloatToString(float floatToRound)
66 {
67 System.Globalization.CultureInfo cultureInfo = new System.Globalization.CultureInfo("en-US");
68 cultureInfo.NumberFormat.CurrencyDecimalDigits = 2;
69 cultureInfo.NumberFormat.CurrencyDecimalSeparator = ".";
70 return floatToRound.ToString("F",cultureInfo);
71 }
72 #endregion//RoundFloatToString
73
74 }
75}
76
6 public class ForcastDisplay : IObserver, IDisplayElement
8 private float currentPressure = 29.92f;
9 private float lastPressure;
12 public ForcastDisplay(ISubject weatherData)
22 lastPressure = currentPressure;
23 currentPressure = pressure;
24 }
26 #endregion
27
28 IDisplayElement Members#region IDisplayElement Members
29
30 public object Display()
31 {
32 StringBuilder sb = new StringBuilder();
34 sb.Append("Forecast: ");
35
36 if(currentPressure > lastPressure)
38 sb.Append("Improving weather on the way!");
40 else if (currentPressure == lastPressure)
41 {
42 sb.Append("More of the same");
43 }
44 else if (currentPressure < lastPressure)
45 {
46 sb.Append("Watch out for cooler, rainy weather");
47 }
48 return sb.ToString();
49 }
50
51 #endregion
52 }
53}
54
這兩個類跟CurrentConditionsDisplay基本結構相同,隻是update()和display()兩個方法的具體表現跟CurrentConditionsDisplay有所不同,具體就不再羅嗦了,看代碼便知。
上面隻是具體的實作代碼,并沒有具體結果的示範,于是這裡提供了一個基于NUnit的測試項目,測試的同時也是很好的示範代碼,具體不詳細說了,大家看代碼便知。
ObserverWeatherDataDisplayFixture:
2using WeatherDataImp = DesignPatterns.Observer.WeatherData;
3using NUnit.Framework;
4
5namespace Test.DesignPatterns.Observer.WeatherData
6{
7 [TestFixture]
8 public class ObserverWeatherDataDisplayFixture
9 {
10 Members#region Members
11 WeatherDataImp.WeatherData weatherData;
12 WeatherDataImp.CurrentConditionsDisplay currentConditionsDisplay;
13 WeatherDataImp.ForcastDisplay forcastDisplay;
14 WeatherDataImp.StatisticsDisplay statisticsDisplay;
15 #endregion//Members
16
17 TestFixtureSetUp Init()#region TestFixtureSetUp Init()
18 [TestFixtureSetUp]
19 public void Init()
20 {
21 weatherData = new WeatherDataImp.WeatherData();
22 currentConditionsDisplay = new WeatherDataImp.CurrentConditionsDisplay(weatherData);
23 forcastDisplay = new WeatherDataImp.ForcastDisplay(weatherData);
24 statisticsDisplay = new WeatherDataImp.StatisticsDisplay(weatherData);
26 #endregion// TestFixtureSetUp Init()
28 TestFixtureTearDown Dispose()#region TestFixtureTearDown Dispose()
29 [TestFixtureTearDown]
30 public void Dispose()
32 weatherData = null;
33 currentConditionsDisplay = null;
34 forcastDisplay = null;
35 statisticsDisplay = null;
36 }
37 #endregion//TestFixtureTearDown Dispose()
38
39 TestCurrentConditionsDisplay#region TestCurrentConditionsDisplay
40 [Test]
41 public void TestCurrentConditionsDisplay()
42 {
43 weatherData.SetMeasurements(80,65,30.4f);
44
45 Assert.AreEqual("Current conditions: 80F degrees and 65% humidity",
46 currentConditionsDisplay.Display());
48 #endregion//TestCurrentConditionsDisplay
49
50 TestForecastDisplay#region TestForecastDisplay
51 [Test]
52 public void TestForecastDisplay()
53 {
54 weatherData.SetMeasurements(81,63,31.2f);
55 //lastPressure = 29.92f
56 Assert.AreEqual("Forecast: Improving weather on the way!",
57 forcastDisplay.Display());
58
59 weatherData.SetMeasurements(81,63,29.92f);
60 Assert.AreEqual("Forecast: Watch out for cooler, rainy weather",
61 forcastDisplay.Display());
62
63 weatherData.SetMeasurements(81,63,29.92f);
64 Assert.AreEqual("Forecast: More of the same",
65 forcastDisplay.Display());
66 }
67 #endregion//TestForecastDisplay
68
69 TestStatisticsDisplay#region TestStatisticsDisplay
70 [Test]
71 public void TestStatisticsDisplay()
72 {
73 weatherData.SetMeasurements(80,63,31.2f);
74 weatherData.SetMeasurements(81,63,29.92f);
75 weatherData.SetMeasurements(84,63,29.92f);
76 if(statisticsDisplay.NumberOfReadings == 3)
77 {
78 Assert.AreEqual("Avg/Max/Min temperature = 81.67F/84F/80F",
79 statisticsDisplay.Display());
80 }
81 if(statisticsDisplay.NumberOfReadings == 8)
82 {
83 Assert.AreEqual("Avg/Max/Min temperature = 81.00F/84F/80F",
84 statisticsDisplay.Display());
85 }
86 }
87 #endregion//TestStatisticsDisplay
88 }
89}
90
完整代碼下載下傳:(VS2005、NUnit2.2/2.4)
上面已經對觀察者模式做了比較詳細的介紹,還是那句話,人無完人,模式也不是萬能的,我們要用好設計模式來解決我們的實際問題,就必須熟知模式的應用場景和優缺點:
觀察者模式的應用場景:
1、 對一個對象狀态的更新,需要其他對象同步更新,而且其他對象的數量動态可變。
2、 對象僅需要将自己的更新通知給其他對象而不需要知道其他對象的細節。
觀察者模式的優點:
1、 Subject和Observer之間是松偶合的,分别可以各自獨立改變。
2、 Subject在發送廣播通知的時候,無須指定具體的Observer,Observer可以自己決定是否要訂閱Subject的通知。
3、 遵守大部分GRASP原則和常用設計原則,高内聚、低偶合。
觀察者模式的缺陷:
1、 松偶合導緻代碼關系不明顯,有時可能難以了解。(廢話)
2、 如果一個Subject被大量Observer訂閱的話,在廣播通知的時候可能會有效率問題。(畢竟隻是簡單的周遊)
備注:關于場景和優缺點,上面肯定說得不夠全面,歡迎大家來補充。
下面我們再使用C#裡的委托/事件機制重新實作上面的氣象站監測系統,大家可以再通過具體代碼實際體會一下,看看使用delegate和event實作的觀察者模式是不是要比前面的實作簡單很多呀!哈哈!
WeatherData:
4namespace DesignPatterns.Observer.CSharp.WeatherData
6 public class WeatherData
8 private Hashtable weatherItems = new Hashtable();
9
10 public WeatherData(float temperature, float humidity,
11 float pressure)
12 {
13 weatherItems.Add("temperature", temperature);
14 weatherItems.Add("humidity", humidity);
15 weatherItems.Add("pressure", pressure);
18 public delegate string GetMeasurementsDelegateHandler(Hashtable measurements, object measurementKey);
19 public event GetMeasurementsDelegateHandler GetUpdatedMeasurements;
20
21 public object Temperature
22 {
23 get
24 {
25 if(this.weatherItems["temperature"] != null)
26 {
27 return GetUpdatedMeasurements(weatherItems, "temperature");
28 }
30 return null;
34 public object Humidity
36 get
38 if(this.weatherItems["humidity"] != null)
39 {
40 return GetUpdatedMeasurements(weatherItems, "humidity");
41 }
42
43 return null;
45 }
46
47 public object Pressure
48 {
49 get
50 {
51 if(this.weatherItems["pressure"] != null)
52 {
53 return GetUpdatedMeasurements(weatherItems, "pressure");
54 }
56 return null;
57 }
58 }
59 }
60}
這個類是使用委托和事件重新實作的,代碼結構很簡單,關鍵就是了解GetUpdatedMeasurements事件即可,這是一個标準的事件實作,在這裡對應于前面實作的WeatherData.NotifyObserver()方法,在事件被觸發的時候,會通知所有挂接在這個事件上的委托方法,實作廣播通知,而這裡的委托GetMeasurementsDelegateHandler則對應于上面的IObserver.Update(),它規定了觀察者與被觀察者之間的接口契約。其實所有我們常用的按鈕事件等功能都是這個原理,從這個角度來了解它們也都是一個觀察者模式的具體實作。
StatisticsDisplay:
6 public class StatisticsDisplay
8 public StatisticsDisplay()
9 {}
11 public string GetUpdatedMeasurements(Hashtable measurements, object measurementKey)
13 switch(measurementKey.ToString())
14 {
15 case "temperature":
16 return "Current temperature is: " +
17 RoundFloatToString((float)Convert.ToInt32(measurements["temperature"]));
18 case "humidity":
19 return "Current humidity is: " +
20 RoundFloatToString((float)Convert.ToInt32(measurements["humidity"]))+ "%";
21 case "pressure":
22 return "Current barometric pressure is: " +
23 RoundFloatToString((float)Convert.ToInt32(measurements["pressure"]));
24 default:
25 return null;
26 }
27 }
29 RoundFloatToString#region RoundFloatToString
30 public static string RoundFloatToString(float floatToRound)
32 System.Globalization.CultureInfo cultureInfo = new System.Globalization.CultureInfo("en-US");
33 cultureInfo.NumberFormat.CurrencyDecimalDigits = 2;
34 cultureInfo.NumberFormat.CurrencyDecimalSeparator = ".";
35 return floatToRound.ToString("F",cultureInfo);
37 #endregion//RoundFloatToString
39 }
40}
這個類也是重新實作的一個顯示裝置對象,其内部實作的GetUpdatedMeasurements()方法符合在WeatherData裡聲明的GetMeasurementsDelegateHandler委托的契約,是以可以被動态綁定到WeatherData的GetUpdatedMeasurements事件上,如此這般就實作了訂閱WeatherData的功能。
同樣上面隻是具體的實作代碼,并沒有具體結果的示範,這裡也提供了一個基于NUnit的測試項目,測試的同時也是很好的示範代碼,具體不詳細說了,大家看代碼便知。
ObserverCSharpStatisticsDisplayFixture:
1using WeatherDataImp = DesignPatterns.Observer.CSharp.WeatherData;
2using NUnit.Framework;
4namespace Test.DesignPatterns.Observer.CSharp.WeatherData
6 [TestFixture]
7 public class ObserverCSharpStatisticsDisplayFixture
8 {
9 Members#region Members
10 WeatherDataImp.WeatherData weatherData;
11 WeatherDataImp.StatisticsDisplay statisticsDisplay;
12 WeatherDataImp.WeatherData.GetMeasurementsDelegateHandler weatherDelegate;
13 #endregion//Members
14
15 TestFixtureSetUp Init()#region TestFixtureSetUp Init()
16 [TestFixtureSetUp]
17 public void Init()
19 weatherData = new WeatherDataImp.WeatherData(65.2f,80.5f, 32.3f);
20 statisticsDisplay = new WeatherDataImp.StatisticsDisplay();
21
22 //通過委托建立松偶合的關聯
23 weatherDelegate = new WeatherDataImp.WeatherData.GetMeasurementsDelegateHandler(statisticsDisplay.GetUpdatedMeasurements);
24 weatherData.GetUpdatedMeasurements += weatherDelegate;
33 statisticsDisplay = null;
34 weatherData = null;
36 #endregion//TestFixtureTearDown Dispose()
37
38 TestStatisticsDisplay#region TestStatisticsDisplay
39 [Test]
40 public void TestStatisticsDisplay()
41 {
42 Assert.AreEqual("Current temperature is: 65.00",
43 weatherData.Temperature);
44 Assert.AreEqual("Current humidity is: 80.00%",
45 weatherData.Humidity);
46 Assert.AreEqual("Current barometric pressure is: 32.00",
47 weatherData.Pressure);
48 }
49 #endregion//TestStatisticsDisplay
50 }
51}
52
相信大家現在對觀察者模式都應該很清楚了吧!OK!那麼,就像我們在前面的文章裡反複強調的一樣,設計原則遠比模式重要,學習設計模式的同時一定要注意體會設計原則的應用。這裡我們再來看看觀察者模式裡都符合那些設計原則。
1、 Identify the aspects of your application that vary and separate them from what stays the same. (找到系統中變化的部分,将變化的部分同其它穩定的部分隔開。)
在觀察者模式的應用場景裡變化的部分是Subject的狀态和Observer的數量。使用Observer模式可以很好地将這兩部分隔離開,我們可以任意改變Observer的數量而不需要去修改Subject,而Subject的狀态也可以任意改變,同樣不會對其Observer有任何影響。
2、 Program to an interface,not an implementation.(面向接口程式設計,而不要面向實作程式設計。)
Subject和Observer都使用接口來實作。Subject隻需要跟蹤那些實作了IObserver接口的對象,是以其隻依賴于IObserver;而所有Observer都通過ISubject接口來注冊、撤銷、接收通知,是以它們也隻依賴于 ISubject;是以這是面向接口程式設計的,這樣的實作方式使得Subject和Observer之間完全沒有任何耦合。
3、 Favor composition over inheritance.(優先使用對象組合,而非類繼承)
觀察者模式使用對象組合将Subject和若幹observer聯系起來。它們之間的關系不是通過類的繼承而是在運作時的動态組合。