使用委托和事件实现观察者模式(Observer Pattern)
观察者模式(Observer Pattern)有时又被称为订阅——发布模式,它主要应对这样的场景:需要将单一事件的通知(比如对象状态发生变化)广播给多个订阅者(观察者)。在这里我们通过C#的委托和事件来实现这一通用的模式。
现在我们来考虑一个温度控制器的例子。假设:一个加热器(Heater)和一个制冷器(Cooler)连接到同一个温度控制器(Thermostat)。温度控制器根据温度的变化通知给加热器(Heater)和制冷器(Cooler),二者根据温度来控制自己开关。
首先我们定义Heater类
1 class Heater
2 {
3 public Heater(float temperature)
4 {
5 this.Temperature = temperature;
6 }
7
8 public float Temperature
9 {
10 get { return _Temperature; }
11 set { _Temperature = value; }
12 }
13 private float _Temperature;
14
15 public void OnTemperatureChanged(float newTemperature)
16 {
17 if (newTemperature > this.Temperature)
18 {
19 Console.WriteLine("温度太高,加热器关闭.");
20 }
21 else
22 {
23 Console.WriteLine("温度太低,加热器打开.");
24 }
25 }
26 }
接着我们定义和Heater类似的Cooler类
1 class Cooler
2 {
3 public Cooler(float temperature)
4 {
5 this.Temperature = temperature;
6 }
7
8 public float Temperature
9 {
10 get { return _Temperature; }
11 set { _Temperature = value; }
12 }
13 private float _Temperature;
14
15 public void OnTemperatureChanged(float newTemperature)
16 {
17 if (newTemperature > this.Temperature)
18 {
19 Console.WriteLine("温度太高,制冷器打开.");
20 }
21 else
22 {
23 Console.WriteLine("温度太低,制冷器关闭.");
24 }
25 }
26 }
下面是关键的Thermostat类
1 class Thermostat
2 {
3 ///<summary>定义一个委托类型</summary>
4 public delegate void TemperatureChangeHandler(float newTemperature);
5
6 public TemperatureChangeHandler OnTemperatureChange
7 {
8 get { return _OnTemperatureChange; }
9 set { _OnTemperatureChange = value; }
10 }
11 private TemperatureChangeHandler _OnTemperatureChange;
12
13 public float CurrentTemperature
14 {
15 get { return _CurrentTemperature; }
16 set
17 {
18 if (value != _CurrentTemperature)
19 {
20 _CurrentTemperature = value;
21 TemperatureChangeHandler localOnChange = OnTemperatureChange;
22 if (localOnChange != null)//调用一个委托之前需要检查它的值是否为空
23 {
24 //使用循环,手动遍历订阅者列表,避免一个调用出现异常时委托链中断
25 foreach (TemperatureChangeHandler handler in localOnChange.GetInvocationList())
26 {
27 try
28 {
29 handler(value);
30 }
31 catch (Exception ex)
32 {
33 Console.WriteLine(ex.Message);
34 }
35 }
36 }
37 }
38 }
39 }
40 private float _CurrentTemperature;
41 }
运行程序
1 class Program
2 {
3 static void Main(string[] args)
4 {
5 Heater heater = new Heater(90);
6 Cooler cooler = new Cooler(60);
7 Thermostat thermostat = new Thermostat();
8 string temperature;
9 //订阅通知
10 thermostat.OnTemperatureChange += heater.OnTemperatureChanged;
11 //thermostat.OnTemperatureChange += (newTemperature) => { throw new Exception(); };
12 thermostat.OnTemperatureChange += cooler.OnTemperatureChanged;
13
14 Console.Write("输入温度:");
15 temperature = Console.ReadLine();
16 thermostat.CurrentTemperature = (float.Parse(temperature));
17 Console.Read();
18 }
19 }
那么我们开始分析上面的程序,Heater类和Cooler类结构很简单,就是一个初始温度属性和初始的构造函数,以及一个控制开关的方法。Thermostat类 也就是自动调温的类是实现Observer模式的关键点,这个类首先定义一个包含一个参数的委托类型(TemperatureChangeHandler),然后定义一个TemperatureChangeHandler这个定义的委托类型的属性(OnTemperatureChange),通过这个属性可以将与这个委托类型(TemperatureChangeHandler)相匹配的方法绑定到这个属性中(所有订阅者方法都必须使用与委托相同的方法签名)。再通过CurrentTemperature属性,将当前实际温度通过广播通知所有和TemperatureChangeHandler属性绑定的委托实例。
虽然上面的代码实现了我们想要达到的效果,但是需要下面的问题:
由于OnTemperatureChange潜在地对应于一个委托链,当使用上面的代码时status只代表了一个委托其他的委托都会全部丢失。
1,当委托实例方法有返回值(ref,out或不是void)时的处理
假如前面的代码中每个委托都有一个状态返回,指出设备是否因温度的改变而启动,修改如下:
public enum Status { On, Off } public delegate Status TemperatureChangeHanlder(float newTemperature);
当我们调用一个有返回值的委托实例:
Status status = OnTemperatureChange(value);
由于这样调用一个委托,就可能造成一个通知发送给多个订阅者,加入订阅者有返回值,就无法确定应该调用哪个订阅者的返回值。
2,人为的使用错误的委托赋值方式
先看前面我们绑定委托的语言:
thermostat.OnTemperatureChange += heater.OnTemperatureChanged;
当我们由于人为的错误把"+="写成了"="时也会丢造成失委托链的Bug。
3,在外部直接调用委托
我们可以直接在外部这样调用:
thermostat.OnTemperatureChange(99);
这样也容易造成异常的出现,比如当没有为委托赋给相应的委托实例的时候就会NullReferenceException异常。
造成上面的问题的出现在于封装得不彻底,即不论是订阅还是发布都不能够得到充分的控制,下面我们使用C#中的事件(Event)来解决上述的问题。
下面是修改后的代码(修改部分):
首先修改Thermostat类
1 public class Thermostat
2 {
3 public class TemperatureArgs : EventArgs
4 {
5 public TemperatureArgs(float newTemperature)
6 {
7 this.NewTemperature = newTemperature;
8 }
9 public float NewTemperature
10 {
11 get { return _NewTemperature; }
12 set { _NewTemperature = value; }
13 }
14 private float _NewTemperature;
15 }
16
17 ///<summary>定义一个委托类型</summary>
18 public delegate void TemperatureChangeHandler(object sender, TemperatureArgs newTemperature);
19
20 /// <summary>定义一个事件</summary>
21 public event TemperatureChangeHandler OnTemperatureChange = delegate { };
22
23
24 public float CurrentTemperature
25 {
26 get { return _CurrentTemperature; }
27 set
28 {
29 if (value != _CurrentTemperature)
30 {
31 _CurrentTemperature = value;
32 if (OnTemperatureChange != null)//调用一个委托之前需要检查它的值是否为空
33 {
34 OnTemperatureChange(this, new TemperatureArgs(value));
35 }
36 }
37 }
38 }
39 private float _CurrentTemperature;
40 }
这里我们增加一个内部类TemperatureArgs,用于传递温度值。
添加了event关键字后,会禁止为一个public委托字段使用赋值运算符,同时只有包容类才能够调用向所有订阅者发出的委托通知。
换言之,event关键字提供了必要的封装来防止任何外部类发布一个事件或者取消之前的订阅者,这样我们就解决了前面直接使用委托存在的问题。
下面是其他类相应的修改:
Heater
1 class Heater
2 {
3 public Heater(float temperature)
4 {
5 this.Temperature = temperature;
6 }
7
8 public float Temperature
9 {
10 get { return _Temperature; }
11 set { _Temperature = value; }
12 }
13 private float _Temperature;
14
15 public void OnTemperatureChanged(object sender, Thermostat.TemperatureArgs newTemperature)
16 {
17 if (newTemperature.NewTemperature > this.Temperature)
18 {
19 Console.WriteLine("温度太高,加热器关闭.");
20 }
21 else
22 {
23 Console.WriteLine("温度太低,加热器打开.");
24 }
25 }
26 }
Cooler
1 class Cooler
2 {
3 public Cooler(float temperature)
4 {
5 this.Temperature = temperature;
6 }
7
8 public float Temperature
9 {
10 get { return _Temperature; }
11 set { _Temperature = value; }
12 }
13 private float _Temperature;
14
15 public void OnTemperatureChanged(object sender,Thermostat.TemperatureArgs newTemperature)
16 {
17 if (newTemperature.NewTemperature > this.Temperature)
18 {
19 Console.WriteLine("温度太高,制冷器打开.");
20 }
21 else
22 {
23 Console.WriteLine("温度太低,制冷器关闭.");
24 }
25 }
26 }
Main程序入口
1 class Program
2 {
3 static void Main(string[] args)
4 {
5 Heater heater = new Heater(90);
6 Cooler cooler = new Cooler(60);
7 Thermostat thermostat = new Thermostat();
8 string temperature;
9 //订阅通知,这里只能使用"+="或者"+-"
10 thermostat.OnTemperatureChange += heater.OnTemperatureChanged;
11 thermostat.OnTemperatureChange += cooler.OnTemperatureChanged;
12
13 Console.Write("输入温度:");
14 temperature = Console.ReadLine();
15 thermostat.CurrentTemperature = (float.Parse(temperature));
16 Console.Read();
17 }
18 }
最后,需要注意:
对OnTemperatureChange()的调用时每个订阅者都收到了通知,但是它们其实是通过顺序调用的,而不是同时调用,因为一个委托能指向另一个委托,后者又能指向其他委托。同时我们可以知道,事件就是对委托进行进一步的封装。委托是类型,事件是对象(或者对象的一个实例成员),事件的内部是用委托来实现的。
猛击下载:示例程序
参考资料:
《C#本质论》事件
作者:晴天猪
出处:http://www.cnblogs.com/IPrograming
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。