1. 项目概述:为什么我们需要“解耦”?
在C#开发中,尤其是构建具有一定复杂度的桌面应用、游戏逻辑或服务端模块时,我们常常会面临一个经典困境:一个模块的变动,像多米诺骨牌一样,引发一连串其他模块的修改。比如,一个“用户登录成功”的动作,可能需要触发更新界面、记录日志、发送通知、初始化用户数据等多项任务。如果这些任务都通过直接调用其他类的方法来实现,代码就会变得高度耦合,牵一发而动全身,维护和扩展将成为噩梦。
“委托与事件”正是C#为解决这类问题而生的利器,它们构成了观察者模式在C#中的优雅实现。简单来说,委托(Delegate)是一种类型安全的函数指针,它定义了方法的签名;而事件(Event)是基于委托的、遵循特定规范的发布-订阅机制。通过它们,我们可以让一个对象(发布者)在特定事情发生时,通知所有关心此事的其他对象(订阅者),而发布者完全不需要知道订阅者是谁、有多少个、具体做了什么。这就是“解耦”的核心——将变化的可能性隔离在订阅者内部,发布者的代码因此变得稳定而清晰。
掌握委托与事件的解耦技巧,是C#程序员从“能写功能”迈向“会设计结构”的关键一步。这不仅仅是语法知识,更是一种重要的架构思维。无论是WinForms/WPF中的按钮点击、ASP.NET Core中的中间件管道,还是Unity游戏引擎中的消息系统,其底层通信机制都深深植根于此。接下来,我将结合十多年的踩坑经验,为你彻底拆解这套机制,让你不仅能写出解耦的代码,更能理解其背后的设计哲学与实战要点。
2. 核心概念深度解析:委托、事件与解耦的三角关系
要玩转解耦,必须吃透委托和事件各自扮演的角色以及它们如何协同工作。很多人学了语法,但用起来还是别扭,问题往往出在对三者关系的理解不够透彻。
2.1 委托:能力契约的缔造者
你可以把委托想象成一份“能力说明书”或“合同”。它不关心是谁来履行合同,只关心履行者必须具备什么样的“能力”(即方法的参数和返回值类型)。
// 定义一个委托类型,它描述了一种“能力”:接收一个string参数,返回void。 public delegate void LogHandler(string message);这行代码声明了一个名为LogHandler的委托类型。它相当于在说:“任何想为我记录日志的对象,你必须提供一个方法,这个方法能接收一个字符串消息并且不返回任何内容。” 委托是一种类型,和class、interface一样,可以定义在命名空间或类内部。
为什么需要委托类型?因为它提供了类型安全。相比于C/C++中的函数指针,委托是面向对象且类型安全的。编译器会确保你赋值给委托变量的方法,其签名(参数类型、个数、返回值)必须与委托定义完全匹配。
public class ConsoleLogger { // 这个方法符合LogHandler合同 public void WriteToConsole(string msg) { Console.WriteLine($"[Console] {msg}"); } } public class FileLogger { // 这个方法也符合LogHandler合同 public void WriteToFile(string msg) { File.AppendAllText("log.txt", $"{DateTime.Now}: {msg}\n"); } } // 使用委托变量 LogHandler logger; logger = new ConsoleLogger().WriteToConsole; // 赋值方法 logger += new FileLogger().WriteToFile; // 多播委托,追加方法 logger("应用程序启动"); // 一次调用,两个方法都会执行关键理解点:委托变量(如logger)可以存储对一个或多个方法的引用。使用+=操作符可以组合多个方法(多播委托),调用该委托变量时会按顺序调用所有方法。=是赋值,会清空之前的引用列表;+=是追加;-=是移除。
注意:对于实例方法,委托不仅保存了方法引用,还隐式保存了方法所属对象实例的引用。这就是为什么你可以把
new ConsoleLogger().WriteToConsole这样的实例方法赋给委托。
2.2 事件:基于委托的安全发布-订阅机制
如果委托是“能力合同”,那么事件就是一份“标准的招标公告流程”。它基于委托,但增加了严格的访问控制,确保了发布-订阅模式的安全性和封装性。
事件在类内部声明,使用event关键字:
public class OrderService { // 1. 定义委托类型(如果使用系统预定义的,如EventHandler,可省略此步) public delegate void OrderProcessedEventHandler(object sender, OrderEventArgs e); // 2. 基于委托类型声明事件 public event OrderProcessedEventHandler OrderProcessed; }事件与委托字段的本质区别: 从外部看,事件就像一个被阉割了的委托变量。对于上面的OrderProcessed事件:
- 订阅者视角:只能做两件事:
+=(订阅)和-=(取消订阅)。不能使用=直接赋值,也不能直接Invoke(调用)事件。OrderService service = new OrderService(); service.OrderProcessed += OnOrderProcessed; // 正确:订阅 service.OrderProcessed -= OnOrderProcessed; // 正确:取消订阅 // service.OrderProcessed = OnOrderProcessed; // 编译错误:不能直接赋值 // service.OrderProcessed(null, null); // 编译错误:不能直接调用 - 发布者(OrderService类内部)视角:可以像调用委托一样,检查是否为
null后安全地触发事件。public void ProcessOrder(Order order) { // ... 处理订单逻辑 ... OnOrderProcessed(new OrderEventArgs { Order = order }); } protected virtual void OnOrderProcessed(OrderEventArgs e) { // 线程安全的触发方式 OrderProcessedEventHandler handler = OrderProcessed; if (handler != null) { handler(this, e); } }
为什么要有事件?直接用公共委托字段不行吗?不行,这关乎封装性和安全性。如果OrderProcessed只是一个公共的OrderProcessedEventHandler委托字段,那么任何外部代码都可以:
- 使用
=操作符,清空所有其他订阅者的列表。 - 直接
Invoke触发事件,冒充发布者。 这完全破坏了发布-订阅模式的设计初衷。事件通过编译器施加的限制,完美地防止了这些情况,确保了只有事件的拥有者(声明它的类)才能触发它,订阅者只能选择加入或退出。
2.3 解耦:委托与事件如何实现
现在我们把三者串联起来看“解耦”是如何发生的:
- 定义契约(委托):
OrderProcessedEventHandler定义了“当订单处理完成后要通知谁”的契约。 - 提供通知通道(事件):
OrderProcessed事件是这个通知通道的入口和出口。 - 发布者(OrderService):只负责在正确的业务逻辑点(
ProcessOrder方法内)触发OrderProcessed事件。它不关心谁在听,也不关心听众做了什么。它的代码因此变得极其稳定,未来无论是要增加邮件通知、短信通知还是更新排行榜,都无需修改OrderService的代码。 - 订阅者(如EmailService, AnalyticsService):它们根据自己的职责,订阅感兴趣的事件(
OrderProcessed)。当事件发生时,它们执行自己的逻辑(发邮件、记录分析数据)。订阅者之间也互不知晓,独立变化。
解耦的威力:假设现在需要新增一个“库存扣减服务”在订单处理后更新库存。我们只需要新建一个InventoryService类,让它订阅OrderProcessed事件即可。OrderService和已有的EmailService等模块都无需做任何修改。系统的可扩展性、可维护性得到了质的提升。
3. 标准事件模式与最佳实践
.NET框架为我们设计了一套成熟的事件模式,遵循它能让你的代码更标准、更易被其他开发者理解。
3.1 .NET事件模式标准写法
这套模式的核心是使用System.EventHandler<TEventArgs>泛型委托和自定义的事件参数类。
// 1. 定义事件参数类,继承自EventArgs public class OrderEventArgs : EventArgs { public Order Order { get; set; } public DateTime ProcessedTime { get; set; } } // 2. 发布者类 public class OrderService { // 使用泛型EventHandler<TEventArgs>声明事件 public event EventHandler<OrderEventArgs> OrderProcessed; // 3. 定义触发事件的受保护虚方法(命名通常为OnXXX)。这是最佳实践。 protected virtual void OnOrderProcessed(Order order) { // 线程安全地获取事件委托实例的副本 EventHandler<OrderEventArgs> handler = OrderProcessed; if (handler != null) { handler(this, new OrderEventArgs { Order = order, ProcessedTime = DateTime.Now }); } } public void ProcessOrder(Order order) { // ... 核心业务逻辑 ... // 业务逻辑完成后,触发事件 OnOrderProcessed(order); } } // 4. 订阅者类 public class EmailService { public EmailService(OrderService orderService) { // 订阅事件 orderService.OrderProcessed += OnOrderProcessed; } // 事件处理方法,签名必须与EventHandler<OrderEventArgs>匹配 private void OnOrderProcessed(object sender, OrderEventArgs e) { var order = e.Order; Console.WriteLine($"发送邮件给 {order.CustomerEmail}: 您的订单 {order.Id} 已处理完成。"); } }为什么这是最佳实践?
- 标准化:
EventHandler<T>是.NET BCL中的标准委托,所有.NET开发者都熟悉。 - sender参数:传递触发事件的对象(
this),方便订阅者知道事件来源。 - 继承EventArgs:方便未来扩展,可以在不改变委托签名的情况下,通过
TEventArgs传递更多数据。 - 受保护的虚方法OnXXX:这是一个重要的设计。它让派生类可以重写事件触发行为(虽然很少需要),更重要的是,它把事件触发的逻辑封装在一个方法里,使
ProcessOrder业务方法更清晰。同时,handler局部变量的赋值操作是线程安全的(在赋值那一刻的瞬间快照),避免了在检查null和调用之间,另一个线程取消订阅导致NullReferenceException的风险。
3.2 自定义委托 vs EventHandler
什么时候需要自定义委托?通常不需要。EventHandler<T>已经覆盖了99%的场景。仅在以下情况考虑自定义委托:
- 需要不同的方法签名(例如,需要一个返回值)。
- 为了极致的性能,避免
EventArgs对象的装箱(在性能敏感的循环中,但这种情况极少)。
但请注意,需要返回值的事件模式非常罕见,通常意味着设计可能有问题(事件是通知,而非请求)。绝大多数情况下,坚持使用EventHandler<T>。
3.3 事件订阅与内存泄漏陷阱
这是委托和事件使用中最经典的“坑”。由于委托持有对目标对象(订阅者)的引用,如果发布者对象的生命周期长于订阅者,且订阅者没有取消订阅,那么订阅者对象将无法被垃圾回收器(GC)回收,导致内存泄漏。
问题场景:
public class Publisher { public event EventHandler SomethingHappened; } public class Subscriber { public Subscriber(Publisher pub) { pub.SomethingHappened += HandleEvent; // 订阅 } private void HandleEvent(object sender, EventArgs e) { } } // 使用 var publisher = new Publisher(); // 长生命周期对象,如静态单例、主窗体 var subscriber = new Subscriber(publisher); subscriber = null; // 试图丢弃subscriber // 此时,由于publisher.SomethingHappened还持有对subscriber.HandleEvent的引用, // subscriber对象无法被GC回收!解决方案:
- 显式取消订阅:当订阅者不再需要事件时,或订阅者生命周期结束时,务必使用
-=。public class Subscriber : IDisposable { private Publisher _publisher; public Subscriber(Publisher pub) { _publisher = pub; pub.SomethingHappened += HandleEvent; } public void Dispose() { // 在Dispose中取消订阅 _publisher.SomethingHappened -= HandleEvent; } private void HandleEvent(object sender, EventArgs e) { } } - 使用弱事件模式:对于WPF等框架,存在
WeakEventManager或第三方库(如WeakEventHandler),它使用弱引用,允许订阅者被回收。但这会带来轻微的性能开销和更复杂的用法,非必要不使用。 - 注意静态事件:静态事件的生命周期与应用域相同,订阅它的实例方法会导致实例永远无法被释放,风险极高。务必谨慎使用静态事件,并确保清理。
实操心得:在桌面应用开发中,窗体(Form)控件的事件订阅是内存泄漏的重灾区。例如,一个自定义控件订阅了主窗体的某个静态事件,当窗体关闭而控件未取消订阅,控件就不会被释放。养成习惯:在窗体的
Dispose方法或FormClosing事件中,集中清理所有非本窗体控件发起的事件订阅。
4. 高级技巧与实战应用模式
掌握了基础,我们来看看如何用委托和事件玩出更多花样,解决更复杂的设计问题。
4.1 泛型委托与Lambda表达式的优雅结合
.NET Framework提供了内置的泛型委托Action<>和Func<>,它们可以替代很多自定义委托声明,让代码更简洁,尤其是在配合Lambda表达式时。
Action<>:表示一个无返回值的方法。Action无参,Action<T>一个参数,以此类推。Func<>:表示一个有返回值的方法。最后一个泛型参数是返回值类型。Func<TResult>无参有返回,Func<T1, TResult>一个参数有返回。
应用场景:回调与策略模式
public class DataProcessor { // 使用Action<string>作为处理日志的回调 public void ProcessData(string data, Action<string> logCallback) { if (string.IsNullOrEmpty(data)) { logCallback?.Invoke("输入数据为空!"); // 安全调用 return; } // ... 处理数据 ... logCallback?.Invoke($"数据{data}处理完成。"); } } // 调用方可以灵活注入日志行为 var processor = new DataProcessor(); // 方式1:传入Lambda表达式 processor.ProcessData("test", msg => Console.WriteLine($"[Info] {msg}")); // 方式2:传入一个方法 processor.ProcessData("test", LogToFile); // 方式3:传入一个匿名方法 processor.ProcessData("test", delegate(string msg) { Debug.WriteLine(msg); }); private void LogToFile(string message) { /*...*/ }这里,Action<string>logCallback参数使得DataProcessor类与具体的日志实现解耦。调用者决定日志如何输出,这体现了策略模式的思想。
4.2 事件聚合器:实现更彻底的解耦
在大型应用中,直接的事件订阅可能导致复杂的依赖网(A订阅B,B订阅C……)。事件聚合器(Event Aggregator)是一种中介模式,它作为全局的、唯一的事件总线,所有模块都通过它来发布和订阅事件,模块之间完全不知道彼此的存在。
简易实现示例:
public interface IEventAggregator { void Publish<TEvent>(TEvent eventToPublish) where TEvent : class; void Subscribe<TEvent>(Action<TEvent> handler) where TEvent : class; void Unsubscribe<TEvent>(Action<TEvent> handler) where TEvent : class; } public class SimpleEventAggregator : IEventAggregator { private readonly Dictionary<Type, List<object>> _handlers = new(); public void Publish<TEvent>(TEvent eventToPublish) where TEvent : class { var eventType = typeof(TEvent); if (_handlers.ContainsKey(eventType)) { // 注意:这里需要处理线程安全和异常隔离 foreach (var handler in _handlers[eventType].Cast<Action<TEvent>>().ToList()) { handler(eventToPublish); } } } public void Subscribe<TEvent>(Action<TEvent> handler) where TEvent : class { var eventType = typeof(TEvent); if (!_handlers.ContainsKey(eventType)) { _handlers[eventType] = new List<object>(); } _handlers[eventType].Add(handler); } public void Unsubscribe<TEvent>(Action<TEvent> handler) where TEvent : class { // 实现省略... } } // 使用 public class OrderCreatedEvent { public Order Order { get; set; } } var eventAggregator = new SimpleEventAggregator(); // 订阅 eventAggregator.Subscribe<OrderCreatedEvent>(e => Console.WriteLine($"订单 {e.Order.Id} 创建了")); // 发布 eventAggregator.Publish(new OrderCreatedEvent { Order = new Order() });Prism、MvvmCross等框架都提供了成熟的事件聚合器实现。它的优点是极致解耦,缺点是失去了编译时的类型检查(所有事件都是通过泛型TEvent来区分的),并且调试时事件流可能不那么直观。
4.3 异步事件(C# 5.0+)
传统的同步事件处理会阻塞发布者,直到所有订阅者处理完毕。从C# 5.0开始,我们可以利用async/await实现异步事件处理。
定义异步事件:
public delegate Task AsyncEventHandler<TEventArgs>(object sender, TEventArgs e); public class AsyncEventPublisher { public event AsyncEventHandler<EventArgs> AsyncWorkCompleted; protected virtual async Task OnAsyncWorkCompleted() { var handler = AsyncWorkCompleted; if (handler != null) { // 依次异步调用所有订阅者,但这里是顺序执行,一个await完才下一个。 // 如果需要并发执行,需使用Task.WhenAll。 foreach (AsyncEventHandler<EventArgs> singleHandler in handler.GetInvocationList()) { await singleHandler(this, EventArgs.Empty); } } } public async Task DoWorkAsync() { await Task.Delay(1000); // 模拟异步工作 await OnAsyncWorkCompleted(); // 异步触发事件 } } // 订阅者 var publisher = new AsyncEventPublisher(); publisher.AsyncWorkCompleted += async (s, e) => { await Task.Delay(500); Console.WriteLine("异步处理完成1"); };关键点:
- 事件处理程序返回
Task。 - 发布者触发事件时使用
await。 - 注意异常处理:一个订阅者的异常会传播到发布者。通常需要单独
try-catch每个处理程序的调用。 - 考虑并发与顺序:上面的例子是顺序执行。如果订阅者之间无依赖,可以使用
Task.WhenAll并发执行以提高效率。
5. 常见问题、调试技巧与性能考量
5.1 常见问题速查表
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 事件触发了,但订阅者没反应 | 1. 订阅者订阅事件的时机不对(在事件触发后才订阅)。 2. 订阅者方法签名与事件委托不匹配。 3. 订阅者对象已被垃圾回收(如果是弱引用或错误处理)。 | 1. 确保在触发前订阅(通常在构造函数或初始化方法中)。 2. 检查参数类型、数量和返回值。 3. 检查对象生命周期,确保持有引用。 |
抛出NullReferenceException | 触发事件时没有检查null。if (OrderProcessed != null) OrderProcessed(...)在多线程下不安全,可能在检查后、调用前被其他线程置为null。 | 使用局部变量副本:var handler = OrderProcessed; if (handler != null) handler(...); |
| 内存使用持续增长 | 事件订阅导致的内存泄漏。长生命周期对象持有短生命周期对象的引用。 | 1. 实现IDisposable,在Dispose中取消订阅。2. 使用弱事件模式(如WPF的 WeakEventManager)。3. 审查静态事件的订阅。 |
| 事件处理顺序不符合预期 | 多播委托的调用顺序是订阅的先后顺序。如果对顺序有强需求,依赖此特性并不稳健,因为订阅顺序可能因代码改动而变化。 | 不要依赖隐式的调用顺序。如果顺序重要,应在发布者内部维护一个明确的有序处理器列表,或者让事件参数包含优先级字段,由订阅者自行处理。 |
| 异步事件中异常被吞掉 | 在async事件处理中,如果没有await,异常可能不会立即抛出。或者使用Task.WhenAll时未处理聚合异常。 | 1. 确保异步事件处理程序被await。2. 使用 try-catch包裹每个处理程序的调用,或处理Task.WhenAll返回的Task的异常。 |
5.2 调试技巧:谁订阅了我的事件?
在复杂项目中,有时很难追踪是哪些对象订阅了某个事件。可以使用调试工具或反射来查看。
使用Visual Studio调试器:
- 在触发事件的代码行设置断点。
- 当断点命中时,在“即时窗口”中输入
?事件名(例如?this.OrderProcessed)。 - 展开返回的委托对象,查看
_invocationList字段(对于多播委托),里面包含了每个订阅者方法的详细信息,包括目标对象(_target)和方法名(_methodPtr或_method)。
通过反射获取订阅列表(用于诊断代码):
public static List<Delegate> GetEventSubscribers(object obj, string eventName) { var field = obj.GetType().GetField(eventName, BindingFlags.Instance | BindingFlags.NonPublic); if (field == null) return new List<Delegate>(); var delegateValue = field.GetValue(obj) as Delegate; if (delegateValue == null) return new List<Delegate>(); return delegateValue.GetInvocationList().ToList(); }注意:此方法依赖于事件的底层实现(编译器生成的私有委托字段),字段名通常就是事件名。但这属于实现细节,不同编译器版本可能不同,仅限调试使用。
5.3 性能考量
- 委托调用开销:委托调用比直接方法调用稍慢,因为多了一次间接寻址。但在绝大多数应用场景中,这种开销微乎其微,不应成为不使用委托的理由。清晰的结构带来的收益远大于此开销。
- 事件触发频率:对于在紧密循环中每秒触发成千上万次的事件,需要评估性能影响。可以考虑使用标志位控制、批量处理或直接调用等优化手段。
- 多播委托的遍历:
GetInvocationList()会返回一个委托数组的副本。在性能关键路径中频繁调用它可能产生压力。如果只是需要调用,直接调用多播委托即可(它会遍历内部列表)。
6. 实战:构建一个简单的消息中心
让我们综合运用以上知识,构建一个用于模块间通信的轻量级消息中心。
// 定义消息基类 public abstract class MessageBase { } // 定义具体消息 public class UserLoggedInMessage : MessageBase { public string UserName { get; set; } } public class OrderShippedMessage : MessageBase { public int OrderId { get; set; } public DateTime ShipDate { get; set; } } // 消息中心(单例) public class MessageCenter { private static readonly Lazy<MessageCenter> _instance = new Lazy<MessageCenter>(() => new MessageCenter()); public static MessageCenter Instance => _instance.Value; private readonly Dictionary<Type, List<object>> _subscribers = new(); private MessageCenter() { } // 订阅消息 public void Subscribe<TMessage>(Action<TMessage> handler) where TMessage : MessageBase { var messageType = typeof(TMessage); if (!_subscribers.ContainsKey(messageType)) { _subscribers[messageType] = new List<object>(); } _subscribers[messageType].Add(handler); } // 取消订阅 public void Unsubscribe<TMessage>(Action<TMessage> handler) where TMessage : MessageBase { var messageType = typeof(TMessage); if (_subscribers.ContainsKey(messageType)) { _subscribers[messageType].Remove(handler); } } // 发布消息 public void Publish<TMessage>(TMessage message) where TMessage : MessageBase { var messageType = typeof(TMessage); if (_subscribers.ContainsKey(messageType)) { // 复制列表以避免在遍历过程中集合被修改 var handlers = _subscribers[messageType].Cast<Action<TMessage>>().ToList(); foreach (var handler in handlers) { try { handler(message); } catch (Exception ex) { // 一个订阅者的异常不应影响其他订阅者 // 在实际项目中,这里应该记录日志 Console.WriteLine($"处理消息 {messageType.Name} 时发生异常: {ex.Message}"); } } } } } // 使用示例 public class UIModule { public UIModule() { MessageCenter.Instance.Subscribe<UserLoggedInMessage>(OnUserLoggedIn); } private void OnUserLoggedIn(UserLoggedInMessage msg) { Console.WriteLine($"UI: 欢迎回来,{msg.UserName}!"); } } public class LoggingModule { public LoggingModule() { MessageCenter.Instance.Subscribe<UserLoggedInMessage>(OnUserLoggedIn); MessageCenter.Instance.Subscribe<OrderShippedMessage>(OnOrderShipped); } private void OnUserLoggedIn(UserLoggedInMessage msg) { Console.WriteLine($"日志: 用户 {msg.UserName} 登录系统。"); } private void OnOrderShipped(OrderShippedMessage msg) { Console.WriteLine($"日志: 订单 {msg.OrderId} 已于 {msg.ShipDate} 发货。"); } } // 模拟应用 class Program { static void Main() { var ui = new UIModule(); var logger = new LoggingModule(); // 用户登录 MessageCenter.Instance.Publish(new UserLoggedInMessage { UserName = "张三" }); // 输出: // UI: 欢迎回来,张三! // 日志: 用户 张三 登录系统。 // 订单发货 MessageCenter.Instance.Publish(new OrderShippedMessage { OrderId = 1001, ShipDate = DateTime.Now }); // 输出: // 日志: 订单 1001 已于 [当前时间] 发货。 } }这个简单消息中心的优点:
- 完全解耦:
UIModule和LoggingModule互不知晓,只与MessageCenter交互。 - 类型安全:使用泛型,在编译时确保消息类型和处理程序匹配。
- 易于扩展:新增消息类型或订阅者无需修改现有代码。
- 集中管理:所有跨模块通信在一个地方可见和管理。
可以改进的方向:
- 线程安全:对
_subscribers字典的访问应加锁(lock)或使用并发集合(ConcurrentDictionary)。 - 依赖注入:将
MessageCenter作为服务注入,而不是使用单例,便于测试。 - 异步支持:提供
PublishAsync方法和支持Func<TMessage, Task>的订阅。 - 弱引用:集成弱引用支持以防止内存泄漏。
委托与事件的解耦艺术,精髓在于让对象各司其职,通过“消息”而非“调用”来协作。从最基础的event关键字到复杂的事件聚合器、消息总线,其核心思想一脉相承。理解并善用这一机制,能让你设计的C#应用程序在应对变化时更加从容,架构也更加清晰健壮。在实际编码中,我个人的习惯是:对于类内部的简单回调,优先用Action/Func;对于跨对象的、标准的状态变更通知,用标准event模式;对于大型应用中的跨模块、跨层通信,则引入消息中心或事件聚合器。多思考“这里的变化点是什么”,你就能更准确地判断该在何时、以何种方式使用这把解耦利器。