1.觀察者模式介紹
觀察者模式又叫釋出-訂閱模式,它定義了物件間的一種一對多關係,當一個物件的狀態發生改變時,所有依賴於它的物件都會收到通知並被自動更新。觀察者模式就四個角色:抽象主題,具體主題,抽象觀察者,具體觀察者。抽象主題是一個抽象的介面或者抽象類,對主題的功能進行抽象,抽象觀察者對具體的觀察者進行抽象。觀察者模式在軟體開發中的應用十分廣泛,如微信訂閱號、微博訂閱等都採用了觀察者模式,我們關注了某個明星的微博,當這個明星更新微博狀態時我們的微博都會收到通知,明星的微博就是主題角色,我們的微博屬於觀察者角色。
下邊通過大話設計模式中祕書做臥底的例子來理解觀察者模式的用法。上班時間有的同事喜歡偷偷看股票行情,有的同事看NBA直播,但是老闆會不定時來辦公室,如果被老闆逮到就不好了,於是大家想出來一個辦法:讓祕書小妹在外邊把風,如果老闆來了就通知下大家,這樣就不會被逮到了。這個栗子中祕書小妹就是一個主題,同事們屬於觀察者,祕書小妹如果看到老闆過來就會通知 送她零食的同事們。程式碼比較簡單:
//抽象主題類 public interface ISubject { //新增觀察者 送零食的加進來,老闆來了通知你 void Add(Observer observer); //刪除觀察者 不送零食的祕書小妹就不通知了 void Remove(Observer observer); //主題狀態 string SubjectState { get; set; } //通知方法 void Notify(); } //具體主題 ,祕書類 public class Mishu : ISubject { //祕書要知道通知哪些同事 private IList<Observer> observers = new List<Observer>(); public void Add(Observer observer) { observers.Add(observer); } public void Remove(Observer observer) { observers.Remove(observer); } public string SubjectState { get; set; } public void Notify() { foreach (Observer o in observers) { o.Update(); } } } //抽象觀察者 public abstract class Observer { //名字 protected string name; //觀察者要知道自己訂閱了那個主題 protected ISubject sub; public Observer(string name, ISubject sub) { this.name = name; this.sub = sub; } //接受到通知後的更新方法 public abstract void Update(); } //看股票的同事 public class StockObserver : Observer { public StockObserver(string name, ISubject sub) : base(name, sub) { } public override void Update() { Console.WriteLine($"通知內容:{sub.SubjectState},反應:{name}關閉股票行情,繼續工作!"); } } //看NBA的同事 public class NBAObserver : Observer { public NBAObserver(string name, ISubject sub) : base(name, sub) { } public override void Update() { Console.WriteLine($"通知內容:{sub.SubjectState},反應:{name}關閉NBA直播,繼續工作!"); } } /// <summary> /// 客戶端呼叫 /// </summary> class Program { static void Main(string[] args) { Mishu mishu = new Mishu(); //新建同事 觀察者角色 Observer tongshi1 = new StockObserver("巴菲特", mishu); Observer tongshi2 = new NBAObserver("麥迪", mishu); //祕書小妹要知道哪些同事要通知(主題要知道所有訂閱了自己的觀察者) mishu.Add(tongshi1); mishu.Add(tongshi2); //主題狀態更改了 mishu.SubjectState = "老闆回來了!"; //呼叫主題的通知方法 mishu.Notify(); Console.ReadKey(); } }
執行程式,執行結果如下:
上邊的例子中祕書小妹充當主題角色,上班偷懶的同事充當觀察者角色,通過觀察者模式實現了功能。但是這裡還有一個小問題:上邊例子能夠成功的前提是所有觀察者的角色都有一個Update()方法來執行更新。但是有時候各個觀察者並不都是相同的型別,如觀察者1收到通知執行Update1()方法,而觀察者2收到通知執行的是Update2()方法,這時候採用上邊的模式就不能滿足需求了。怎麼改進呢?①各個觀察者不屬於同一類,所以不需要抽象觀察者類了 ②因為各個觀察者的反應不是同一的Update(),所以我們不能foreach遍歷觀察者集合來統一呼叫Update()方法了,這時可以考慮通過事件委託在客戶端確定所有觀察者的反應。改進後的程式碼如下:
//抽象主題角色 public interface ISubject { //新增觀察者 void Add(Observer observer); //刪除觀察者 void Remove(Observer observer); //主題狀態 string SubjectState { get; set; } //通知方法 void Notify(); } //***********************1.定義一個委託 public delegate void EventHandler();
//具體主題角色 祕書類 public class Mishu : ISubject { public event EventHandler Update; //儲存要通知的同事 public IList<Observer> observers = new List<Observer>(); public string SubjectState { get; set; } public void Add(Observer observer) { observers.Add(observer); } public void Remove(Observer observer) { observers.Remove(observer); } //**********2.通知方法中不能通過遍歷來統一呼叫每個觀察者的Update()方法了,改成執行一個委託 public void Notify() { Update(); } } //抽象觀察者角色 public abstract class Observer { protected ISubject sub; protected string name; protected Observer(string name,ISubject sub) { this.name = name; this.sub = sub; } } //具體觀察者角色 看股票的同事 public class StockObserver { public string name; public ISubject sub; public StockObserver(string name,ISubject sub) { this.name = name; this.sub = sub; } public void CloseStockMarket() { Console.WriteLine($"通知內容:{sub.SubjectState},反應:{name}關閉股票行情,繼續工作!"); } } //具體觀察者角色 看NBA的同事 public class NBAObserver { public string name; public ISubject sub; public NBAObserver(string name, ISubject sub) { this.name = name; this.sub = sub; } public void CloseNBA() { Console.WriteLine($"通知內容:{sub.SubjectState},反應:{name}關閉NBA直播,繼續工作!"); } } class Program { static void Main(string[] args) { Mishu mishu = new Mishu(); //觀察者訂閱了主題mishu StockObserver tongshi1 = new StockObserver("巴菲特", mishu); NBAObserver tongshi2 = new NBAObserver("麥迪", mishu); //*******************3.將遍歷觀察者並呼叫觀察者的Update(),改成了事件委託形式 mishu.Update += tongshi1.CloseStockMarket; mishu.Update += tongshi2.CloseNBA; //主題狀態更改,並通知 mishu.SubjectState = "老闆回來了!"; mishu.Notify(); Console.ReadKey(); } }
執行結果:
我們看到通過事件委託我們可以實現讓不同的觀察者呼叫不同的方法,當我們遇到類似這樣的情況:點選一個按鈕,有的頁面元素顯示Show(),有的隱藏Hide(),有的關閉Close()就可以採用這種模式。但是這種模式沒有對具體的觀察者進行抽象,如果觀察者太多也會造成事件委託過於複雜。兩種觀察者模式的實現各有利弊,我們可以根據實際的情況來選擇。
2.小結
上邊栗子的類圖:
觀察者模式的使用場景:當一個物件的狀態發生變化時,需要讓其它物件知道並作出反應可以考慮觀察者模式。
觀察者模式的優點:想一下如果不使用觀察者模式,訂閱者怎麼獲取訂閱號的更新?最直接的方法應該就是輪詢了,這種方式十分浪費資源,而且獲取更新也不及時。觀察者模式的主要功能就是解決了這一問題。這種模式符合依賴倒置原則,同時降低了觀察者和主題間的耦合,建立了一套觸發機制。