觀察者模式定義了物件之間的一種一對多依賴關係,當一個物件(主題/Subject)的狀態發生改變時,所有依賴於它的物件(觀察者/Observer)都會得到通知並自動更新。這是 GoF《設計模式》中經典的行為型模式之一,廣泛應用於事件驅動系統、訊息通知機制以及 MVC 架構中的 View 更新。
想像一場拍賣會:拍賣師是主題,競標者是觀察者。每當有新出價(狀態變更),拍賣師會廣播給所有競標者,他們據此決定是否跟進。這完美詮釋了「一對多、鬆耦合、自動通知」的核心精神。
假設我們要監控微軟股價變化並通知投資者。初版代碼直接在 Microsoft 類別中持有 Investor 實例:
public class Microsoft {
private Investor _investor;
public void Update() { _investor.SendData(this); }
// ... 屬性省略
}
public class Investor {
public void SendData(Microsoft ms) {
Console.WriteLine("Notified {0} of {1}'s change to {2:C}", _name, ms.Symbol, ms.Price);
}
}
問題:Microsoft 與 Investor 雙向依賴,新增通知對象(如手機簡訊 Mobile)必須修改 Microsoft 類別,嚴重違反開放-封閉原則。
定義 IObserver 介面,讓 Microsoft 依賴抽象而非具體類別:
public interface IObserver { void SendData(Microsoft ms); }
public class Investor : IObserver { /* 實作 */ }
public class Microsoft {
private IObserver _investor;
public void Update() { _investor.SendData(this); }
// ...
}
雖然降低了耦合,但仍只能支援單一觀察者,新增類型仍需修改主題類別。
在 Microsoft 中改用 List<IObserver>,提供註冊/移除方法:
public class Microsoft {
private List<IObserver> _observers = new List<IObserver>();
public void AddObserver(IObserver o) { _observers.Add(o); }
public void RemoveObserver(IObserver o) { _observers.Remove(o); }
public void Update() {
foreach (var ob in _observers) ob.SendData(this);
}
}
現在支援多觀察者,且主題不再依賴具體觀察者類別。
將共同邏輯上移至抽象基類 Stock,具體股票(Microsoft、IBM 等)繼承即可:
public abstract class Stock {
private List<IObserver> _observers = new List<IObserver>();
protected Stock(string symbol, double price) { Symbol = symbol; Price = price; }
public void AddObserver(IObserver o) { _observers.Add(o); }
public void RemoveObserver(IObserver o) { _observers.Remove(o); }
public void Update() {
foreach (var ob in _observers) ob.SendData(this);
}
public string Symbol { get; protected set; }
public double Price { get; protected set; }
}
public class Microsoft : Stock {
public Microsoft(string symbol, double price) : base(symbol, price) { }
}
public interface IObserver { void SendData(Stock stock); }
public class Investor : IObserver { /* 實作 */ }
此時已具備完整觀察者模式結構:Stock 是 Subject,IObserver 是 Observer 介面,具體觀察者實作介面,主題透過集合管理觀察者並統一通知。
主題主動將資料作為參數「推」給觀察者(如上例 ob.SendData(this))。優點:觀察者被動接收,與主題鬆耦合。缺點:所有觀察者收到相同資料,無法按需取用;參數變動會影響所有觀察者。
通知時不帶參數(ob.SendData()),觀察者自行向主題「拉」取所需資料。需讓觀察者持有主題參考:
public interface IObserver { void SendData(); }
public class Investor : IObserver {
private Stock _stock;
public Investor(string name, Stock stock) { _name = name; _stock = stock; }
public void SendData() {
Console.WriteLine("Notified {0} of {1}'s change to {2:C}", _name, _stock.Symbol, _stock.Price);
}
}
優點:觀察者可按需取用資料。缺點:觀察者與主題耦合度提高,可透過抽象主題介面緩解。
.NET 內建的事件/委派機制本質就是觀察者模式的語言級實現,更簡潔優雅:
public delegate void NotifyEventHandler(object sender);
public class Stock {
public event NotifyEventHandler NotifyEvent;
public void Update() { OnNotifyChange(); }
protected virtual void OnNotifyChange() { NotifyEvent?.Invoke(this); }
// ... Symbol, Price 屬性
}
public class Investor {
public void SendData(object obj) {
if (obj is Stock stock)
Console.WriteLine("Notified {0} of {1}'s change to {2:C}", _name, stock.Symbol, stock.Price);
}
}
// 使用
var stock = new Stock("Microsoft", 120.00);
var investor = new Investor("Jom");
stock.NotifyEvent += investor.SendData;
stock.Update();
委派扮演 Observer 介面,事件扮演 Subject 的註冊/通知機制,完全符合開放-封閉原則,且語法原生支援多播。
適用性:
觀察者模式透過「主題-觀察者」間接依賴取代直接耦合,極大提升系統可維護性與擴展性。在 .NET 中,優先考慮使用事件/委派原生實現;若需跨語言、跨進程或更精細控制通知邏輯,則採用經典介面ベース實作。掌握推/拉兩種模式權衡,能根據業務場景做出最佳設計決策。