【设计模式】用观察者模式对比事件订阅(相机举例)

📷 用观察者模式对比事件订阅(相机举例)

标签: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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈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…