1. 观察者模式:编程界的杂志订阅机制
想象一下你最喜欢的科技杂志。当你订阅后,每次新刊发布就会自动送到你家门口,而不需要你每天跑去书店查看。观察者模式在软件设计中实现的正是这种"订阅-通知"机制,只不过主角从杂志社和读者变成了对象与对象。
在C#中实现观察者模式时,最经典的例子莫过于事件处理系统。比如一个按钮(Subject)被多个事件处理器(Observers)监听,当点击动作发生时,所有注册的处理器都会自动执行。微软的.NET框架大量运用了这一模式,WinForms的控件事件、ASP.NET的页面生命周期事件都是典型案例。
关键区别:观察者模式与委托(delegate)经常被混淆。委托是C#实现观察者模式的语言特性,而观察者是一种设计模式,可以用委托、接口或事件等多种方式实现。
2. 模式结构解剖:C#实现的三要素
2.1 Subject(主题)的职责边界
在C#中,一个标准的Subject类通常包含这些核心成员:
csharp复制public class MagazinePublisher
{
private List<ISubscriber> _subscribers = new List<ISubscriber>();
public void Subscribe(ISubscriber subscriber)
{
if(!_subscribers.Contains(subscriber))
_subscribers.Add(subscriber);
}
public void Unsubscribe(ISubscriber subscriber)
{
_subscribers.Remove(subscriber);
}
protected void NotifySubscribers(string message)
{
foreach(var sub in _subscribers)
sub.Update(message);
}
}
这里有个实际开发中的经验点:_subscribers字段为什么用List而不用HashSet?在订阅者数量少于100时,List的性能差异可以忽略,且更节省内存。但当预期有高频的订阅/取消订阅操作时,HashSet的O(1)时间复杂度会更优。
2.2 Observer(观察者)的契约设计
观察者接口通常极其简单:
csharp复制public interface ISubscriber
{
void Update(string message);
}
但在实际项目中,我建议增加一个sender参数:
csharp复制void Update(object sender, EventArgs args);
这样观察者就能知道事件源,这在多主题场景下特别有用。.NET的事件模型(EventHandler委托)正是采用这种设计。
2.3 通知机制的四种演化形态
- 推模式:主题将全部数据推送给观察者
csharp复制void Update(ProductPrice price); - 拉模式:观察者根据需要从主题拉取数据
csharp复制void Update(IPriceProvider provider); - 事件委托:使用C#内置event关键字
csharp复制public event EventHandler PriceChanged; - IObservable接口:.NET提供的标准实现
csharp复制public class PriceWatcher : IObservable<decimal> { private List<IObserver<decimal>> _observers; }
在性能敏感的场景中,事件委托通常是最佳选择,因为编译器会为event生成线程安全的add/remove方法。
3. 实战:股票行情通知系统
3.1 基础实现版本
假设我们要建立一个股票价格变动通知系统:
csharp复制public class StockTicker : ISubject
{
private decimal _price;
public decimal Price
{
get => _price;
set
{
if(_price != value) {
_price = value;
NotifyObservers();
}
}
}
// 实现ISubject接口...
}
public class MobileApp : IObserver
{
public void Update(ISubject subject)
{
if(subject is StockTicker ticker) {
Console.WriteLine($"APP通知:最新股价 {ticker.Price:C}");
}
}
}
3.2 线程安全改造
上述代码在单线程环境下工作良好,但在实际金融系统中,股价更新可能来自网络线程,而观察者可能在UI线程:
csharp复制private readonly object _lock = new object();
private List<IObserver> _observers = new List<IObserver>();
public void NotifyObservers()
{
List<IObserver> observersCopy;
lock(_lock) {
observersCopy = new List<IObserver>(_observers);
}
foreach(var observer in observersCopy) {
try {
observer.Update(this);
} catch(Exception ex) {
// 记录日志但继续执行其他观察者
LogError(ex);
}
}
}
这里有两个关键点:
- 锁内快速复制列表,减少锁持有时间
- 单个观察者异常不应中断整个通知流程
3.3 性能优化技巧
当观察者数量超过1000时,通知可能成为瓶颈。我们可以:
- 使用ReaderWriterLockSlim替代lock,适合读多写少场景
- 对观察者进行分组,实现批量通知
- 引入异步通知模式:
csharp复制但要注意:异步通知会改变事件顺序语义,金融系统等对顺序敏感的场景需谨慎。public async Task NotifyObserversAsync() { var tasks = _observers.Select(o => Task.Run(() => o.Update(this))); await Task.WhenAll(tasks); }
4. 模式变体与.NET集成方案
4.1 IObservable 标准实现
.NET 4.0引入了响应式扩展(Rx)的基础接口:
csharp复制public class StockObservable : IObservable<decimal>
{
private List<IObserver<decimal>> _observers = new List<IObserver<decimal>>();
public IDisposable Subscribe(IObserver<decimal> observer)
{
if(!_observers.Contains(observer))
_observers.Add(observer);
return new Unsubscriber(_observers, observer);
}
private class Unsubscriber : IDisposable
{
private List<IObserver<decimal>> _observers;
private IObserver<decimal> _observer;
public void Dispose()
{
if(_observer != null && _observers.Contains(_observer))
_observers.Remove(_observer);
}
}
}
这种实现更符合.NET生态,特别是与LINQ配合时能实现强大的事件流查询。
4.2 事件与委托的语法糖
C#的event关键字实际上是观察者模式的语法糖:
csharp复制public class Button
{
public event EventHandler Clicked;
protected virtual void OnClicked()
{
Clicked?.Invoke(this, EventArgs.Empty);
}
}
编译器会自动生成线程安全的add/remove方法,以及背后的委托字段。这是最符合C#习惯的实现方式。
4.3 弱引用模式解决内存泄漏
观察者模式最常见的问题是忘记取消订阅导致的内存泄漏。弱引用(WeakReference)可以缓解:
csharp复制public class WeakEventManager
{
private List<WeakReference<EventHandler>> _handlers = new List<WeakReference<EventHandler>>();
public void AddHandler(EventHandler handler)
{
_handlers.Add(new WeakReference<EventHandler>(handler));
}
public void RaiseEvent(object sender, EventArgs args)
{
for(int i = _handlers.Count - 1; i >= 0; i--) {
if(_handlers[i].TryGetTarget(out var handler)) {
handler(sender, args);
} else {
_handlers.RemoveAt(i);
}
}
}
}
WPF的WeakEventManager就是这种思想的典型应用。
5. 模式对比与选型指南
5.1 观察者 vs 中介者模式
两者都用于对象间通信,但:
- 观察者:一对多关系,主题知道观察者的存在
- 中介者:多对多关系,同事对象不知道彼此
在聊天室场景中:
- 如果用观察者,每个用户需要订阅其他所有用户
- 用中介者,用户只需与聊天室中介交互
5.2 观察者 vs 发布/订阅模式
发布/订阅是观察者的分布式演进:
- 观察者:直接通信,耦合度高
- 发布/订阅:通过消息队列解耦
Azure Service Bus或RabbitMQ等消息中间件实现了生产级的发布/订阅。
5.3 何时不该使用观察者
- 通知顺序至关重要时(观察者顺序通常不确定)
- 观察者处理耗时严重影响主题性能时
- 需要历史事件重放时(考虑事件溯源模式)
在电商订单系统中,订单状态变更适合用观察者,但支付流水记录更适合用事件溯源。
6. 实战陷阱与调试技巧
6.1 内存泄漏检测
使用Visual Studio的诊断工具:
- 运行程序并执行订阅操作
- 触发GC并获取内存快照
- 取消所有订阅后再次获取快照
- 比较两次快照中的对象实例数
如果订阅者对象未被释放,说明存在泄漏。
6.2 性能问题定位
当通知变慢时:
- 使用Stopwatch测量Notify方法耗时
- 用VS的性能分析器查看热点
- 检查是否有观察者执行同步IO操作
我曾遇到一个案例:某个观察者在Update方法中同步写入日志文件,导致整个系统变慢。改为异步写入后吞吐量提升8倍。
6.3 单元测试策略
测试观察者模式时要注意:
csharp复制[Test]
public void PriceChange_Notifies_AllObservers()
{
var mockObserver1 = new Mock<IObserver>();
var mockObserver2 = new Mock<IObserver>();
var subject = new StockTicker();
subject.Subscribe(mockObserver1.Object);
subject.Subscribe(mockObserver2.Object);
subject.Price = 100m;
mockObserver1.Verify(o => o.Update(subject), Times.Once);
mockObserver2.Verify(o => o.Update(subject), Times.Once);
}
特别注意验证通知次数(Times.Once),避免重复通知或漏通知。
7. 现代C#中的演进趋势
7.1 记录(Record)类型作为事件载体
C# 9.0引入的记录类型非常适合作为通知消息:
csharp复制public record PriceChanged(decimal OldPrice, decimal NewPrice, DateTime ChangeTime);
public event EventHandler<PriceChanged> PriceChanged;
7.2 模式匹配增强可读性
C# 7.0的模式匹配可以简化观察者处理:
csharp复制public void Update(object sender, EventArgs e)
{
switch (sender, e)
{
case (StockTicker ticker, PriceChanged pc):
Console.WriteLine($"股价从{pc.OldPrice}变到{pc.NewPrice}");
break;
case (NewsFeed feed, BreakingNews bn):
AlertUser(bn.Title);
break;
}
}
7.3 异步事件流处理
使用System.Threading.Channels实现高性能事件队列:
csharp复制private Channel<PriceChanged> _channel = Channel.CreateUnbounded<PriceChanged>();
public async Task StartProcessing()
{
await foreach(var change in _channel.Reader.ReadAllAsync())
{
await ProcessChangeAsync(change);
}
}
public void OnPriceChanged(decimal oldPrice, decimal newPrice)
{
_channel.Writer.TryWrite(new PriceChanged(oldPrice, newPrice));
}
观察者模式在C#生态中经历了从简单实现到企业级应用的完整演进。理解其核心思想后,可以根据具体场景选择最适合的实现方式——无论是简单的事件委托、标准的IObservable,还是分布式的消息队列。关键在于把握"变化传播"这一核心诉求,避免过度设计。
