【設計模式】用觀察者模式對比事件訂閱(相機舉例)

📷 用觀察者模式對比事件訂閱(相機舉例)

標簽: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>

最后一問:為啥被觀察者也要定義一個接口?

在觀察者模式中引入**抽象被觀察者接口(如SubjectISubject)**主要有以下幾個原因:

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):

  • 強耦合:觀察者與特定被觀察者綁定,難以復用。
  • 擴展性差:新增被觀察者需修改觀察者代碼。
  • 違反單一職責:被觀察者類既要管理狀態,又要處理觀察者邏輯,職責過重。

總結

抽象被觀察者接口是觀察者模式的核心設計,它通過抽象隔離變化,使系統更靈活、可擴展和易維護。在大型系統中,這種設計模式能顯著降低模塊間的耦合度,提升代碼質量。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/pingmian/85708.shtml
繁體地址,請注明出處:http://hk.pswp.cn/pingmian/85708.shtml
英文地址,請注明出處:http://en.pswp.cn/pingmian/85708.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

【數據分析九:Association Rule】關聯分析

一、數據挖掘定義 數據挖掘&#xff1a; 從大量的數據中挖掘那些令人感興趣的、有用的、隱含的、先前未知的 和可能有用的 模式或知識 &#xff0c;并據此更好的服務人們的生活。 二、四類任務 數據分析有哪些任務&#xff1f; 今天我們來講述其中的關聯分析 三、關聯分析 典…

AWS Security Hub郵件告警設置

問題 需要給AWS Security Hub設置郵件告警。 前提 已經啟用AWS Security Hub。 AWS SNS 創建一個AWS Security Hub告警主題SecurityHub-Topic&#xff0c;如下圖&#xff1a; 創建完成后&#xff0c;訂閱該主題。 AWS EventBridge 設置規則名SecurityHubFindings-Rules…

(OSGB轉3DTiles強大工具)ModelSer--強大的實景三維數據分布式管理平臺

1. ModelSer 能幫我們做什么 1.1 最快速的 osgb 發布 3dtiles 服務 測試的速度大于 10G/分鐘&#xff0c;且速度基本是線性的&#xff08;100G10分鐘&#xff0c;1T100分鐘&#xff09;。支持城市級傾斜數據半天內完成服務發布&#xff0c;并支持數據的單塊更新。 1.2 支持所見…

《HTTP權威指南》 第5-6章 Web服務器和代理

基本Web服務器請求的步驟 1、建立連接 接受一個客戶端連接&#xff0c;或者如果不希望與這個客戶端建立連接&#xff0c;就將其關閉。 處理新連接客戶端主機名識別&#xff1a;反向DNS查找&#xff0c;將IP地址轉換為客戶端主機名過ident確定客戶端用戶&#xff1a;客戶端支持…

微信二次開發,對接智能客服邏輯

接口友情鏈接&#xff0c;點擊即可訪問。 ## 設備創建與復用機制 首次調用/login/getLoginQrCode需傳空appId觸發設備創建&#xff0c;響應返回固定設備ID。后續登錄必須復用此ID以避免風控&#xff08;同一微信號綁定固定設備&#xff09;。設備類型可選ipad/mac&#xff0c;當…

網站并發訪問量達到1萬以上需要注意哪些事項

當網站并發訪問量達到1萬以上時&#xff0c;需要注意以下幾個方面?&#xff1a; ?服務器硬件配置?&#xff1a; ?處理器&#xff08;CPU&#xff09;?&#xff1a;選擇多核、高頻率的CPU&#xff0c;以確保服務器能夠高效地處理大量的請求。?內存&#xff08;RAM&#xf…

二、OpenCV的第一個程序

文章目錄 一、第一個程序&#xff1a;顯示圖片1.1 cv::imread1.2 cv::namedWindow1.3 cv::imshow 二、第二個程序&#xff1a;視頻2.1 cv::VideoCapture 三、加入了滑動條的基本瀏覽窗口 一、第一個程序&#xff1a;顯示圖片 示例&#xff1a;一個簡單的加載并顯示圖像的OpenC…

第14次:商品列表、熱銷商品及詳情

第1步&#xff1a;定義獲取商品列表的視圖類ListView&#xff0c;本視圖中完成了如下功能&#xff1a; 根據商品類別id獲取商品類別信息&#xff0c;并根據類別信息反向查詢到所有的該類別的商品。根據頁號和排序方式兩個參數&#xff0c;獲取某個頁面的商品列表信息。 #good…

基于雙層注意力重加權 LSTM 的中文長文本謠言檢測模型

文章目錄 1.摘要2.介紹3.相關工作3.1 假新聞檢測數據集3.2 假新聞檢測方法3.3 長文本假新聞檢測的挑戰與進展3.4 與現有方法的區別 4.方法4.1 模型結構4.2模型代碼4.3 損失函數與優化方法 5. 實驗5.1 數據集與預處理5.2 實驗設置5.3 實驗結果5.4 對比分析5.5 結果分析與討論 6.…

