设计模式实战(七):观察者模式——C# event 的真相与领域事件

写在前面

我写 DDD 第四篇讲"领域事件"时说过:聚合做完自己的事,发个事件,通知所有感兴趣的下游。这套机制的底层,就是观察者模式

而 C# 是所有语言里观察者模式最舒服的——event 和委托是语言级支持,不用手写。这篇讲清观察者,并把它和 DDD 领域事件、Rx 串起来。


一、问题:怎么通知"一群"依赖者?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
场景:订单状态变了,要通知:库存系统、积分系统、通知系统、报表系统。

  朴素写法(被观察者直接调用每个下游):
    class Order {
      void StatusChanged() {
        _inventory.Handle(this);
        _points.Handle(this);
        _notifier.Handle(this);
        _report.Handle(this);
      }
    }
  病:
  ✗ 订单模块依赖了库存、积分、通知、报表 → 强耦合
  ✗ 加一个新下游(比如风控)要改 Order 类 → 违反开闭
  ✗ 下游增减要在被观察者里改

  病根:被观察者"主动知道"每个下游是谁。
  反转:让下游"主动订阅",被观察者只负责"广播",不认识具体下游。
  这就是观察者模式。

二、观察者:发布者 + 订阅者

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
意图:定义对象间一对多依赖。一个对象状态变化,所有依赖者自动收到通知。

  角色:
    Subject(主题/发布者)—— 持有一组观察者,状态变化时遍历通知
    Observer(观察者)     —— 提供更新接口,被通知时执行

  关键:
    - 发布者不认识具体观察者,只认识接口
    - 订阅者随时 + / - 加入退出
    - 一对多(一个发布者 → N 个订阅者)

  应用:事件总线、消息广播、UI 数据绑定、领域事件、响应式编程

三、C# 的 event/委托 = 天生观察者

很多语言要手写 Subject/Observer 接口。C# 直接把这套做进了语言

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// 1. 声明一个事件(发布者)
public class Order
{
    // event 关键字 = 受控的观察者列表;EventHandler = 标准回调签名
    public event EventHandler<OrderStatusChangedEventArgs>? StatusChanged;

    public void Cancel()
    {
        Status = Cancelled;
        // 2. 状态变化时,触发事件 = 通知所有订阅者
        StatusChanged?.Invoke(this, new OrderStatusChangedEventArgs(Status));
    }
}

// 3. 订阅者:用 += 注册,-= 取消
order.StatusChanged += Inventory.OnOrderStatusChanged;
order.StatusChanged += Points.OnOrderStatusChanged;
order.StatusChanged += Notifier.OnOrderStatusChanged;

// 订单状态一变,三个订阅者自动被调用
order.Cancel();
1
2
3
4
5
6
7
8
拆解:
  event            → Subject 的"观察者列表"(语言帮你管,外部只能 +=/-=)
  EventHandler     → Observer 接口的化身(一个回调签名)
  Invoke           → Subject 遍历通知
  += / -=          → 订阅 / 取消订阅

  → GoF 那套 Subject/Observer 抽象,C# 用 event 三行就实现了。
  这就是上一篇说的"模式在 .NET 里融化进语言"的典型。

四、手写 IObservable / IObserver(标准接口)

如果不用 event,.NET 也提供了一对标准接口,常用于"推数据流"的场景:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 发布者
public class PriceFeed : IObservable<decimal>
{
    private readonly List<IObserver<decimal>> _subs = new();
    public IDisposable Subscribe(IObserver<decimal> observer)
    {
        _subs.Add(observer);
        return new Unsub(() => _subs.Remove(observer));   // 返回"取消订阅"句柄
    }
    public void Push(decimal price)
    {
        foreach (var s in _subs) s.OnNext(price);          // 通知所有订阅者
    }
}

// 订阅者
public class PriceLogger : IObserver<decimal>
{
    public void OnNext(decimal price)  => Log($"价格: {price}");  // 收到数据
    public void OnError(Exception e)   => Log($"出错: {e}");
    public void OnCompleted()          => Log("结束");
}
1
2
3
4
5
6
event vs IObservable:
  event        → "发生了一件事"通知(离散事件,如 StatusChanged)
  IObservable  → "数据流"推送(连续序列,如股价、鼠标移动)

  后者配合 Rx(Reactive Extensions)能做强大的流式处理:
  窗口聚合、防抖、过滤、合并 —— 把事件当集合用。

五、和 DDD 领域事件的关系

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
DDD 第四篇讲的"领域事件",本质就是观察者模式在领域层的应用:

  聚合(Order)  = Subject,变更后发事件
  领域事件(OrderPlaced) = 通知的载荷
  下游订阅者(库存/积分/通知) = Observer
  事件分发器(MediatR 等)   = Subject 的通知遍历机制

  对应关系:
    Order.Collect events → Dispatcher.Dispatch → 各 Handler.Handle
    就是   Subject.Notify → 遍历 Observers → 各 Observer.Update

  区别:
    C# event:进程内、同步(Invoke 直接调)
    领域事件:可进程内,也可跨服务(配 Outbox 走消息队列,最终一致)

  → DDD 领域事件不是新东西,是观察者模式在"跨边界/异步"场景的工程化。

六、坑:事件订阅不取消 = 内存泄漏

观察者模式最经典的坑,C# 里尤其要注意:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 短生命周期对象订阅了长生命周期对象的事件
public class MyWindow
{
    public MyWindow(Order longLivedOrder)
    {
        longLivedOrder.StatusChanged += OnChanged;   // 订阅
    }
    void OnChanged(object? s, EventArgs e) { ... }
}

// Window 关闭、被丢弃后,只要 Order 还活着、还持有这个事件委托,
// 就等于持有 Window 的引用 → Window 永远不被 GC → 内存泄漏!
1
2
3
4
5
6
7
8
9
原因:event 持有订阅者的委托 = 持有订阅者的引用。
     订阅者想被回收,必须先 -= 取消订阅。

  对策:
  1. 订阅者销毁前 -= 取消(实现 IDisposable)
  2. 短生命周期对象订阅长生命周期事件,用弱事件(WeakEventManager)
  3. 局部订阅用完即退,别让 lambda 偷偷持有 this

  这是观察者模式落地时最常见的 bug,面试常考。

七、何时用 / 何时别用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
该用观察者:
  ✓ 一对多通知(一个变化,多个下游关心)
  ✓ 发布者和订阅者要解耦(互不认识)
  ✓ 订阅者动态增减
  ✓ 事件驱动/响应式场景

别用:
  ✗ 就一对一回调 → 普通接口/委托就够,别上事件
  ✗ 调用顺序严格、必须按序、不能少 → 观察者顺序不定,别用
  ✗ 需要订阅者返回值/同步结果 → 事件是单向通知,不适合
  ✗ 忘了管理生命周期 → 泄漏

  信号:当 A 的变化要让 B、C、D 都反应,但 A 不想认识 B/C/D —— 上观察者。

八、小结

  • 问题:一对多通知,发布者不该认识每个下游
  • 观察者模式:Subject 持有一组 Observer,状态变化时遍历通知
  • C# event 是天生观察者event = 观察者列表,+=/-= = 订阅/取消,Invoke = 通知
  • IObservable/IObserver:标准接口,适合数据流(配合 Rx)
  • 领域事件就是观察者在 DDD 的工程化(进程内或跨服务)
  • 大坑:事件订阅不 -= → 内存泄漏(订阅者被发布者持有,无法 GC)
  • 信号:一变多通知、要解耦 → 观察者

下一篇讲单例模式——以及它为什么是"最该谨慎使用的模式"(反模式面 + DI 的正确姿势)。