我們在寫一些即時系統的時候,會遇到一些情況,就是當資料更新時,我的頁面也要即時更新,像是聊天室, 股市資訊, 監控軟體...等。但是要更新的話,總不能每個相關元件的相依性都要一直 Keep 住吧,如果未來加了一堆相關元件不就改到死。這種一對多相依性的情況下就可以套用 Observer Pattern (觀察者模式),減少物件的相依性,並可自動更新。
題外話:在 Gang of Four 的書中提到一句話就可以貫穿整個 Observer Pattern 的精隨,
"Don't call us. We will call you!",
這句話是 Hollywood Principle。在 Hollywood 的環境中,想成名的演員們都會主動把履歷給星探,但是星探手中有許多許多的演員資料,如果每個演員每天都打電話給星探問說自己有沒有機會,那星探不就被煩死了,所以星探在一開始就會跟演員們說這句話,當星探覺得這個演員OK才會主動聯繫,如果演員一直沒接到電話那就代表GG了。
在 Observer Pattern 中的角色主要分為 Subject (被觀察者)與 Observer (觀察者)。對於 Subject 來說,他只要知道有多少觀察他的 Observer,不需要知道各個 Observer 的類型是什麼;而對於 Observer 來說,他只需要向 Subject 註冊,接著只要負責接收到通知更新就好了。
以下為範例程式碼:
SubjectBase.cs
ObserverBase.cs
StockInfo.cs
Investor.cs
Program.cs
範例程式碼要注意的有幾點。因為股價會波動,所以在這裡設計當股價改變時會去呼叫 Notify 方法去觸發 Investor Update。Investor 呼叫 AddFocus 將關注的標的加入,其實 AddFocus 內部實作就是呼叫 StockInfo 實體的 Attach 方法。
從執行結果來看,當台積電股價跳動時,Limited 和 Craig 因為有加入關注,所以會更新到。當大立光股價跳動時,只有 Limited 會更新,因為只有 Limited 有加入關注。
Observer Pattern 是個蠻常使用到的 Design Pattern,.Net 也為此在內部提供了改良過的 Observer Pattern 相關介面與實作方式。IObservable<T> 和 IObserver<T>介面。
.Net Observer Pattern 可說是訂閱者模式,與觀察者模式(Observer Pattern)不同的是,它主要角色為 Subscriber 與 Provider。
Subscriber 對應到 Observer,處於被動更新狀態。Subscriber 實作 IObserver<T>介面,並實作 OnCompleted, OnError, OnNext 方法。OnCompleted 定義為 Provider 對 Subscriber 進行通知完畢後執行的方法;OnError 定義為 Provider 在處理更新資料發生錯誤時執行的方法;OnNext 定義為 Subscriber 接收到 Porvider 派送的更新資訊。
Provider 實作 IObserverable<T>介面,專門負責派送資料的更新給 Subscriber。IObserverable<T>介面提供 Subscribe 方法,這和 Subject 的 Attach 方法類似,就是將此 Subscriber 加入未來派送更新的目標。限量一開始看到 Subscribe 方法回傳的 IDisposable 完全不知道是什麼意思,後來看了 MSDN 的說明後才知道我們還要額外加入一個 Unsubscriber 的類別,就是進行 Subject 的 Detach 的動作。
了解了基本的架構後,就要把這個架構串起來,通常最簡單的方式是在 Provider 類別中寫一個進行資料異動的方法,在資料異動完後去呼叫所有 Subscriber 的 OnNext 進行更新,以下限量將前面的範例程式改寫成 .Net Observer Pattern:
StockInfo.cs
StockTracker.cs
Investor.cs
Program.cs
上面修改後的程式碼中, StockInfo 回歸到最原本的"純資料"的結構,新增的 StockTracker 就負責處理更新與通知,而 Investor 一樣負責接收更新的資料並額外加入了錯誤處理與解除派送的事件。
參考來源:
Design Patterns - Gang of Four
dofactory - Observer
MSDN - IObserver<T> 介面
MSDN - IObserverable<T> 介面
題外話:在 Gang of Four 的書中提到一句話就可以貫穿整個 Observer Pattern 的精隨,
"Don't call us. We will call you!",
這句話是 Hollywood Principle。在 Hollywood 的環境中,想成名的演員們都會主動把履歷給星探,但是星探手中有許多許多的演員資料,如果每個演員每天都打電話給星探問說自己有沒有機會,那星探不就被煩死了,所以星探在一開始就會跟演員們說這句話,當星探覺得這個演員OK才會主動聯繫,如果演員一直沒接到電話那就代表GG了。
目的
建立相關物件之間的一對多關係,使得每個物件在改變內部狀態時,其他相關物件會自動被通知更新。動機
一群互相關聯的物件為了維持資料的同步一致性,如果各自都 Keep 住彼此的相依性會造成 High Coupling,造成難以維護與使用。使用時機
- 當一個物件改變狀態時,該物件會不知道更新了多少相關物件(How many)
- 當一個物件改變狀態時,該物件會不知道更新了哪些相關物件(Who)
結構
實作
限量用證券的例子來當 Observer Pattern 的實作範例。在證券市場中,投資人會將關注的股票設為投資標的,也就是只要該股票的價格發生變化,各個投資人所看到的會是最新的價格。可以想像成在券商的分公司裡的電視牆會不斷的更新股票的價格,而投資人會一直盯著電視牆的股票資訊。以下為範例程式碼:
SubjectBase.cs
/// <summary> /// Subject /// </summary> public abstract class SubjectBase { protected List<ObserverBase> Observers { get; set; } public SubjectBase() { Observers = new List<ObserverBase>(); } public void Attach(ObserverBase observer) { Observers.Add(observer); } public void Detach(ObserverBase observer) { Observers.Remove(observer); } /// <summary> /// 通知 /// </summary> protected void Notify() { foreach(var observer in Observers) { observer.Update(this); } } }
ObserverBase.cs
/// <summary> /// Observer /// </summary> public abstract class ObserverBase { protected SubjectBase Subject { get; set; } public abstract void Update(SubjectBase subject); }
StockInfo.cs
/// <summary> /// Concrete Subject /// </summary> public class StockInfo : SubjectBase { public string Name { get; set; } public string IDNo { get; set; } private double _price; public double Price { get { return _price; } set { if(_price != value) { _price = value; Notify(); } } } }
Investor.cs
/// <summary> /// Concrete Observer /// </summary> public class Investor : ObserverBase { public string Name { get; set; } public override void Update(SubjectBase subject) { var stock = subject as StockInfo; Console.WriteLine($"客戶({Name}) - 股票代號: {stock.IDNo}, 股票名稱: {stock.Name}, 現價: {stock.Price}"); } /// <summary> /// 加入關注 /// </summary> public void AddFocus(StockInfo stock) { stock.Attach(this); } }
Program.cs
static void Main(string[] args) { // 初始化 var stock2330 = new StockInfo { Name = "台積電", IDNo = "2330", Price = 180 }; var stock3008 = new StockInfo { Name = "大立光", IDNo = "3008", Price = 3725 }; var investorA = new Investor { Name = "Limited" }; var investorB = new Investor { Name = "Craig" }; // 各自加入關注 investorA.AddFocus(stock2330); investorA.AddFocus(stock3008); investorB.AddFocus(stock2330); // 價格跳動 stock2330.Price += 1; stock3008.Price += 15; /* * 執行結果: * 客戶(Limited) - 股票代號: 2330, 股票名稱: 台積電, 現價: 181 * 客戶(Craig) - 股票代號: 2330, 股票名稱: 台積電, 現價: 181 * 客戶(Limited) - 股票代號: 3008, 股票名稱: 大立光, 現價: 3740 */ Console.ReadLine(); }
範例程式碼要注意的有幾點。因為股價會波動,所以在這裡設計當股價改變時會去呼叫 Notify 方法去觸發 Investor Update。Investor 呼叫 AddFocus 將關注的標的加入,其實 AddFocus 內部實作就是呼叫 StockInfo 實體的 Attach 方法。
從執行結果來看,當台積電股價跳動時,Limited 和 Craig 因為有加入關注,所以會更新到。當大立光股價跳動時,只有 Limited 會更新,因為只有 Limited 有加入關注。
Observer Pattern 是個蠻常使用到的 Design Pattern,.Net 也為此在內部提供了改良過的 Observer Pattern 相關介面與實作方式。IObservable<T> 和 IObserver<T>介面。
.Net Observer Pattern 可說是訂閱者模式,與觀察者模式(Observer Pattern)不同的是,它主要角色為 Subscriber 與 Provider。
Subscriber 對應到 Observer,處於被動更新狀態。Subscriber 實作 IObserver<T>介面,並實作 OnCompleted, OnError, OnNext 方法。OnCompleted 定義為 Provider 對 Subscriber 進行通知完畢後執行的方法;OnError 定義為 Provider 在處理更新資料發生錯誤時執行的方法;OnNext 定義為 Subscriber 接收到 Porvider 派送的更新資訊。
Provider 實作 IObserverable<T>介面,專門負責派送資料的更新給 Subscriber。IObserverable<T>介面提供 Subscribe 方法,這和 Subject 的 Attach 方法類似,就是將此 Subscriber 加入未來派送更新的目標。限量一開始看到 Subscribe 方法回傳的 IDisposable 完全不知道是什麼意思,後來看了 MSDN 的說明後才知道我們還要額外加入一個 Unsubscriber 的類別,就是進行 Subject 的 Detach 的動作。
了解了基本的架構後,就要把這個架構串起來,通常最簡單的方式是在 Provider 類別中寫一個進行資料異動的方法,在資料異動完後去呼叫所有 Subscriber 的 OnNext 進行更新,以下限量將前面的範例程式改寫成 .Net Observer Pattern:
StockInfo.cs
public class StockInfo { public string Name { get; set; } public string IDNo { get; set; } public double Price { get; set; } }
StockTracker.cs
/// <summary> /// Provider /// </summary> public class StockTracker : IObservable<StockInfo> { public StockInfo Stock { get; private set; } private List<IObserver<StockInfo>> Observers { get; set; } = new List<IObserver<StockInfo>>(); public StockTracker(StockInfo stock) { Stock = stock; } public IDisposable Subscribe(IObserver<StockInfo> observer) { if (Observers.Contains(observer) == false) Observers.Add(observer); return new Unsubscriber(Observers, observer); } public void EndAllTrack() { Observers.ForEach(x => { x.OnCompleted(); }); Observers.Clear(); } public void ChangeStockPrice(double gains) { try { if (gains == 0) return; Stock.Price += gains; Observers.ForEach(x => { x.OnNext(Stock); }); } catch (Exception e) { Observers.ForEach(x => { x.OnError(e); }); } } class Unsubscriber : IDisposable { private List<IObserver<StockInfo>> Observers { get; set; } private IObserver<StockInfo> Observer { get; set; } public Unsubscriber(List<IObserver<StockInfo>> observers, IObserver<StockInfo> observer) { Observers = observers; Observer = observer; } public void Dispose() { if (Observer != null && Observers.Contains(Observer)) { Observer.OnCompleted(); Observers.Remove(Observer); } } } }
Investor.cs
/// <summary> /// Subscriber /// </summary> public class Investor : IObserver<StockInfo> { public string Name { get; set; } public Investor(string name) { Name = name; } /// <summary> /// StockTracker 解除派送 /// </summary> public void OnCompleted() { Console.WriteLine("StockTracker 解除派送"); } /// <summary> /// StockTracker 發生錯誤通知 /// </summary> public void OnError(Exception error) { Console.WriteLine(error.Message); } /// <summary> /// StockTracker 發出的新資訊通知 /// </summary> public void OnNext(StockInfo value) { Console.WriteLine($"客戶({Name}) - 股票代號: {value.IDNo}, 股票名稱: {value.Name}, 現價: {value.Price}"); } }
Program.cs
static void Main(string[] args) { var stock2330 = new StockInfo { IDNo = "2330", Name = "台積電", Price = 180 }; var stock3008 = new StockInfo { IDNo = "3008", Name = "大立光", Price = 3725 }; var investorA = new Investor("Limited"); var investorB = new Investor("Craig"); var tracker2330 = new StockTracker(stock2330); tracker2330.Subscribe(investorA); tracker2330.Subscribe(investorB); var tracker3008 = new StockTracker(stock3008); tracker3008.Subscribe(investorA); tracker2330.ChangeStockPrice(1); tracker3008.ChangeStockPrice(15); /* * 執行結果: * 客戶(Limited) - 股票代號: 2330, 股票名稱: 台積電, 現價: 181 * 客戶(Craig) - 股票代號: 2330, 股票名稱: 台積電, 現價: 181 * 客戶(Limited) - 股票代號: 3008, 股票名稱: 大立光, 現價: 3740 */ }
上面修改後的程式碼中, StockInfo 回歸到最原本的"純資料"的結構,新增的 StockTracker 就負責處理更新與通知,而 Investor 一樣負責接收更新的資料並額外加入了錯誤處理與解除派送的事件。
優點
Subject 更新時不須指定接收的 Observer ,更新會自動通知有註冊的 Observer。相關樣式
- Mediator
- Singleton
使用頻率
參考來源:
Design Patterns - Gang of Four
dofactory - Observer
MSDN - IObserver<T> 介面
MSDN - IObserverable<T> 介面
感謝分享 : ) 對於.net寫法更清楚且簡潔
回覆刪除