在 MyBatis 的xml中,什么時候大于號和小于號可以不用轉義

在 MyBatis 中&#xff0c;< 和 > ?在動態 SQL 標簽內部? 無需轉義的功能是在以下版本引入的&#xff1a; &#x1f4cc; 關鍵版本說明 版本支持情況注意事項?MyBatis 3.3.0??? 在 <if>、<where>、<set> 等動態 SQL 標簽內部可直接使用 < 和…

Redis 的穿透、雪崩、擊穿

Redis 的穿透、雪崩、擊穿 1、緩存穿透 定義 緩存穿透是指查詢一個不存在的數據&#xff0c;由于緩存中沒有該數據&#xff0c;每次請求都會直接訪問數據庫&#xff0c;導致數據庫壓力過大 產生原因 惡意攻擊&#xff1a;攻擊者故意請求大量不存在的key&#xff0c;導致請求直…

有道翻譯官手機版:智能翻譯,隨行助手

在當今全球化的時代&#xff0c;語言不再是交流的障礙。無論是學習外語、出國旅游、商務出差還是日常交流&#xff0c;一款高效、準確的翻譯軟件都能成為我們的好幫手。有道翻譯官手機版正是這樣一款功能強大、操作便捷的語言翻譯軟件&#xff0c;它憑借先進的翻譯技術和豐富的…

nuxt3 + vue3 分片上傳組件全解析(大文件分片上傳)

本文將詳細介紹一個基于 Vue.js 的分片上傳組件的設計與實現,該組件支持大文件分片上傳進度顯示等功能。 組件概述 這個上傳組件主要包含以下功能: 支持大文件分片上傳(默認5MB一個分片)支持文件哈希計算,用于文件唯一標識顯示上傳進度(整體和單個文件)支持自定義UI樣…

正則表達式與C++

轉自個人博客 1. 概述 1.1 正則表達式概述 正則表達式&#xff08;Regular Expressions&#xff0c;簡稱 regex&#xff09;是用于匹配文本模式的一種特殊字符序列&#xff0c;其可以用一系列字符來表示出不同文本的對應模式。正則表達式的應用范圍十分廣泛&#xff0c;包括驗…

OpenCV CUDA模塊設備層-----在 GPU上計算反雙曲正切函數atanh()

操作系統&#xff1a;ubuntu22.04 OpenCV版本&#xff1a;OpenCV4.9 IDE:Visual Studio Code 編程語言&#xff1a;C11 算法描述 對輸入的 uchar1 像素值&#xff08;范圍 [0, 255]&#xff09;&#xff0c;先歸一化到 [0.0, 1.0] 浮點區間&#xff0c;然后計算其 反雙曲正切…

搶占西南產業高地:入駐成都芯谷金融中心文化科技產業園的價值

入駐成都芯谷金融中心文化科技產業園&#xff0c;對企業而言具有顯著的戰略價值&#xff0c;主要體現在以下幾個方面&#xff1a; 產業聚集效應與協同發展 產業鏈完善&#xff1a;成都芯谷聚焦集成電路、新型顯示、人工智能等核心產業&#xff0c;入駐企業可享受完善的產業鏈…

領域驅動設計(DDD)【2】之項目啟動與DDD基本開發流程

文章目錄 一 項目背景與目標二 核心需求分析初步需求詳細分析需求總結表 三 DDD核心概念與開發流程領域和領域專家領域驅動設計開發流程 四 潛在擴展需求 一 項目背景與目標 項目定位 開發基于SaaS的企業管理系統&#xff0c;聚焦軟件服務企業的細分市場&#xff0c;功能需求包…

深度融合數智化,百勝軟件聯合華為云加速零售行業轉型升級

當前&#xff0c;企業數字化轉型縱深推進&#xff0c;滿足企業數智化全階段、全場景的需求變得尤為關鍵。為此&#xff0c;華為云攜手上萬家伙伴共同發起第三屆828 B2B企業節&#xff0c;依托云底座為企業數智化供需“架橋”“鋪路”&#xff0c;加速企業智改數轉&#xff0c;助…

《HTTP權威指南》 第4章 連接管理

帶著問題學習&#xff08;通常是面試考點&#xff09; HTTP是如何使用TCP連接的TCP連接的時延、瓶頸及存在的障礙HTTP的優化&#xff0c;包括并行連接、keep-alive&#xff08;持久連接&#xff09;和管道化連接管理連接時應該和不應該做的事 TCP連接 TCP的數據通過IP分組&am…

StartUML入門級使用教程——畫Class類圖

一、破解安裝StartUML StarUML建模工具最新版破解安裝詳細教程https://blog.csdn.net/m0_74146638/article/details/148709643?spm1001.2014.3001.5502 二、類圖實戰 1.主界面 ? 默認打開starUML后&#xff0c;會默認進入類圖模式&#xff0c;各模塊區域功能如下&#x…