1. 乱成一锅粥的对象通信到底该怎么收场先聊一个我实际接手的项目。一个在线客服系统里面有用户、客服、工单、消息推送、坐席调度好几个模块。一开始大家各写各的用户下单要通知客服客服接单要改工单状态工单状态变了要触发消息推送推送完还得更新坐席的负载。大概迭代半年之后代码里全是互相 new 出来的对象引用A 模块的人改一个构造函数B、C、D 三个模块集体编译报错。那个时候改一个需求先得拉一圈人问你这块用了我什么方法。说白了对象之间的关系已经从协作变成了蜘蛛网。这种场景就是中介模式Mediator Pattern真正该登场的时候。它的核心思想用一个词概括不要自己跟别人说话要说话先找中间人。中介模式属于行为型设计模式GoF 二十三种设计模式之一。它解决的核心问题是当系统里的多个对象之间存在大量多对多交互时用一个中介对象封装这些交互让对象之间不再互相持有引用而是通过中介者进行通信。说人话就是把一张乱糟糟的蜘蛛网捋成一个规整的星型结构。这个模式适合谁适合所有写业务系统的同学尤其是还在用类图靠想象、依赖靠硬引的方式做多模块协作的人。不管你是 Java、C#、Python 还是 Go 的开发者只要你写过两个以上的对象互相通知、互相调用的代码中介模式就值得你花十分钟搞明白。它能解决的不只是代码冗余问题而是整个架构的可维护性问题。2. 把网状变成星型中介模式的设计思路拆解2.1 没有中介时对象通信到底有多痛假设你做一个会议管理系统有三个角色主持人、参会者、会务组。需求是这样的主持人宣布开始所有参会者收到通知参会者提问主持人收到提醒主持人决定结束会务组要做会议纪要。如果你不用中介模式代码会长成什么样参会者类里要持有主持人的引用主持人类里得拿着参会者列表会务组既要认主持人又要认参会者。等需求再膨胀一点加一个会议直播的角色所有人都得改一遍。这个问题的本质是僵化的耦合关系。每一对对象之间都有一条直接的调用链类图画出来密密麻麻全是线。这在小型系统里还能忍受一旦规模上来就面临三个致命问题一是改动一个类就要连带改动所有引用它的类二是对象之间的调用规则散落在各个类里根本没法统一把控三是排查问题时你根本不知道该从哪条链上开始追。2.2 中介模式的角色划分与协作模型中介模式一共四个角色非常清晰Mediator抽象中介者定义通信接口声明各个同事对象之间交互需要的方法。ConcreteMediator具体中介者实现中介者接口持有并管理所有同事对象的引用具体协调逻辑都写在这里。Colleague抽象同事类定义同事的公共接口持有抽象中介者的引用。ConcreteColleague具体同事类实现自己的业务逻辑需要跟别人通信时直接调中介者的方法不关心消息最终发给谁。这四个角色配合起来通信流程就变成了同事 A 要发消息它只调用中介者中介者拿到消息之后自己决定该转发给同事 B 还是 C或者广播给所有人。对象之间不再有直接引用所有的交互策略都集中在中介者里面。我用一个生活化的类比帮助你理解这就相当于租房。以前你租房得自己一个个跑房东的电话打十个电话八个是中介还经常被放鸽子。现在你找一个房产中介告诉他你的需求他会替你去匹配房源、协调看房时间、谈价格。你不直接跟房东打交道房东也不需要认识每一个租客所有的博弈都发生在中介那里。这个房产中介就是你的 Mediator。2.3 中介模式与门面模式别傻傻分不清很多初学者经常把中介模式跟门面模式Facade Pattern搞混我在这里多说一句。门面模式的核心是对外提供统一入口它是单向的外部系统只跟门面打交道门面背后怎么组织子系统外部完全不用管。而中介模式是内部之间的协调是多个平级对象之间通过中介者进行双向通信。你可以这样记门面是给你的客户用的中介是给你自己内部人用的。搞明白了这个区别你写代码的时候选型才不会出错。如果只是想给外部调用方提供一个简单接口用门面如果是一堆同等地位的对象需要互相协作用中介。3. 手把手实现一个完整的中介模式案例3.1 案例场景多人聊天室要说中介模式最直观的落地场景就是聊天室。聊天室就是一个标准的中介者用户之间不直接通信而是把消息发给服务器服务器再转发给其他人。我带大家一起写一个聊天室的实现这里用 C# 演示但逻辑完全适用于 Java 和 Python。整个项目从定义一个中介者接口开始。先定义抽象中介者和抽象同事类// 抽象中介者 public interface IChatMediator { void SendMessage(string message, User sender); void RegisterUser(User user); } // 抽象同事类 public abstract class User { protected IChatMediator _mediator; public string Name { get; } public User(string name, IChatMediator mediator) { Name name; _mediator mediator; } public abstract void Send(string message); public abstract void Receive(string message, User sender); }这里有一个很关键的细节同事类持有的是抽象中介者的引用而不是具体中介者的引用。这么做的原因是遵循依赖倒置原则让同事类不依赖具体实现将来切换中介者实现时不需要动同事类的代码。3.2 实现具体中介者和具体同事类接下来实现具体中介者它的职责是管理用户列表并负责消息的路由public class ChatMediator : IChatMediator { private ListUser _users new ListUser(); public void RegisterUser(User user) { _users.Add(user); } public void SendMessage(string message, User sender) { // 广播给所有用户除了发送者本人 foreach (var user in _users) { if (user ! sender) { user.Receive(message, sender); } } } } public class BasicUser : User { public BasicUser(string name, IChatMediator mediator) : base(name, mediator) { } public override void Send(string message) { Console.WriteLine($[发送] {Name} 发出消息: {message}); _mediator.SendMessage(message, this); } public override void Receive(string message, User sender) { Console.WriteLine($[接收] {Name} 收到来自 {sender.Name} 的消息: {message}); } }这里要注意中介者的SendMessage方法把谁在发消息这个信息传进去了这样中介者在做路由时可以决定是否排除发送者本人。实际业务里你完全可以让中介者把所有消息都转发给所有人包括发送者这取决于你的规则。规则定义在中介者里这是中介模式最大的优势。3.3 客户端调用与运行结果验证写一个客户端代码验证一下class Program { static void Main() { ChatMediator mediator new ChatMediator(); User alice new BasicUser(Alice, mediator); User bob new BasicUser(Bob, mediator); User carol new BasicUser(Carol, mediator); mediator.RegisterUser(alice); mediator.RegisterUser(bob); mediator.RegisterUser(carol); alice.Send(大家好我是 Alice); bob.Send(你好 Alice我是 Bob); } }运行结果[发送] Alice 发出消息: 大家好我是 Alice [接收] Bob 收到来自 Alice 的消息: 大家好我是 Alice [接收] Carol 收到来自 Alice 的消息: 大家好我是 Alice [发送] Bob 发出消息: 你好 Alice我是 Bob [接收] Alice 收到来自 Bob 的消息: 你好 Alice我是 Bob [接收] Carol 收到来自 Bob 的消息: 你好 Alice我是 Bob注意观察一个关键点Alice 和 Bob 的代码里没有任何彼此引用的地方它们连对方的存在都不知道。所有人的消息都发给中介者由中介者决定转发策略。以后再加入一个新用户 Dave只需要这样写User dave new BasicUser(Dave, mediator); mediator.RegisterUser(dave);其他人一行代码都不用改。这就是解耦的好处——系统扩展时改动面被控制在中介者和新增类两处而不是牵一发动全身。3.4 升级版让中介者处理业务规则聊天室只是一个最朴素的例子。实际工作中中介者处理的不只是消息转发还可以承担各种业务规则。我来扩展一个场景一个支持私聊和敏感词过滤的聊天室。修改后的中介者这样实现public class AdvancedChatMediator : IChatMediator { private ListUser _users new ListUser(); private Liststring _blockedWords new Liststring { 广告, 违禁词 }; public void SendMessage(string message, User sender) { // 规则1: 敏感词过滤 foreach (var word in _blockedWords) { if (message.Contains(word)) { Console.WriteLine($[系统] {sender.Name} 的消息包含敏感词已拦截); return; } } // 规则2: 广播给其他人 foreach (var user in _users) { if (user ! sender) { user.Receive(message, sender); } } } }看到没敏感词过滤这个逻辑放在中介者里所有的消息都经过它你只需要维护一份敏感词列表就能统一控制所有用户的消息内容。如果这个逻辑放在同事类里你得在每个 User 子类里写一遍还容易漏。再往深了说生产级系统中中介者完全可以对接日志系统给消息加序号做消息持久化做异步推送。凡是所有通信都必须要经过的公共逻辑都适合放在中介者里。这也是为什么很多框架内部的事件总线消息分发器本质上都是中介模式思想的体现。4. 中介模式在真实项目里的选型与扩展4.1 哪些场景适合中介模式从我做过的项目来看适合引入中介模式的场景有几个明显特征第一个是对象数量多且交互密集。比如一个表单里有十几个控件每个控件的变化都会影响其他控件的状态。最典型的例子就是机票预订界面选城市、选日期、选舱位、选乘客人数任何一个变化都会联动价格展示和库存显示。如果让每个控件都持有其他控件的引用代码会爆炸。用一个中介者在 UI 领域它经常叫 Controller 或 Coordinator统一处理控件之间的联动是最优雅的解法。第二个是交互规则经常变化。假设你的系统里有几十个模块需要互相通信而通信规则动不动就变。今天要求 A 模块的消息要同步给 B 和 C明天又改成只给 C。如果每个模块自己管理通信每次规则变了都要改一堆类。一旦走中介者只需要改中介者的路由逻辑一行代码搞定。第三个是想把公共逻辑集中管理。认证、权限校验、日志记录、限流这些横切逻辑如果分散到每个对象里重复代码会非常多。集中到中介者里所有通信必然经过它天然就变成了一个 AOP 的切入点。4.2 中介模式与事件总线、消息队列的关系聊到这儿肯定有同学会问中介模式跟我们在微服务里用的消息队列是不是一回事我明确说一下思想一致层次不同。消息队列比如 Kafka、RabbitMQ的核心思想就是生产者不直接依赖消费者生产者把消息发到队列消费者自己去订阅。这就是中介思想在分布式系统层面的应用只不过那个中介变成了一个独立部署的中间件。而 GoF 中的中介模式是进程内的对象通信模式它的中介是一个普通对象处理的是同一个进程内、同一个 JVM/CLR 内的对象交互。在你单体应用内部你可以用一个事件总线EventBus框架来实现中介模式的效果。比如 C# 里的 Prism EventAggregator、Java 里的 Guava EventBus它们的本质就是把中介者做成一个全局单例对象通过发布/订阅模式通信。这样你甚至不需要自己手写同事类和中介类只需要发布事件、订阅事件即可。但我要提醒一点如果用了事件总线你的代码就会变得隐式协作阅读代码时很难一眼看出 A 事件会触发哪些 B 行为。手写中介模式虽然代码多一些但它是显式调用可读性更好。到底选哪种取决于你的团队风格和项目复杂程度。4.3 一个被很多人忽略的应用解耦第三方 SDK我在实际项目中还发现一个特别实用的用法用中介模式包装第三方 SDK 的调用。当时项目接了一个云存储服务业务代码里有十几处地方直接调用了云服务的 SDK。后来要换供应商替换工作量巨大而且每一处调用的参数格式还不一样。我就把这些调用收敛到一个StorageMediator里业务模块只知道mediator.Upload(file)至于底层是调阿里云、腾讯云还是华为云全部由中介者内部封装。后来真换了供应商我只改了中介者一个类业务代码零改动。这就是中介模式的隐藏实现细节价值它不只是用来协调对象之间交互的也可以用来统一封装外部依赖。当然这种用法严格来说更接近门面模式的思路但如果你同时要有多个业务模块互相通知的需求完全可以让中介者既管交互又管外部依赖。5. 实战中的坑与避坑经验5.1 中介者膨胀成上帝对象这是中介模式最典型的坑。随着业务复杂度的提升中介者会不断吸收协调逻辑最后变成一个几千行的上帝类——什么逻辑都往里面塞谁都要找它它一挂整个系统瘫痪。我的建议是当中介者的职责超过单一维度时拆分成多个专门的中介者。比如一个系统里既有聊天协调又有权限协调又有数据同步协调那就拆成三个中介者各自管理各自的同事。不要让一个中介者干所有事。这跟单一职责原则是一个道理中介者本身也要遵守。另外中介者和业务对象的关系要控制好。中介者持有同事的引用但如果同事的使用周期比中介者短就得注意清理引用否则会造成内存泄漏。比如用户退出登录后中介者还持有这个用户的引用系统里几百个用户频繁登录退出内存占用会很可观。5.2 什么时候不要用中介模式中介模式不是银弹有些场景用了反而是负担。如果只有两个对象交互用中介模式就是画蛇添足。两个对象之间本来清清楚楚的一条线你非要插一个中介者进去等于把简单问题复杂化了。如果交互规则非常简单固定完全没有集中管理的必要也不用中介模式。比如一个观察者模式就能解决的一拖多场景用中介模式反而让每个被观察者都依赖中介者平白增加一层间接性。还有一种情况你的同事类里有一些无法抽象出来的自定义逻辑强行套中介模式会让代码变得很不自然。设计模式永远是解决痛点的工具不是炫技的手段。没有痛点的地方不需要模式。5.3 结合事件驱动架构的落地思路最后说一下现代项目里中介模式怎么跟事件驱动结合。如果你不想自己手写中介类可以考虑用事件驱动的方式实现同样的效果。定义一个OrderServiceMediator它内部维护一个事件处理器字典同事之间通过发事件来通信public class OrderMediator { private DictionaryType, ListActionobject _handlers new(); public void RegisterTEvent(ActionTEvent handler) { var eventType typeof(TEvent); if (!_handlers.ContainsKey(eventType)) { _handlers[eventType] new ListActionobject(); } _handlers[eventType].Add(e handler((TEvent)e)); } public void PublishTEvent(TEvent evt) { if (_handlers.ContainsKey(typeof(TEvent))) { foreach (var handler in _handlers[typeof(TEvent)]) { handler(evt); } } } }这种事件驱动的中介者比手写的 if-else 路由更灵活新增一种事件类型不需要修改中介者的核心逻辑只要增加注册即可。代价是丢掉了一些编译期安全性你要自己保证事件类型匹配。实际项目中我一般倾向于用这种方案因为它更好地平衡了耦合度与扩展性。6. 典型问题排查与面试点梳理6.1 排查日记中介者的消息风暴有一次线上系统出现性能问题数据监控显示某个中介者的方法被调用了成千上万次。排查下来发现同事 A 发一条消息中介者转发给 BB 收到消息后又调中介者给 C 发消息C 又触发给 A 发消息A 又给 B 发……形成了循环依赖。这是中介模式里非常隐蔽的问题——中介者虽然解耦了对象之间的直接依赖但没有切断它们之间的逻辑循环。排查方法很简单要么在中介者的路由逻辑里加去重和防抖要么给消息带上来源标记和最大转发次数限制超过阈值直接丢弃。除此之外打印完整的调用链路日志用链路追踪工具看调用树也能很快定位循环点。6.2 面试高频问题参考如果你正在准备面试中介模式相关的几个高频问题我帮你列一下第一个是中介模式和观察者模式的区别。观察者模式是一对多的关系一个被观察者变化多个观察者响应方向是单向的。中介模式是多对多的协调关系方向是双向的而且所有的交互策略都在中介者里统一管理。简单记观察者是发布-订阅中介者是中心路由。第二个是中介模式和门面模式的区别。这个我前面已经讲过了门面是单向的对外封装中介是多向的内部协调。第三个是中介模式的缺点很多人只记得优点一问缺点就卡壳。核心缺点有三点中介者可能膨胀成上帝对象中介者自身成了单点故障源它性能出问题整个通信链路都出问题代码的隐式调用增多静态代码分析时很难直观地看到对象之间的调用关系。第四个是为什么同事类要持有抽象中介者的引用。答案是依赖倒置原则让高级模块中介者和低级模块同事类都依赖抽象,这样替换中介者实现时不需要改同事类。6.3 快速记忆口诀我自己总结了一个便于记忆的口诀分享给团队新人用对象通信不直连中间找个协调员路由规则集中管网状变成星型间。十六个字把核心都说透了。7. 最后分享一点实战体会中介模式我前后用了五年多踩过不少坑也尝到过不少甜头。最深的体会是这个模式的精髓不在于那个中介类本身而在于你把通信规则放在了哪里。很多人学完模式只记住了代码结构却忽略了模式的本质——封装变化。中介模式封装的是对象之间交互方式的变化这才是它的价值所在。另外不管是手写中介者还是用事件总线心里都要绷着一根弦中介者是服务业务的不是统治业务的。当你发现中介者开始堆积业务状态、承载业务模型的时候就该停下来重新审视设计了。它应该是一个通信协调层而不是业务逻辑的收纳箱。有人问过我说中介者跟 Controller 有什么区别——我的回答是Controller 管的是 HTTP 接口和业务用例的编排中介者管的是多个领域对象之间的协作它们两个可以共存甚至 Controller 内部完全可以用中介者来协调多个领域服务。最后再分享一个小技巧在写中介模式之前先画一下当前的类关系图。如果互相连接的线超过你手指能数清的数量基本就可以考虑引入中介者了。这个标准很土但每次都很管用。