注册网站是哪个部门,wordpress 商业模板,中国企业排行,会计专业简历制作#x1f4f7; 用观察者模式对比事件订阅(相机举例) 标签#xff1a;WPF、C#、Halcon、设计模式、观察者模式、事件机制 在日常开发中#xff0c;我们经常使用 事件机制#xff08;Event#xff09; 来订阅图像采集信号。然而当系统日益复杂#xff0c;多个模块同时需要响… 用观察者模式对比事件订阅(相机举例) 标签WPF、C#、Halcon、设计模式、观察者模式、事件机制 在日常开发中我们经常使用 事件机制Event 来订阅图像采集信号。然而当系统日益复杂多个模块同时需要响应图像变化 时事件机制常常暴露出诸多痛点
回调函数难以管理抛异常一个挂全挂 ❌详见下文解耦能力差测试困难缺乏灵活扩展能力过滤、异步、重试等
于是我重构了图像采集模块采用 观察者模式Observer Pattern让系统结构更加优雅、可控、可扩展 传统事件订阅方式的写法
public class Camera
{public event ActionHObject 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 ListICameraObserver 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 ISubjectT
interface IObserverT最后一问为啥被观察者也要定义一个接口
在观察者模式中引入**抽象被观察者接口如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 MockISubject();
var observer new CurrentConditionsDisplay(mockSubject.Object);// 验证观察者是否正确注册
mockSubject.Verify(s s.RegisterObserver(observer), Times.Once);5. 避免菱形继承问题
若使用继承而非接口当一个类需要同时成为多个被观察者的子类时会引发多重继承冲突如C的菱形继承问题。接口允许多实现规避了这一问题。
对比无接口的实现问题
若不使用抽象接口直接让观察者依赖具体被观察者类如WeatherData
强耦合观察者与特定被观察者绑定难以复用。扩展性差新增被观察者需修改观察者代码。违反单一职责被观察者类既要管理状态又要处理观察者逻辑职责过重。
总结
抽象被观察者接口是观察者模式的核心设计它通过抽象隔离变化使系统更灵活、可扩展和易维护。在大型系统中这种设计模式能显著降低模块间的耦合度提升代码质量。