1. 介紹
想像你有一個物件在執行某些動作——比如一個按鈕或我們的 Worker。同時有許多其他物件需要對這些動作做出反應。如果在 Worker 類裡硬編寫所有可能的「listener」,維護這段程式會變成噩夢:對訂閱者清單的任何修改都需要改動 Worker 本身。
這會違反開放-封閉原則 (OCP),被視為不良的架構習慣。
Observer 模式:總體概念
模式 "觀察者" (Observer) 解決了這個問題。它讓發布者(publisher)可以通知任意數量的感興趣的 listener,而不需要知道那些 listener 是誰、在做什麼。發布者只是「廣播」,有興趣的就各自處理。
類比:訂閱新聞信。編輯室或頻道(發布者)寄出新郵件,所有訂閱者(觀察者)都收到。編輯室不需要知道那些人是誰,也不需要知道。
有趣的事實:Observer 非常常見,是 GoF(四人幫)設計模式之一。
Observer 在 C#:用事件和 delegate 實作
在 C# 裡,Observer 模式已透過事件和 delegate 機制「內建」。事件 是擴充點,外部可以訂閱不同的 handler。相較於手動維護訂閱者清單,語言的事件機制幫你處理這些。下面先看「手動」實作,再看事件版。
2. 傳統的 Observer 實作(不使用事件)
假如語言沒有事件,可能會長這樣:
// 觀察者介面
public interface IObserver
{
void Update(string message);
}
// 發布者
public class Worker
{
private List<IObserver> observers = new List<IObserver>();
public void Subscribe(IObserver observer)
{
observers.Add(observer);
}
public void Unsubscribe(IObserver observer)
{
observers.Remove(observer);
}
public void DoWork()
{
Console.WriteLine("Worker 工作中...");
NotifyObservers("工作完成!");
}
private void NotifyObservers(string message)
{
foreach (var observer in observers)
{
observer.Update(message);
}
}
}
// 具體觀察者
public class WorkListener : IObserver
{
public void Update(string message)
{
Console.WriteLine($"
WorkListener 收到訊息: {message}");
}
}
初始化:
var worker = new Worker();
var listener = new WorkListener();
worker.Subscribe(listener);
worker.DoWork();
筆記:這裡訂閱者清單(List<IObserver> observers)是手動維護的,訂閱/退訂是明確的方法 Subscribe/Unsubscribe。
3. 事件與 delegate —— C# 的高階 Observer 實作
我們可以用事件把同樣的功能做得更簡潔、更優雅,這就是 C# 風格的 Observer:
public class Worker
{
public event EventHandler<WorkCompletedEventArgs>? WorkCompleted;
public void DoWork()
{
Console.WriteLine("Worker 工作中...");
OnWorkCompleted("工作完成!");
}
protected virtual void OnWorkCompleted(string message)
{
WorkCompleted?.Invoke(this, new WorkCompletedEventArgs { Message = message });
}
}
public class WorkCompletedEventArgs : EventArgs
{
public string Message { get; set; }
}
public class WorkListener
{
public void OnWorkCompleted(object? sender, WorkCompletedEventArgs e)
{
Console.WriteLine($"WorkListener 收到訊息: {e.Message}");
}
}
// 訂閱:
var worker = new Worker();
var listener = new WorkListener();
worker.WorkCompleted += listener.OnWorkCompleted;
worker.DoWork();
這種做法的優點:
- 不用手動維護訂閱者清單。
- 事件的功能完整:多重訂閱、退訂、lambda 等都可用。
- 安全性:只有發布者能觸發事件。
- 低耦合:發布者不知道任何 listener 的實作細節。
4. 觀察者如何融入應用程式
把 Observer 模式整合進我們的 console 程式。讓 Worker 可以有任意數量的 handler,針對完成工作做不同反應:有人寫到 console、有人計算完成次數、有人寄信說 "老闆! 都做好了!"。
用示例擴充程式碼
// 第二個 listener:計數器
public class WorkCounter
{
public int Count { get; private set; }
public void OnWorkCompleted(object? sender, WorkCompletedEventArgs e)
{
Count++;
Console.WriteLine($"工作已記錄。總共: {Count} 完成。");
}
}
// 建立物件
var worker = new Worker();
var listener = new WorkListener();
var counter = new WorkCounter();
// 兩個訂閱
worker.WorkCompleted += listener.OnWorkCompleted;
worker.WorkCompleted += counter.OnWorkCompleted;
// 模擬多次工作
worker.DoWork();
worker.DoWork();
// Output:
// Worker 工作中...
// WorkListener 收到訊息: 工作完成!
// 工作已記錄。總共: 1 完成。
// Worker 工作中...
// WorkListener 收到訊息: 工作完成!
// 工作已記錄。總共: 2 完成。
這樣一來,你可以按需加入觀察者,而不用改動 Worker 的任何程式碼。Worker 保持不變,系統行為透過訂閱者延伸。
5. 實用小細節
真實範例:介面和 GUI 中的 Observer
Observer 是所有 GUI framework 的基礎。在 Windows Forms 或 WPF 中,按鈕的按下會觸發 Click 事件。你寫 handler(觀察者)來回應事件——無論是你的 Button 類或 .NET 庫都不需要知道你的訂閱者是誰。
// 在 WPF 或 WinForms(大致範例)
myButton.Click += (s, e) => MessageBox.Show("使用者按了按鈕!");
Observer 在實際專案中的應用
- 使用者介面(回應點擊、變更、定時器等)。
- 通知系統與事件系統。
- 可擴充系統的 plugin(核心產生事件,擴充套件訂閱)。
- 分散式系統與遊戲引擎(弱耦合的反應鏈)。
總之,如果你需要做一個可擴充的系統,讓某些部分可以對其他部分的變更作出反應——Observer 必備!
7. 特性、常見錯誤與預防
使用 Observer 時可能遇到的問題
記憶體洩漏。如果訂閱者訂閱了事件但沒有退訂(尤其是長生命週期的物件),GC 無法回收該物件,因為發布者仍透過事件 delegate 持有參考。當訂閱者不再需要而發布者還活著時,這會很危險。
重複訂閱。如果同一個 handler 被訂閱了兩次,它會被呼叫兩次——會導致重複行為和意外結果。
處理者內的例外。如果某個 handler 拋出例外,之後的訂閱者可能不會被呼叫。請讓 handler 具備韌性,必要時在調用時用 try-catch 包起來,確保其他訂閱者仍能執行。
常見的洩漏圖示
flowchart LR
Publisher["發布者
(Worker)"] -- 事件 --> ObserverA["聽眾 A (還活著!)"]
Publisher -- 事件 --> ObserverB["聽眾 B (洩漏: 忘記退訂)"]
GO TO FULL VERSION