18、观察者模式实现(三):基于事件驱动(Event)的观察者模式实现
好,我们继续聊观察者模式。前面两章我们分别用传统方式和委托方式实现了观察者模式。今天我要讲的是我个人最常用、也最推荐的一种实现——基于事件驱动(Event)的方式。
为什么我偏爱事件驱动?说白了,它更贴近现实世界的运作方式。你想想看,现实中的事件通知,从来不是「观察者主动去问」,而是「事件发生了,通知自动送达」。事件驱动模式,就是把这种自然逻辑映射到代码里。
事件驱动的核心思想
事件驱动观察者模式,本质上是对传统观察者模式的一次封装升级。它的核心组件有三个:
- 事件(Event):描述「发生了什么」的数据对象
- 事件源(Event Source):产生事件的对象,也就是被观察者
- 事件监听器(Event Listener):对特定事件感兴趣的对象,也就是观察者
我在项目中遇到过不少团队,把事件驱动和观察者模式当成两回事。其实它们是一脉相承的——事件驱动是观察者模式的一种高级实现形态。
一个完整的例子:订单状态变更通知
我们用一个电商订单的例子来说明。假设系统里有订单模块、物流模块、短信模块和日志模块。当订单状态变更时,后面三个模块都需要做出响应。
先定义事件基类:
// 事件基类
public class Event
{
public DateTime OccurredAt { get; set; } = DateTime.Now;
public string EventName { get; set; }
}
// 订单状态变更事件
public class OrderStatusChangedEvent : Event
{
public string OrderId { get; set; }
public string OldStatus { get; set; }
public string NewStatus { get; set; }
public OrderStatusChangedEvent(string orderId, string oldStatus, string newStatus)
{
EventName = "OrderStatusChanged";
OrderId = orderId;
OldStatus = oldStatus;
NewStatus = newStatus;
}
}
嗯,这里要注意:事件类不仅仅是传递数据,它还携带了事件发生的时间戳和事件名称。这个设计细节,在排查问题时特别有用。
事件总线:核心调度器
接下来是事件总线,它是整个事件驱动架构的心脏:
public class EventBus
{
private readonly Dictionary<Type, List<Action<Event>>> _handlers = new();
// 订阅事件
public void Subscribe<T>(Action<T> handler) where T : Event
{
var eventType = typeof(T);
if (!_handlers.ContainsKey(eventType))
{
_handlers[eventType] = new List<Action<Event>>();
}
_handlers[eventType].Add(e => handler((T)e));
}
// 发布事件
public void Publish<T>(T eventData) where T : Event
{
var eventType = typeof(T);
if (_handlers.ContainsKey(eventType))
{
foreach (var handler in _handlers[eventType])
{
handler(eventData);
}
}
}
}
这段代码看起来简单,但它是整个模式的基石。我曾经在一个微服务项目里,用类似的结构管理了上百种事件类型,运行了两年多没出过问题。
各模块订阅事件
现在让各个模块成为事件的监听者:
// 物流模块
public class LogisticsService
{
public void OnOrderStatusChanged(OrderStatusChangedEvent e)
{
if (e.NewStatus == "Shipped")
{
Console.WriteLine($"[物流] 订单 {e.OrderId} 已发货,安排配送");
}
}
}
// 短信模块
public class SmsService
{
public void OnOrderStatusChanged(OrderStatusChangedEvent e)
{
Console.WriteLine($"[短信] 发送通知:订单 {e.OrderId} 状态变更为 {e.NewStatus}");
}
}
// 日志模块
public class LogService
{
public void OnOrderStatusChanged(OrderStatusChangedEvent e)
{
Console.WriteLine($"[日志] 记录:{e.OccurredAt} 订单 {e.OrderId} 从 {e.OldStatus} 变为 {e.NewStatus}");
}
}
把它们组装起来
public class OrderService
{
private readonly EventBus _eventBus;
public OrderService(EventBus eventBus)
{
_eventBus = eventBus;
}
public void ChangeOrderStatus(string orderId, string newStatus)
{
string oldStatus = "Pending"; // 假设从数据库获取
// 业务逻辑...
// 发布事件
var evt = new OrderStatusChangedEvent(orderId, oldStatus, newStatus);
_eventBus.Publish(evt);
}
}
// 使用示例
var eventBus = new EventBus();
var logistics = new LogisticsService();
var sms = new SmsService();
var log = new LogService();
// 订阅
eventBus.Subscribe<OrderStatusChangedEvent>(logistics.OnOrderStatusChanged);
eventBus.Subscribe<OrderStatusChangedEvent>(sms.OnOrderStatusChanged);
eventBus.Subscribe<OrderStatusChangedEvent>(log.OnOrderStatusChanged);
// 触发
var orderService = new OrderService(eventBus);
orderService.ChangeOrderStatus("ORD-001", "Shipped");
关键点:事件发布者(OrderService)完全不知道谁在监听。它只负责发布事件。这种解耦,让系统扩展性极强——加一个新模块,只需要写一个监听器并订阅事件,不用改任何现有代码。
事件驱动 vs 传统观察者模式
| 对比维度 | 传统观察者模式 | 事件驱动模式 |
|---|---|---|
| 耦合方式 | 观察者直接注册到主题 | 通过事件总线解耦 |
| 扩展性 | 需要修改主题代码 | 只需添加新监听器 |
| 事件类型 | 通常只有一种通知 | 支持多种事件类型 |
| 异步支持 | 需要额外实现 | 天然支持(可轻松改为异步) |
| 调试难度 | 较低 | 较高(事件流追踪) |
避坑指南:我曾经在一个项目中,事件总线里注册了上百个监听器,结果某个事件触发时,所有监听器同步执行,导致响应时间飙升到好几秒。后来我加了一个异步队列,把事件处理改为异步执行,问题才解决。所以,如果你的监听器里有耗时操作,一定要考虑异步处理。
事件驱动架构的进阶设计
在实际项目中,我通常会在此基础上做几个增强:
- 事件优先级:某些监听器需要先执行(比如数据验证),可以给事件处理器加优先级
- 事件过滤:监听器只关心特定条件的事件,比如只关心金额大于1000的订单
- 事件溯源:把所有事件持久化,方便回溯和审计
- 异常处理:某个监听器抛出异常,不应该影响其他监听器
你想想看,这些增强其实都是在解决真实业务场景中的痛点。我建议你在学习时,不要只停留在「能跑就行」的阶段,多想想「如果线上出问题了怎么办」。
事件驱动模式的核心流程图
个人经验:事件驱动模式最大的价值,在于它让系统具备了「可插拔」的能力。我参与过一个大型电商平台的重构,核心业务逻辑几乎没动,只是把模块间的直接调用改成了事件驱动,结果后续半年内新增了6个业务模块,没有改过一行核心代码。这就是解耦的力量。
好了,关于事件驱动的观察者模式,我们就聊到这里。这种实现方式,说白了就是把「观察者注册到主题」这个动作,变成了「监听器订阅事件」。虽然概念上只是换了个说法,但在工程实践中,它带来的灵活性和可维护性是完全不同量级的。