Design Pattern (G4) - Observer Pattern

我們在寫一些即時系統的時候,會遇到一些情況,就是當資料更新時,我的頁面也要即時更新,像是聊天室, 股市資訊, 監控軟體...等。但是要更新的話,總不能每個相關元件的相依性都要一直 Keep 住吧,如果未來加了一堆相關元件不就改到死。這種一對多相依性的情況下就可以套用 Observer Pattern (觀察者模式),減少物件的相依性,並可自動更新。


題外話:在 Gang of Four 的書中提到一句話就可以貫穿整個 Observer Pattern 的精隨,
"Don't call us. We will call you!"
這句話是 Hollywood Principle。在 Hollywood 的環境中,想成名的演員們都會主動把履歷給星探,但是星探手中有許多許多的演員資料,如果每個演員每天都打電話給星探問說自己有沒有機會,那星探不就被煩死了,所以星探在一開始就會跟演員們說這句話,當星探覺得這個演員OK才會主動聯繫,如果演員一直沒接到電話那就代表GG了。

目的

建立相關物件之間的一對多關係,使得每個物件在改變內部狀態時,其他相關物件會自動被通知更新。

動機

一群互相關聯的物件為了維持資料的同步一致性,如果各自都 Keep 住彼此的相依性會造成 High Coupling,造成難以維護與使用。

使用時機

  1. 當一個物件改變狀態時,該物件會不知道更新了多少相關物件(How many)
  2. 當一個物件改變狀態時,該物件會不知道更新了哪些相關物件(Who)

結構

在 Observer Pattern 中的角色主要分為 Subject (被觀察者)與 Observer (觀察者)。對於 Subject 來說,他只要知道有多少觀察他的 Observer,不需要知道各個 Observer 的類型是什麼;而對於 Observer 來說,他只需要向 Subject 註冊,接著只要負責接收到通知更新就好了。




實作

限量用證券的例子來當 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。


相關樣式

  1. Mediator
  2. Singleton

使用頻率











參考來源:
Design Patterns - Gang of Four 
dofactory - Observer
MSDN - IObserver<T> 介面
MSDN - IObserverable<T> 介面






留言

張貼留言