我們在寫一些即時系統的時候,會遇到一些情況,就是當資料更新時,我的頁面也要即時更新,像是聊天室, 股市資訊, 監控軟體...等。但是要更新的話,總不能每個相關元件的相依性都要一直 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寫法更清楚且簡潔
回覆刪除