现在我们要为一家气象站开发一套气象监控系统,按照客户的要求,这个监控系统必须可以实时跟踪当前的天气状况(温度、湿度、大气压力),并且可以在三种不同设备上显示出来(当前天气状况、天气统计、天气预测)。客户还希望这个系统可以对外提供一个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联系起来。它们之间的关系不是通过类的继承而是在运行时的动态组合。