标签: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 抛异常,后续的不会执行
}
因此,如果某个订阅者(例如日志模块)在处理事件时抛出异常,整个事件的执行链条会被中断,导致后续模块(如图像分析模块)完全无法接收到通知。
这不是因为其他模块无法处理异常,而是它们根本没有被调用!
在更实际的项目中,相机的图像采集往往是通过第三方 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);
因为界面模块实现了接口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()
用法示例那观察者模式好在哪里?就体现在如下的几个方面!!!!一些功能事件订阅的方式是无法实现的。
public void Notify(HObject image)
{
foreach (var observer in observers)
{
try
{
observer.Update(image);
}
catch (Exception ex)
{
Console.WriteLine($"❌ {observer.GetType().Name} 出错:{ex.Message}");
}
}
}
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);
}
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}"); }
}
}
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
)**主要有以下几个原因:
接口定义了被观察者的行为契约,使得观察者只依赖于抽象接口,而非具体实现类。这实现了依赖倒置原则:
WeatherData
扩展为StockData
),只要实现相同接口,观察者无需修改。示例:
若直接依赖WeatherData
类,后续新增StockData
类时,观察者代码需重新修改;而依赖ISubject
接口后,新增被观察者只需实现该接口即可。
通过接口,可以有多个不同的被观察者实现,它们可以是:
Update
。示例:
// 不同被观察者实现相同接口
class WeatherData : ISubject { /* 同步通知 */ }
class AsyncWeatherData : ISubject { /* 异步通知 */ }
接口使系统更具扩展性:
Observer
接口并注册。ISubject
接口,现有观察者可无缝适配。接口便于创建测试替身(如Mock对象):
示例(使用Moq框架):
var mockSubject = new Mock<ISubject>();
var observer = new CurrentConditionsDisplay(mockSubject.Object);
// 验证观察者是否正确注册
mockSubject.Verify(s => s.RegisterObserver(observer), Times.Once);
若使用继承而非接口,当一个类需要同时成为多个被观察者的子类时,会引发多重继承冲突(如C++的菱形继承问题)。接口允许多实现,规避了这一问题。
若不使用抽象接口,直接让观察者依赖具体被观察者类(如WeatherData
):
抽象被观察者接口是观察者模式的核心设计,它通过抽象隔离变化,使系统更灵活、可扩展和易维护。在大型系统中,这种设计模式能显著降低模块间的耦合度,提升代码质量。