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的订单
  • 事件溯源:把所有事件持久化,方便回溯和审计
  • 异常处理:某个监听器抛出异常,不应该影响其他监听器

你想想看,这些增强其实都是在解决真实业务场景中的痛点。我建议你在学习时,不要只停留在「能跑就行」的阶段,多想想「如果线上出问题了怎么办」。

事件驱动模式的核心流程图

事件驱动观察者模式核心流程 事件源 OrderService 事件对象 OrderStatusChangedEvent 事件总线 EventBus 监听器1 LogisticsService 监听器2 SmsService 监听器3 LogService 创建 发布 通知 通知 通知 事件源 → 创建事件 → 发布到事件总线 → 事件总线通知所有已订阅的监听器

个人经验:事件驱动模式最大的价值,在于它让系统具备了「可插拔」的能力。我参与过一个大型电商平台的重构,核心业务逻辑几乎没动,只是把模块间的直接调用改成了事件驱动,结果后续半年内新增了6个业务模块,没有改过一行核心代码。这就是解耦的力量。

好了,关于事件驱动的观察者模式,我们就聊到这里。这种实现方式,说白了就是把「观察者注册到主题」这个动作,变成了「监听器订阅事件」。虽然概念上只是换了个说法,但在工程实践中,它带来的灵活性和可维护性是完全不同量级的。

公众号:蓝海资料掘金营,微信deep3321