📷 用觀察者模式對比事件訂閱(相機舉例)
標簽:WPF、C#、Halcon、設計模式、觀察者模式、事件機制
在日常開發中,我們經常使用 事件機制(Event) 來訂閱圖像采集信號。然而當系統日益復雜,多個模塊同時需要響應圖像變化 時,事件機制常常暴露出諸多痛點:
- 回調函數難以管理
- 拋異常一個掛全掛 ?(詳見下文)
- 解耦能力差,測試困難
- 缺乏靈活擴展能力(過濾、異步、重試等)
于是我重構了圖像采集模塊,采用 觀察者模式(Observer Pattern),讓系統結構更加優雅、可控、可擴展!
🧱 傳統事件訂閱方式的寫法
public class Camera
{public event Action<HObject> ImageGrabbed;public void SimulateGrab(){HObject image = GetImage();ImageGrabbed?.Invoke(image); // 拋異常就“炸鏈”}private HObject GetImage(){HObject image;HOperatorSet.GenEmptyObj(out image);return image;}
}
👇 模擬訂閱多個模塊
camera.ImageGrabbed += img => Console.WriteLine("? UI 模塊收到:" + img);camera.ImageGrabbed += img =>
{Console.WriteLine("? 日志模塊出錯了!");throw new Exception("磁盤已滿");
};camera.ImageGrabbed += img => Console.WriteLine("🔬 圖像分析模塊收到:" + img);
? 為什么事件中一個模塊拋異常,其他模塊就收不到了?
C# 中事件是多播委托(MulticastDelegate),其底層是一個同步執行的委托鏈:
foreach (var handler in ImageGrabbed.GetInvocationList())
{handler.DynamicInvoke("圖像1"); // 如果某個 handler 拋異常,后續的不會執行
}
因此,如果某個訂閱者(例如日志模塊)在處理事件時拋出異常,整個事件的執行鏈條會被中斷,導致后續模塊(如圖像分析模塊)完全無法接收到通知。
📌 這不是因為其他模塊無法處理異常,而是它們根本沒有被調用!
? 引入觀察者模式(命名為 ICameraSubject)
在更實際的項目中,相機的圖像采集往往是通過第三方 SDK 注冊回調函數獲得的。例如:
camera.RegisterImageCallback(OnImageReceived);private void OnImageReceived(byte[] rawBuffer)
{HObject image = ConvertToHObject(rawBuffer);Notify(image);
}
此時,CameraSubject 充當了“驅動層和業務邏輯之間的橋梁”。我們可以將采集到的圖像統一分發給多個“觀察者”,如 UI 展示模塊、日志記錄模塊、圖像分析模塊等。
🔗 接口定義
//觀察者需要實現的接口
public interface ICameraObserver
{void Update(HObject image);
}
//被觀察者需要實現的接口
public interface ICameraSubject
{void Add(ICameraObserver observer);void Remove(ICameraObserver observer);void Notify(HObject image);
}
📷 被觀察者實現(事件發布者)
public class CameraSubject : ICameraSubject
{private readonly List<ICameraObserver> observers = new();public void Add(ICameraObserver observer){observers.Add(observer);}public void Remove(ICameraObserver observer){observers.Remove(observer);}public void Notify(HObject image){foreach (var observer in observers){try{observer.Update(image);}catch (Exception ex){Console.WriteLine($"[異常] {observer.GetType().Name} 處理圖像失敗: {ex.Message}");}}}
}
被觀察者實例定義的Notify()里面會調用所有已添加過的觀察者的Update()
📷 相機驅動模塊實現
public class CameraDriver
{private readonly ICameraSubject cameraSubject;public CameraDriver(ICameraSubject cameraSubject){this.cameraSubject = cameraSubject;}// 假設由 SDK 回調觸發public void OnImageGrabbedFromDriver(byte[] buffer){HObject image = ConvertToHObject(buffer);cameraSubject.Notify(image); // 使用 Subject 通知觀察者}private HObject ConvertToHObject(byte[] buffer){HObject image;HOperatorSet.GenEmptyObj(out image);// 這里添加具體的圖像轉換邏輯return image;}
}
注意,相機驅動模塊里會調用被觀察者對象的Notify方法,就是通知所有的觀察者!
因為:被觀察者的Notify()里面會調用所有已添加過的觀察者的Update()
🖼? 界面模塊如何接收圖像?
我們創建一個 UI 模塊,界面模塊作為觀察者,實現 ICameraObserver
接口:
public class MainWindowObserver : ICameraObserver
{public void Update(HObject image){// 例如綁定到 ImageControl 或刷新控件Console.WriteLine("主界面刷新圖像");}
}
然后在界面初始化時訂閱:
cameraSubject.Add(this);
為什么是cameraSubject.Add(this)?
因為界面模塊實現了接口ICameraObserver
而作為被觀察者實例
cameraSubject管理全部的觀察者,所以這里是:cameraSubject.Add(this); 表示界面訂閱被觀察者將會觸發的事件!!!被cameraSubject收入麾下(觀察者你需要時刻關注我啦)。
cameraSubject 通常會被作為單例注冊到容器中。其他模塊可以通過容器拿到被觀察者的實例對象。
然后,觀察者實現觀察者接口,最后通過被觀察者的實例對象加入自己(this)。
小結
模塊 | 說明 |
---|---|
ICameraObserver | 觀察者接口,定義 Update(HObject image) 方法,用于接收圖像更新通知并處理圖像數據。 |
ICameraSubject | 被觀察者接口,定義 Add , Remove , Notify 方法,用于管理觀察者的注冊、注銷以及事件通知。 |
CameraSubject | 實現 ICameraSubject 接口的具體類,負責維護觀察者列表并通知所有已注冊的觀察者。 |
CameraDriver | 相機驅動類,負責從 SDK 獲取圖像,并通過 CameraSubject 發布事件,觸發觀察者的更新方法。 |
ImageProcessorA | 具體的觀察者實現類,實現了 ICameraObserver 接口,負責執行特定的圖像處理任務(如圖像增強)。 |
ImageProcessorB | 另一個具體的觀察者實現類,也實現了 ICameraObserver 接口,負責執行不同的圖像分析任務(如目標檢測)。 |
然后,cameraSubject 被觀察者實例,如果Add了觀察者實例,那么就相當于該實例訂閱了一個事件。
所以這里也可以感受到,觀察者模式和事件訂閱的差別。
事件訂閱模式是,模塊自己訂閱事件。
而觀察者模式是,有一個第三方的被觀察者實例,把你納入麾下,你就是訂閱了(當然你還得實現觀察者接口)。
總的來說:cameraSubject 被觀察者實例,既存在于相機驅動模塊(需要調用Notify()觸發事件)又存在于處理事件模塊(需要添加自己進去,以及需要實現Update方法!!!)
最后,被觀察者的Notify()里面會調用所有觀察者的Update()。
💡 多種 Notify()
用法示例
那觀察者模式好在哪里?就體現在如下的幾個方面!!!!一些功能事件訂閱的方式是無法實現的。
1?? 異常捕獲(防止“炸鏈”)
public void Notify(HObject image)
{foreach (var observer in observers){try{observer.Update(image);}catch (Exception ex){Console.WriteLine($"? {observer.GetType().Name} 出錯:{ex.Message}");}}
}
2?? 異步處理(提高響應效率)
public async void Notify(HObject image)
{var tasks = observers.Select(o => Task.Run(() =>{try { o.Update(image); }catch (Exception ex){Console.WriteLine($"? {o.GetType().Name} 異步處理失敗:{ex.Message}");}}));await Task.WhenAll(tasks);
}
3?? 條件過濾(比如只處理亮度高的圖像)
public interface IFilterableObserver : ICameraObserver
{bool ShouldHandle(HObject image);
}public void Notify(HObject image)
{foreach (var o in observers){if (o is IFilterableObserver f && !f.ShouldHandle(image))continue;try { o.Update(image); }catch (Exception ex) { Console.WriteLine($"? {o.GetType().Name} 出錯:{ex.Message}"); }}
}
4?? 自動重試(適合網絡上傳、數據庫保存等)
private void SafeUpdate(ICameraObserver observer, HObject image)
{int retry = 3;while (retry-- > 0){try{observer.Update(image);return;}catch (Exception ex){Console.WriteLine($"?? {observer.GetType().Name} 第 {3 - retry} 次失敗: {ex.Message}");Thread.Sleep(100); // 可配置}}Console.WriteLine($"? {observer.GetType().Name} 重試失敗,放棄");
}public void Notify(HObject image)
{foreach (var observer in observers){SafeUpdate(observer, image);}
}
? 實際使用演示
var camera = new CameraSubject();camera.Add(new UIObserver());
camera.Add(new LoggerObserver());
camera.Add(new AnalyzerObserver());
🎯 對比總結
功能/特性 | event 事件 | 觀察者模式 |
---|---|---|
多模塊響應圖像 | ? 支持 | ? 支持 |
異常隔離 | ? 不支持 | ? 支持 |
條件過濾 | ? 不支持 | ? 支持 |
異步支持 | ? 手工復雜 | ? 易擴展 |
重試機制 | ? 不支持 | ? 支持 |
解耦性 | ? 緊耦合 | ? 松耦合 |
測試友好 | ? 不好 mock | ? 好測試 |
📌 小結
事件機制雖然語法簡潔,但在復雜系統中,尤其是圖像采集 + 多模塊處理的系統,劣勢顯現明顯:
- 一旦拋異常,系統整體功能中斷
- 缺乏擴展空間
- 不利于維護和測試
觀察者模式完美解決這些問題,邏輯集中、擴展靈活、結構清晰、異常獨立、安全可靠。
📘 推薦命名實踐
如果你希望語義清晰又不太抽象,推薦使用:
interface ICameraSubject
interface ICameraObserver
如果你計劃封裝為通用框架,可以用:
interface ISubject<T>
interface IObserver<T>
最后一問:為啥被觀察者也要定義一個接口?
在觀察者模式中引入**抽象被觀察者接口(如Subject
或ISubject
)**主要有以下幾個原因:
1. 實現解耦與多態
接口定義了被觀察者的行為契約,使得觀察者只依賴于抽象接口,而非具體實現類。這實現了依賴倒置原則:
- 觀察者只需知道如何注冊/注銷自己,以及如何接收通知(通過接口方法)。
- 具體被觀察者可以自由變化(如從
WeatherData
擴展為StockData
),只要實現相同接口,觀察者無需修改。
示例:
若直接依賴WeatherData
類,后續新增StockData
類時,觀察者代碼需重新修改;而依賴ISubject
接口后,新增被觀察者只需實現該接口即可。
2. 支持多種被觀察者實現
通過接口,可以有多個不同的被觀察者實現,它們可以是:
- 同步通知:如示例中的直接遍歷觀察者列表調用
Update
。 - 異步通知:將通知放入隊列,由單獨線程處理。
- 廣播通知:通過消息中間件發布事件。
示例:
// 不同被觀察者實現相同接口
class WeatherData : ISubject { /* 同步通知 */ }
class AsyncWeatherData : ISubject { /* 異步通知 */ }
3. 遵循開閉原則
接口使系統更具擴展性:
- 新增觀察者:無需修改被觀察者代碼,直接實現
Observer
接口并注冊。 - 新增被觀察者:實現
ISubject
接口,現有觀察者可無縫適配。
4. 便于單元測試
接口便于創建測試替身(如Mock對象):
- 在測試觀察者時,可以用Mock對象模擬被觀察者的行為,隔離外部依賴。
示例(使用Moq框架):
var mockSubject = new Mock<ISubject>();
var observer = new CurrentConditionsDisplay(mockSubject.Object);// 驗證觀察者是否正確注冊
mockSubject.Verify(s => s.RegisterObserver(observer), Times.Once);
5. 避免菱形繼承問題
若使用繼承而非接口,當一個類需要同時成為多個被觀察者的子類時,會引發多重繼承沖突(如C++的菱形繼承問題)。接口允許多實現,規避了這一問題。
對比:無接口的實現問題
若不使用抽象接口,直接讓觀察者依賴具體被觀察者類(如WeatherData
):
- 強耦合:觀察者與特定被觀察者綁定,難以復用。
- 擴展性差:新增被觀察者需修改觀察者代碼。
- 違反單一職責:被觀察者類既要管理狀態,又要處理觀察者邏輯,職責過重。
總結
抽象被觀察者接口是觀察者模式的核心設計,它通過抽象隔離變化,使系統更靈活、可擴展和易維護。在大型系統中,這種設計模式能顯著降低模塊間的耦合度,提升代碼質量。