当前位置: 首页 > news >正文

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

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

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

  • 强耦合:观察者与特定被观察者绑定,难以复用。
  • 扩展性差:新增被观察者需修改观察者代码。
  • 违反单一职责:被观察者类既要管理状态,又要处理观察者逻辑,职责过重。

总结

抽象被观察者接口是观察者模式的核心设计,它通过抽象隔离变化,使系统更灵活、可扩展和易维护。在大型系统中,这种设计模式能显著降低模块间的耦合度,提升代码质量。

相关文章:

  • 【K8S】详解NodePort 和 ClusterIP
  • 【K8S】详解Labels​​ 和 ​​Annotations
  • Android 应用多语言与系统语言偏好设置指南
  • 容器运行时保护:用Falco构建云原生安全防线
  • 简单理解HTTP/HTTPS协议
  • 基于 Apache POI 实现的 Word 操作工具类
  • AI公文写作,推荐AI材料星!
  • vue3 动态绑定 ref 并获取其 dom
  • Python 自动化运维与DevOps实践
  • Docker如何实现容器之间的通信
  • 李沐动手深度学习(pycharm中运行笔记)——12.权重衰退
  • GitLab CVE-2025-4278 安全漏洞解决方案
  • openLayers切换基于高德、天地图切换矢量、影像、地形图层
  • 使用NPOI库导出多个Excel并压缩zip包
  • 【跨界新视野】信号处理遇上VR/AR:下一代沉浸体验的核心技术与您的发表蓝海
  • STUN (Session Traversal Utilities for NAT) 服务器是一种网络协议
  • Linux 上 Ollama 部署为系统服务
  • 飞算JavaAI编程助手在IDEA中的安装教程:本地安装、离线安装、在线安装方法大全
  • 【Redis】Redis核心探秘:数据类型的编码实现与高速访问之道
  • 114. 二叉树展开为链表 --- 头插法(js)
  • 王晴儿网站建设/西安网站建设排名
  • 短剧cps分销平台官网/北京seo网站开发
  • 长春百度网站排名优化/男生最喜欢的浏览器
  • 网站手机自动跳转/教育培训机构网站
  • 深圳罗湖做网站公司/百度问问我要提问
  • 网站未备案可以上线吗/金华网站建设