觀察者模式(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
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接,否則保留追究法律責任的權利。