事件聚合器的跨模块交互、解耦
前言
在现代工业自动化系统中,上位机软件作为关键的控制与监测平台,其复杂性与模块化需求日益增长。使用WPF(Windows Presentation Foundation)构建上位机应用程序,结合事件聚合器(Event Aggregator)模式,可以有效实现跨模块的交互与解耦,提升系统的可维护性和扩展性。本文将详细介绍事件聚合器在WPF工业自动化上位机中的应用,结合具体实现代码,探讨其在模块交互与解耦中的优势
事件聚合器简介
事件聚合器是一种设计模式,用于简化组件或模块之间的通信。它充当发布者和订阅者之间的中介,允许不同的组件彼此通信而无需直接引用。这在大型应用程序中尤为有用,可以降低模块之间的耦合度,提高代码的可维护性和可扩展性。在WPF的MVVM架构中结合事件聚合器,进一步解耦View与ViewModel之间的通信
IEventAggregatorService接口设计
首先,我们定义一个IEventAggregatorService
接口,提供事件订阅、取消订阅和发布的方法
namespace DataMaster.Modules.MAS.EventAggregator {
/// <summary>
/// 事件聚合器服务接口
/// </summary>
public interface IEventAggregatorService {
/// <summary>
/// 订阅事件
/// </summary>
/// <typeparam name="TEvent">要订阅的事件的类型</typeparam>
/// <param name="action">当事件发生时要执行的操作。这是一个接受事件类型作为参数的委托(方法)</param>
void Subscribe<TEvent>(Action<TEvent> action);
/// <summary>
/// 异步订阅事件
/// </summary>
/// <typeparam name="TEvent">要订阅的事件的类型</typeparam>
/// <param name="action">当事件发生时要异步执行的操作。这是一个接受事件类型作为参数的异步委托(方法)</param>
void SubscribeAsync<TEvent>(Func<TEvent, Task> action);
/// <summary>
/// 取消事件订阅
/// </summary>
/// <typeparam name="TEvent">要取消订阅的事件的类型</typeparam>
/// <param name="action">当事件发生时应该执行的操作。这是一个方法委托,
/// 它接受一个 TEvent 类型的参数,这个参数是之前订阅时指定的。
/// 传入的委托应与订阅时使用的委托完全相同,包括其引用
/// </param>
void Unsubscribe<TEvent>(Action<TEvent> action);
/// <summary>
/// 取消异步事件订阅
/// </summary>
/// <typeparam name="TEvent">要取消订阅的事件的类型</typeparam>
/// <param name="action">
/// 当事件发生时应该异步执行的操作。这是一个异步方法委托,
/// 它接受一个 <typeparamref name="TEvent"/> 类型的参数,这个参数是之前订阅时指定的
/// 传入的委托应与订阅时使用的委托完全相同,包括其引用
/// </param>
void UnsubscribeAsync<TEvent>(Func<TEvent, Task> action);
/// <summary>
/// 发布一个事件,通知所有订阅了该事件的订阅者
/// </summary>
/// <typeparam name="TEvent">要发布的事件的类型</typeparam>
/// <param name="eventToPublish">要发布的事件的实例</param>
void Publish<TEvent>(TEvent eventToPublish);
/// <summary>
/// 异步发布一个事件,通知所有订阅了该事件的异步订阅者
/// </summary>
/// <remarks>
/// Fire-and-Forget 模式。在单独的后台线程中启动所有订阅者的异步处理任务后立即返回,不等待这些任务完成
/// </remarks>
/// <typeparam name="TEvent">要发布的事件的类型</typeparam>
/// <param name="eventToPublish">要发布的事件的实例</param>
/// <returns>返回一个已完成的异步任务结果,表示事件发布操作已经启动</returns>
Task PublishAsync<TEvent>(TEvent eventToPublish);
}
}
接口方法解析
- Subscribe:用于订阅特定类型的事件,提供一个回调函数,当事件发生时触发
- SubscribeAsync:用于异步订阅事件,适用于需要异步处理的场景
- Unsubscribe/UnsubscribeAsync:用于取消订阅,防止内存泄漏和无效调用
- Publish:同步发布事件,通知所有订阅者
- PublishAsync:异步发布事件,适用于需要异步处理的订阅者
IEventAggregatorService接口实现
接下来,我们实现IEventAggregatorService
接口,创建EventAggregatorServices
类
using DataMaster.Modules.MAS.Loggers;
using System.Collections.Concurrent;
namespace DataMaster.Modules.MAS.EventAggregator {
/// <summary>
/// 事件聚合器服务,用于管理事件的订阅和发布
/// </summary>
internal class EventAggregatorServices : IEventAggregatorService {
private readonly ConcurrentDictionary<Type, List<Delegate>> _subscribers = new();
public void Subscribe<TEvent>(Action<TEvent> action) {
List<Delegate> delegateList = _subscribers.GetOrAdd(typeof(TEvent), _ => new List<Delegate>());
lock (delegateList) {
if (!delegateList.Contains(action)) {
delegateList.Add(action);
}
}
}
public void SubscribeAsync<TEvent>(Func<TEvent, Task> action) {
var delegateList = _subscribers.GetOrAdd(typeof(TEvent), _ => new List<Delegate>());
lock (delegateList) {
if (!delegateList.Contains(action)) {
delegateList.Add(action);
}
}
}
public void Unsubscribe<TEvent>(Action<TEvent> action) {
if (!_subscribers.TryGetValue(typeof(TEvent), out List<Delegate>? delegateList)) {
return;
}
lock (delegateList) {
delegateList.Remove(action);
if (delegateList.Count == 0) {
_subscribers.TryRemove(typeof(TEvent), out _);
}
}
}
public void UnsubscribeAsync<TEvent>(Func<TEvent, Task> action) {
if (!_subscribers.TryGetValue(typeof(TEvent), out var delegateList)) {
return;
}
lock (delegateList) {
delegateList.Remove(action);
if (delegateList.Count == 0) {
_subscribers.TryRemove(typeof(TEvent), out _);
}
}
}
public void Publish<TEvent>(TEvent eventToPublish) {
if (!_subscribers.TryGetValue(typeof(TEvent), out List<Delegate>? delegates)) {
return;
}
List<Action<TEvent>> actions = delegates.OfType<Action<TEvent>>().ToList();
foreach (Action<TEvent> action in actions) {
try {
action(eventToPublish);
} catch (Exception ex) {
LoggerHelper.System.Error(ex, $"发布事件 {GetFullTypeName(typeof(TEvent))} 时发生错误:{ex.Message}");
}
}
}
public Task PublishAsync<TEvent>(TEvent eventToPublish) {
if (!_subscribers.TryGetValue(typeof(TEvent), out List<Delegate>? delegates)) {
return Task.CompletedTask;
}
List<Func<TEvent, Task>> asyncActions = delegates.OfType<Func<TEvent, Task>>().ToList();
Task.Run(async () => {
List<Task> tasks = new List<Task>();
foreach (var action in asyncActions) {
try {
tasks.Add(action(eventToPublish));
} catch (Exception ex) {
LoggerHelper.System.Error(ex, $"异步处理事件 {GetFullTypeName(typeof(TEvent))} 时发生错误:{ex.Message}");
}
}
try {
await Task.WhenAll(tasks);
} catch (Exception ex) {
LoggerHelper.System.Error(ex, $"事件 {GetFullTypeName(typeof(TEvent))} 的一个或多个异步操作失败:{ex.Message}");
}
});
return Task.CompletedTask;
}
/// <summary>
/// 获取完整的类型限定名称
/// </summary>
private static string GetFullTypeName(Type type) {
if (!type.IsGenericType) {
return type.Name;
}
string typeName = type.GetGenericTypeDefinition().Name;
typeName = typeName[..typeName.IndexOf('`')];
string[] typeParameters = type.GetGenericArguments().Select(GetFullTypeName).ToArray();
return $"{typeName}<{string.Join(", ", typeParameters)}>";
}
}
}
实现细节解析
- 线程安全的订阅管理:使用
ConcurrentDictionary
和lock
确保订阅列表的线程安全 - 订阅和取消订阅:在添加或移除订阅时,检查是否已存在,防止重复添加或错误移除
- 事件发布:在发布事件时,遍历所有订阅者并调用其回调方法,使用
try-catch
捕获并记录异常,防止单个订阅者的错误影响整个发布过程 - 异步事件发布:使用
Task.Run
启动异步任务,不阻塞调用线程,使用Task.WhenAll
等待所有异步操作完成,并捕获可能的异常
应用示例(使用依赖注入)
定义事件类型
public class DataUpdatedEvent {
public int DataId { get; set; }
public string NewValue { get; set; }
}
配置依赖注入容器
在应用程序启动时,配置依赖注入容器,将IEventAggregatorService
注册为单例(Singleton),以确保所有模块共享同一个实例
// 注册事件聚合器服务为单例
services.AddSingleton<IEventAggregatorService, EventAggregatorServices>();
在模块中订阅事件
在需要订阅事件的模块中,通过构造函数注入获取IEventAggregatorService
的实例
public class DataSubscriber {
private readonly IEventAggregatorService _eventAggregatorService;
// 构造函数注入事件聚合器服务
public DataSubscriber(IEventAggregatorService eventAggregatorService) {
_eventAggregatorService = eventAggregatorService;
_eventAggregatorService.Subscribe<DataUpdatedEvent>(OnDataUpdated);
}
private void OnDataUpdated(DataUpdatedEvent evt) {
// 处理数据更新逻辑
Console.WriteLine($"Data with ID {evt.DataId} updated to {evt.NewValue}");
}
}
在模块中发布事件
在需要发布事件的模块中,同样通过依赖注入获取IEventAggregatorService
的实例
public class DataPublisher {
private readonly IEventAggregatorService _eventAggregatorService;
// 构造函数注入事件聚合器服务
public DataPublisher(IEventAggregatorService eventAggregatorService) {
_eventAggregatorService = eventAggregatorService;
}
public void UpdateData() {
var dataEvent = new DataUpdatedEvent { DataId = 1, NewValue = "New Data" };
// 发布事件
_eventAggregatorService.Publish(dataEvent);
}
}
注意事项
在使用事件聚合器模式时,取消事件订阅(Unsubscribe)是一个至关重要的步骤,尤其在以下几个方面:
一、防止内存泄漏:
当模块订阅了某个事件后,事件聚合器会持有对该模块回调方法的引用。如果模块在不需要接收该事件时没有取消订阅,事件聚合器将持续持有该引用,导致垃圾回收器无法回收模块实例,从而引发内存泄漏
二、避免无效调用:
如果模块已经卸载或不再需要处理某些事件,但仍然保留订阅,事件聚合器在发布事件时会尝试调用这些无效的回调方法,可能导致异常或不可预期的行为
三、提升应用性能:
保持不必要的订阅会增加事件发布时的处理开销,尤其是在高频事件发布的场景下。通过取消不必要的订阅,可以减少事件聚合器需要遍历和调用的回调方法数量,从而提升整体性能
为什么使用依赖注入
-
单一实例管理:通过将
IEventAggregatorService
注册为单例,我们确保整个应用程序中只有一个事件聚合器实例,所有模块都共享它 -
解耦合:模块不需要知道事件聚合器的实现细节,只需要依赖于
IEventAggregatorService
接口。这样,当我们需要更换事件聚合器的实现时,不需要修改模块的代码 -
可测试性:在单元测试中,我们可以使用模拟(Mock)的
IEventAggregatorService
来进行测试,而不依赖于具体的实现 -
生命周期管理:依赖注入容器负责对象的创建和销毁,我们不需要手动管理这些对象的生命周期
待扩展功能
- 支持事件类型过滤
- 事件的优先级处理
结语
可以在不引入额外的框架下实现自己的事件聚合器,在大型应用程序中,依赖注入是实现模块化和解耦合的重要手段。结合事件聚合器模式,我们可以有效地管理模块之间的通信,而不增加模块间的耦合度。通过使用依赖注入,我们的代码变得更加清晰、可维护,并且更容易进行单元测试。是现代软件架构设计中不可或缺的组件之一