C#設計模式(4)——觀察者模式(Observer Pattern)
一、引言
在現實生活中,處處可見觀察者模式,例如,微信中的訂閱號,訂閱部落格和QQ微博中關注好友,這些都屬於觀察者模式的應用。在這一章將分享我對觀察者模式的理解,廢話不多說了,直接進入今天的主題。
二、 觀察者模式的介紹
2.1 觀察者模式的定義
從生活中的例子可以看出,只要對訂閱號進行關注的客戶端,如果訂閱號有什麼更新,就會直接推送給訂閱了的使用者。從中,我們就可以得出觀察者模式的定義。
觀察者模式定義了一種一對多的依賴關係,讓多個觀察者物件同時監聽某一個主題物件,這個主題物件在狀態發生變化時,會通知所有觀察者物件,使它們能夠自動更新自己的行為。
2.2 觀察者模式的結構
從上面觀察者模式的定義和生活中的例子,很容易知道,觀察者模式中首先會存在兩個物件,一個是觀察者物件,另一個就是主題物件,然而,根據面向介面程式設計的原則,則自然就有抽象主題角色和抽象觀察者角色。理清楚了觀察者模式中涉及的角色後,接下來就要理清他們之間的關聯了,要想主題物件狀態發生改變時,能通知到所有觀察者角色,則自然主題角色必須所有觀察者的引用,這樣才能在自己狀態改變時,通知到所有觀察者。有了上面的分析,下面觀察者的結構圖也就很容易理解了。具體結構圖如下所示:
圖 觀察者模式結構圖
可以看出,在觀察者模式的結構圖有以下角色:
- 抽象主題角色(Subject):抽象主題把所有觀察者物件的引用儲存在一個列表中,並提供增加和刪除觀察者物件的操作,抽象主題角色又叫做抽象被觀察者角色,一般由抽象類或介面實現。
- 抽象觀察者角色(Observer):為所有具體觀察者定義一個介面,在得到主題通知時更新自己,一般由抽象類或介面實現。
- 具體主題角色(ConcreteSubject):實現抽象主題介面,具體主題角色又叫做具體被觀察者角色。
- 具體觀察者角色(ConcreteObserver):實現抽象觀察者角色所要求的介面,以便使自身狀態與主題的狀態相協調。
2.3 觀察者模式的實現
下面以微信訂閱號的例子來說明觀察者模式的實現。現在要實現監控騰訊遊戲訂閱號的狀態的變化。這裡一開始不採用觀察者模式來實現,而通過一步步重構的方式,最終重構為觀察者模式。因為一開始拿到需求,自然想到有兩個類,一個是騰訊遊戲訂閱號類,另一個是訂閱者類。訂閱號類中必須引用一個訂閱者物件,這樣才能在訂閱號狀態改變時,呼叫這個訂閱者物件的方法來通知到訂閱者物件。有了這個分析,自然實現的程式碼如下所示:
1 // 騰訊遊戲訂閱號類 2 public class TenxunGame 3 { 4 // 訂閱者物件 5 public Subscriber Subscriber {get;set;} 6 7 public String Symbol {get; set;} 8 9 public string Info {get ;set;} 10 11 public void Update() 12 { 13 if (Subscriber != null) 14 { 15 // 呼叫訂閱者物件來通知訂閱者 16 Subscriber.ReceiveAndPrintData(this); 17 } 18 } 19 20 } 21 22 // 訂閱者類 23 public class Subscriber 24 { 25 public string Name { get; set; } 26 public Subscriber(string name) 27 { 28 this.Name = name; 29 } 30 31 public void ReceiveAndPrintData(TenxunGame txGame) 32 { 33 Console.WriteLine("Notified {0} of {1}'s" + " Info is: {2}", Name, txGame.Symbol, txGame.Info); 34 } 35 } 36 37 // 客戶端測試 38 class Program 39 { 40 static void Main(string[] args) 41 { 42 // 例項化訂閱者和訂閱號物件 43 Subscriber LearningHardSub = new Subscriber("LearningHard"); 44 TenxunGame txGame = new TenxunGame(); 45 46 txGame.Subscriber = LearningHardSub; 47 txGame.Symbol = "TenXun Game"; 48 txGame.Info = "Have a new game published ...."; 49 50 txGame.Update(); 51 52 Console.ReadLine(); 53 } 54 }
上面程式碼確實實現了監控訂閱號的任務。但這裡的實現存在下面幾個問題:
- TenxunGame類和Subscriber類之間形成了一種雙向依賴關係,即TenxunGame呼叫了Subscriber的ReceiveAndPrintData方法,而Subscriber呼叫了TenxunGame類的屬性。這樣的實現,如果有其中一個類變化將引起另一個類的改變。
- 當出現一個新的訂閱者時,此時不得不修改TenxunGame程式碼,即新增另一個訂閱者的引用和在Update方法中呼叫另一個訂閱者的方法。
上面的設計違背了“開放——封閉”原則,顯然,這不是我們想要的。對此我們要做進一步的抽象,既然這裡變化的部分是新訂閱者的出現,這樣我們可以對訂閱者抽象出一個介面,用它來取消TenxunGame類與具體的訂閱者之間的依賴,做這樣一步改進,確實可以解決TenxunGame類與具體訂閱者之間的依賴,使其依賴與介面,從而形成弱引用關係,但還是不能解決出現一個訂閱者不得不修改TenxunGame程式碼的問題。對此,我們可以做這樣的思考——訂閱號存在多個訂閱者,我們可以採用一個列表來儲存所有的訂閱者物件,在訂閱號內部再新增對該列表的操作,這樣不就解決了出現新訂閱者的問題了嘛。並且訂閱號也屬於變化的部分,所以,我們可以採用相同的方式對訂閱號進行抽象,抽象出一個抽象的訂閱號類,這樣也就可以完美解決上面程式碼存在的問題了,具體的實現程式碼為:
1 // 訂閱號抽象類 2 public abstract class TenXun 3 { 4 // 儲存訂閱者列表 5 private List<IObserver> observers = new List<IObserver>(); 6 7 public string Symbol { get; set; } 8 public string Info { get; set; } 9 public TenXun(string symbol, string info) 10 { 11 this.Symbol = symbol; 12 this.Info = info; 13 } 14 15 #region 新增對訂閱號列表的維護操作 16 public void AddObserver(IObserver ob) 17 { 18 observers.Add(ob); 19 } 20 public void RemoveObserver(IObserver ob) 21 { 22 observers.Remove(ob); 23 } 24 #endregion 25 26 public void Update() 27 { 28 // 遍歷訂閱者列表進行通知 29 foreach (IObserver ob in observers) 30 { 31 if (ob != null) 32 { 33 ob.ReceiveAndPrint(this); 34 } 35 } 36 } 37 } 38 39 // 具體訂閱號類 40 public class TenXunGame : TenXun 41 { 42 public TenXunGame(string symbol, string info) 43 : base(symbol, info) 44 { 45 } 46 } 47 48 // 訂閱者介面 49 public interface IObserver 50 { 51 void ReceiveAndPrint(TenXun tenxun); 52 } 53 54 // 具體的訂閱者類 55 public class Subscriber : IObserver 56 { 57 public string Name { get; set; } 58 public Subscriber(string name) 59 { 60 this.Name = name; 61 } 62 63 public void ReceiveAndPrint(TenXun tenxun) 64 { 65 Console.WriteLine("Notified {0} of {1}'s" + " Info is: {2}", Name, tenxun.Symbol, tenxun.Info); 66 } 67 } 68 69 // 客戶端測試 70 class Program 71 { 72 static void Main(string[] args) 73 { 74 TenXun tenXun = new TenXunGame("TenXun Game", "Have a new game published ...."); 75 76 // 新增訂閱者 77 tenXun.AddObserver(new Subscriber("Learning Hard")); 78 tenXun.AddObserver(new Subscriber("Tom")); 79 80 tenXun.Update(); 81 82 Console.ReadLine(); 83 } 84 }
上面程式碼是我們進行重構後的實現,重構後的程式碼實現類圖如下所示:
從上圖可以發現,這樣的實現就是觀察者模式的實現。這樣,在任何時候,只要呼叫了TenXun類的Update方法,它就會通知所有的觀察者物件,同時,可以看到,觀察者模式,取消了直接依賴,變為間接依賴,這樣大大提供了系統的可維護性和可擴充套件性。這裡並不是直接給出觀察者模式的實現,而是通過一步步重構的方式來引出觀察者模式的實現,相信通過這個方式,大家可以更深刻地理解觀察者模式所解決的問題和帶來的好處。
三、.NET 中觀察者模式的應用
在.NET中,我們可以使用委託與事件來簡化觀察者模式的實現,上面的例子用事件和委託的實現如下程式碼所示:
1 namespace ObserverInNET 2 { 3 class Program 4 { 5 // 委託充當訂閱者介面類 6 public delegate void NotifyEventHandler(object sender); 7 8 // 抽象訂閱號類 9 public class TenXun 10 { 11 public NotifyEventHandler NotifyEvent; 12 13 public string Symbol { get; set; } 14 public string Info { get; set; } 15 public TenXun(string symbol, string info) 16 { 17 this.Symbol = symbol; 18 this.Info = info; 19 } 20 21 #region 新增對訂閱號列表的維護操作 22 public void AddObserver(NotifyEventHandler ob) 23 { 24 NotifyEvent += ob; 25 } 26 public void RemoveObserver(NotifyEventHandler ob) 27 { 28 NotifyEvent -= ob; 29 } 30 31 #endregion 32 33 public void Update() 34 { 35 if (NotifyEvent != null) 36 { 37 NotifyEvent(this); 38 } 39 } 40 } 41 42 // 具體訂閱號類 43 public class TenXunGame : TenXun 44 { 45 public TenXunGame(string symbol, string info) 46 : base(symbol, info) 47 { 48 } 49 } 50 51 // 具體訂閱者類 52 public class Subscriber 53 { 54 public string Name { get; set; } 55 public Subscriber(string name) 56 { 57 this.Name = name; 58 } 59 60 public void ReceiveAndPrint(Object obj) 61 { 62 TenXun tenxun = obj as TenXun; 63 64 if (tenxun != null) 65 { 66 Console.WriteLine("Notified {0} of {1}'s" + " Info is: {2}", Name, tenxun.Symbol, tenxun.Info); 67 } 68 } 69 } 70 71 static void Main(string[] args) 72 { 73 TenXun tenXun = new TenXunGame("TenXun Game", "Have a new game published ...."); 74 Subscriber lh = new Subscriber("Learning Hard"); 75 Subscriber tom = new Subscriber("Tom"); 76 77 // 新增訂閱者 78 tenXun.AddObserver(new NotifyEventHandler(lh.ReceiveAndPrint)); 79 tenXun.AddObserver(new NotifyEventHandler(tom.ReceiveAndPrint)); 80 81 tenXun.Update(); 82 83 Console.WriteLine("-----------------------------------"); 84 Console.WriteLine("移除Tom訂閱者"); 85 tenXun.RemoveObserver(new NotifyEventHandler(tom.ReceiveAndPrint)); 86 tenXun.Update(); 87 88 Console.ReadLine(); 89 } 90 } 91 }
從上面程式碼可以看出,使用事件和委託實現的觀察者模式中,減少了訂閱者介面類的定義,此時,.NET中的委託正式充到訂閱者介面類的角色。使用委託和事件,確實簡化了觀察者模式的實現,減少了一個IObserver介面的定義,上面程式碼的執行結果如下圖所示:
四、觀察者模式的適用場景
在下面的情況下可以考慮使用觀察者模式:
- 當一個抽象模型有兩個方面,其中一個方面依賴於另一個方面,將這兩者封裝在獨立的物件中以使它們可以各自獨立地改變和複用的情況下。從方面的這個詞中可以想到,觀察者模式肯定在AOP(面向方面程式設計)中有所體現,更多內容參考:Observern Pattern in AOP.
- 當對一個物件的改變需要同時改變其他物件,而又不知道具體有多少物件有待改變的情況下。
- 當一個物件必須通知其他物件,而又不能假定其他物件是誰的情況下。
五、觀察者模式的優缺點
觀察者模式有以下幾個優點:
- 觀察者模式實現了表示層和資料邏輯層的分離,並定義了穩定的更新訊息傳遞機制,並抽象了更新介面,使得可以有各種各樣不同的表示層,即觀察者。
- 觀察者模式在被觀察者和觀察者之間建立了一個抽象的耦合,被觀察者並不知道任何一個具體的觀察者,只是儲存著抽象觀察者的列表,每個具體觀察者都符合一個抽象觀察者的介面。
- 觀察者模式支援廣播通訊。被觀察者會向所有的註冊過的觀察者發出通知。
觀察者也存在以下一些缺點:
- 如果一個被觀察者有很多直接和間接的觀察者時,將所有的觀察者都通知到會花費很多時間。
- 雖然觀察者模式可以隨時使觀察者知道所觀察的物件傳送了變化,但是觀察者模式沒有相應的機制使觀察者知道所觀察的物件是怎樣發生變化的。
- 如果在被觀察者之間有迴圈依賴的話,被觀察者會觸發它們之間進行迴圈呼叫,導致系統崩潰,在使用觀察者模式應特別注意這點。