写在前面
我写 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 的正确姿势)。