中介者模式实战:把网状对象依赖变成星型解耦协作
发布时间:2026/10/14 22:48:37 锦皓数字建站

中介者模式Mediator Pattern大概是二十多个经典设计模式里最有“职场感”的一个。因为在现实工作里最让人头疼的往往不是某个类写得有多复杂而是好几个类之间的调用关系像一团毛线——拉一根带出一片谁都不敢动。中介者模式解决的就是这种“多对多互相纠缠”的问题。我最早接触它是在一个GUI项目里当时按钮、下拉框、列表、文本框彼此监听着对方的状态逻辑乱到新同事接手直接崩溃。后来重构时引入了中介者模式才彻底理解了它为什么是行为型模式里最被低估的一个。如果你写代码遇到过“A要通知BB又要通知CC又回过头影响A”的场景或者接手过那种互相持有引用、一动全炸的老模块这篇文章就是写给你看的。我会把中介者模式的定义、结构、代码实现、真实应用场景以及我在实际项目中踩过的坑一次性讲清楚不绕弯子。1. 先从一个让我加班的破设计说起1.1 一个典型的多组件联动噩梦想象你维护一个订单后台页面左侧是筛选条件中间是订单列表右侧是统计面板。看起来很简单实际写出来是这样的筛选区选了“已完成”状态要刷新列表列表翻页要刷新统计统计面板要展示金额金额又受到筛选条件影响用户点击某一行订单还要联动旁边的详情区域。用传统写法每个组件都得知道其他组件的存在。搜索框要持有列表引用列表要持有统计面板引用统计面板又要持有搜索框的引用。代码里的构造器一个比一个长十几个组件互相new来new去像极了过年时一屋子亲戚互相敬酒——每个人都认识每个人场面看着热闹谁举杯都能引起一圈连锁反应。这种设计的本质问题有两个对象之间的耦合度太高任何一个类的构造函数发生变化所有关联方都要跟着改交互逻辑散落在各个对象内部你想搞清楚“一次筛选点击到底影响了哪些模块”得把这十几个文件全部翻一遍。1.2 中介者模式的出现把“互相敬酒”变成“都找群主”中介者模式的想法特别朴素既然大家都要互相沟通那就别一对一聊了拉个群。所有消息都发到群里再由群主决定转发给谁。对应到代码里这个“群”就是一个中介者对象那些互相联动的组件就是“同事类”。用中介者模式重构之后搜索框不再持有列表的引用它只需要把“筛选条件变了”这件事告诉中介者。中介者再去调用列表的刷新方法、统计面板的更新方法。搜索框不认识列表列表也不认识搜索框它们之间唯一的桥梁就是中介者。这个模式是GoF二十三种设计模式里非常典型的行为型模式目的就四个字解耦协作。它把原本发生在多个对象之间的“网状依赖”统一收敛成了“星型依赖”。每个对象只依赖中介者不再依赖任何其他同事对象。2. 中介者模式的结构拆解四个角色、一条协作链2.1 四个参与者的职责划分理解一个设计模式先看参与者。中介者模式一共四个角色我一个个说清楚Mediator抽象中介者定义同事对象之间通信的接口。通常是一组方法签名比如“发送消息”“注册用户”“转发通知”。Concrete Mediator具体中介者实现上面的接口维护所有同事对象的列表真正处理对象之间的协调逻辑。它知道每个同事对象是什么类型、什么能力、什么时候该调谁。Colleague抽象同事类定义业务对象对外交互的公共接口。每个同事类对象都持有中介者的引用在需要通知别人的时候调用中介者的方法而不是直接调用另一个同事的方法。Concrete Colleague具体同事类真正执行业务的对象。它只关注自己的读、自己的写从来不直接引用其他具体同事类。用一套电商系统的订单状态流来套订单模块、库存模块、支付模块、物流模块就是四个同事类订单状态变更协调中心就是具体中介者。订单支付成功订单模块只管通知协调中心“支付状态变了”协调中心再去调库存模块扣库存、调物流模块创建配送单。订单模块的代码里不存在任何一行“库存模块.xxx()”的调用。2.2 协作时序一次消息到底怎么流转用文字描述一下标准的协作顺序具体的同事对象A在业务处理过程中产生了一个事件调用自己持有的中介者引用上的某个方法中介者收到这个事件后会根据自己的逻辑判断该通知哪些B、C对象中介者调用B、C对象上的处理方法把上下文传递过去。整个过程里A对B、C的存在一无所知它只知道一个中介者接口。这和我上节说的“拉群”类比完全一致普通用户A在群里发一句“有人吗”他根本不需要知道群里的B、C都在哪、住在哪条街、有没有在线消息由群管理平台统一分发。这里有一个关键设计点非常容易被初学者忽略同事类持有的应该是抽象中介者引用而不是具体中介者引用。这样做的好处是具体中介者的实现逻辑可以被替换。比如测试阶段你换一个假的Mock中介者业务的同事类代码完全不用动。这也是面向接口编程在模式层面的落地。2.3 从网状结构到星型结构的本质变化没有中介者时N个对象如果两两通信最多会有N(N-1)/2条依赖关系。4个对象就有6条依赖7个对象就是21条。每新增一个对象都要考虑跟已有对象之间的所有关系复杂度是平方级增长。引入中介者之后每个对象只需要指向中介者一条线N个对象就是N条依赖。新增一个对象时只需要把它注册到中介者那里其他对象完全无感。复杂度从平方级变成了线性级。这就是为什么经典的Java GUI框架也好前端组件库也好都喜欢引入事件总线或者集中式状态管理。它们本质上都是中介者模式思想在不同场景下的变体。这种结构转变还有一个隐藏收益交互逻辑被集中起来了。以前你想知道“一个下单动作会触发哪些后续步骤”要在各个类里来回跳。现在你只需要打开中介者这一个文件所有协调规则一目了然。这个优势在后文做代码实现时会体现得特别明显。3. 用聊天室Demo落地中介者模式3.1 Demo的结构设计写代码是理解模式最有效的方式。我用一个大家最熟悉的场景——聊天室用户A发言聊天室把消息广播给其他所有用户。如果这个需求用传统写法用户对象持有其他所有用户的列表A发言时遍历列表挨个发消息以后每次新增用户都要改A的代码。这明显不对。于是我用中介者模式重构。抽象中介者接口定义两个方法注册用户、发送消息。具体中介者聊天室维护一个用户Map。用户类是一个抽象类持有一个中介者引用提供“发送消息”和“接收消息”两个抽象方法对应的具体用户类实现这两个方法。在这套结构里用户A的send方法内部调用的是中介者的sendMsg方法由中介者去遍历注册表找到其他用户并调用他们的receive方法。A从头到尾不知道有哪些人也在线它只面向中介者编程。3.2 完整可运行的代码示例// 抽象中介者定义同事对象之间的通信接口 public interface ChatMediator { // 注册用户到聊天室 void registerUser(User user); // 广播消息来自from用户的消息发送给所有人 void sendMessage(String message, User from); // 私聊消息只发送给指定的to用户 void sendPrivateMessage(String message, User from, String toName); }import java.util.HashMap; import java.util.Map; // 具体中介者聊天室实现类 public class ChatRoom implements ChatMediator { private MapString, User userMap new HashMap(); Override public void registerUser(User user) { // 核心点中介者统一管理所有同事对象 userMap.put(user.getName(), user); // 通知所有人新用户加入 sendMessage(用户[ user.getName() ]加入了聊天室, null); } Override public void sendMessage(String message, User from) { // 广播遍历所有注册用户逐个发送 // 特别注意from为null时表示系统消息不回发 for (User user : userMap.values()) { if (from null || user ! from) { user.receiveMessage(message, from null ? 系统 : from.getName()); } } } Override public void sendPrivateMessage(String message, User from, String toName) { User target userMap.get(toName); if (target null) { System.out.println(系统: 用户[ toName ]不存在消息发送失败); return; } // 私聊只发送给目标用户 target.receiveMessage(message, from.getName()); } }// 抽象同事类业务对象统一继承该类 public abstract class User { protected String name; protected ChatMediator mediator; public User(String name, ChatMediator mediator) { this.name name; this.mediator mediator; } public String getName() { return name; } // 发送消息具体逻辑交给中介者而不是自己找其他用户 public abstract void send(String message); public abstract void sendPrivate(String message, String toName); // 接收消息具体如何展示由具体同事类决定 public abstract void receiveMessage(String message, String fromName); }// 具体同事类普通用户 public class NormalUser extends User { public NormalUser(String name, ChatMediator mediator) { super(name, mediator); } Override public void send(String message) { // 只看得到中介者完全不知道其他用户存在 mediator.sendMessage(message, this); } Override public void sendPrivate(String message, String toName) { mediator.sendPrivateMessage(message, this, toName); } Override public void receiveMessage(String message, String fromName) { System.out.println(name 收到 fromName 的消息: message); } }// 具体同事类管理员用户重写接收方法展示不同的表现 public class AdminUser extends User { public AdminUser(String name, ChatMediator mediator) { super(name, mediator); } Override public void send(String message) { mediator.sendMessage(message, this); } Override public void sendPrivate(String message, String toName) { mediator.sendPrivateMessage(message, this, toName); } Override public void receiveMessage(String message, String fromName) { System.out.println(name [管理员] 收到 fromName 的消息: message); } }public class MediatorPatternDemo { public static void main(String[] args) { ChatMediator chatRoom new ChatRoom(); // 创建用户注意传入的都是抽象中介者引用 User alice new NormalUser(小明, chatRoom); User bob new NormalUser(小红, chatRoom); User admin new AdminUser(管理员, chatRoom); // 注册到聊天室 chatRoom.registerUser(alice); chatRoom.registerUser(bob); chatRoom.registerUser(admin); // 小明广播 alice.send(大家好我是小明); System.out.println(---------------); // 管理员私聊小红 admin.sendPrivate(小红你提交的工单已通过, 小红); } }运行结果如下系统 的广播: 用户[小明]加入了聊天室 小红 收到 系统 的消息: 用户[小明]加入了聊天室 管理员[管理员] 收到 系统 的消息: 用户[小明]加入了聊天室 系统 的广播: 用户[小红]加入了聊天室 小明 收到 系统 的消息: 用户[小红]加入了聊天室 管理员[管理员] 收到 系统 的消息: 用户[小红]加入了聊天室 系统 的广播: 用户[管理员]加入了聊天室 小明 收到 系统 的消息: 用户[管理员]加入了聊天室 小红 收到 系统 的消息: 用户[管理员]加入了聊天室 小明 收到 小明 的消息: 大家好我是小明 小红 收到 小明 的消息: 大家好我是小明 管理员[管理员] 收到 小明 的消息: 大家好我是小明 --------------- 小红 收到 管理员 的消息: 小红你提交的工单已通过看到那段“小明 收到 小明 的消息”了吗这是因为广播循环里的判断条件我没有排除发送者本人是一个经典细节。真实代码里一般会把fromthis的情况排除掉或者从广播名单里过滤。这个先不急着改后面第7章讲坑的时候我会正式提它。3.3 代码背后的三个设计关键第一同事类只持有中介者抽象接口。NormalUser和AdminUser的构造器接收的都是ChatMediator而不是ChatRoom。这样测试时可以替换成Mock中介者业务代码不用改。我们在实际项目中写单测时经常会用这种方式把网络、数据库等外部依赖全部隔离掉。第二交互规则只存在于ChatRoom内部。广播和私聊的区别、针对不存在的用户怎么提示、新用户注册后要不要通知全员这些逻辑全部收敛在具体中介者里。别的类完全不用关心这正是中介者模式最重要的价值。第三具体同事类可以有不同的行为表现。AdminUser重写了receiveMessage加了“[管理员]”前缀但它对中介者来说仍然只是一个User。中介者不用关心它到底怎么展示消息只需要调用统一的receiveMessage方法。这就是多态的配合。4. 中介者模式在真实系统中到底用在哪4.1 消息队列分布式场景里的“超级中介者”很多人学完中介者模式有个疑问设计模式课上的例子都很小真实项目里真的有这么用的吗答案是太有了而且你大概率每天都在用只是不叫这个名。拿消息中间件举例。生产者和消费者之间如果直接点对点互通系统每加一种消息类型所有相关服务都要跟着改。引入消息队列之后生产者只往某个Topic发消息消费者只订阅自己感兴趣的Topic生产者跟消费者完全解耦。这个消息队列扮演的是什么就是那个中介者。我在参与某个微服务改造项目时把原本订单服务直接调用库存服务、库存服务直接调用通知服务的一串硬编码调用全部改成了发布订阅模式效果立竿见影——新增一个“订单超时提醒”服务只需要订阅对应事件完全不用改原有服务代码。这种场景里消息队列甚至比模式定义里的中介者更彻底因为它不仅是逻辑上的中枢物理上也是独立部署的节点。处理能力不够时还可以横向扩容用集群来消除中介者本身的性能瓶颈。4.2 前端状态管理与事件总线前端开发对中介者模式的感知应该更深刻。早期jQuery时代多个模块互相协作靠的是全局回调函数A模块绑定了B模块的函数B模块出了问题A模块也跟着炸。后来流行的集中式状态管理库做的事情和中介者一模一样所有组件不再直接互相调用而是通过dispatch事件到一个统一的Store由Store负责改变状态再驱动视图更新。组件之间你连我我连你的现象被彻底消除。事件总线Event Bus就更典型了。几个不相关的模块要通信走总线发事件接受方监听自己关心的事件。总线就是中介者事件就是消息。尤其是在中后台系统里不同业务模块经常需要跨区域联动走事件总线是成本最低的解法。4.3 MVC框架里的Controller很多人学MVC的时候都会疑惑Controller到底存在的意义是什么用中介者模式的视角看就特别清楚。Model和View本来不应该互相知道对方如果没有ControllerView要监听Model的变化做刷新Model要回调View的输入更新逻辑双向依赖肯定乱成一锅粥。有了ControllerView的输入事件发给ControllerController去更新ModelModel变化后通知ControllerController再去刷新View。虽然现代前后端分离架构里这种严格的三层关系已经被组件化替代了但Controller作为协调中枢的思想本质就是中介者模式的一种工业级应用。4.4 游戏系统和GUI框架游戏里的战斗系统是中介者模式的高发区。英雄、怪物、技能、Buff、道具互相作用如果不设一个战斗管理器每个实体都要持有其他实体的引用写出来的代码基本没法维护。战斗管理器作为中介者统一接收每个参战实体的行为请求再统一结算和广播结果这就是大型游戏客户端中常见的战斗框架设计。GUI框架也是一样。窗口里的控件如果直接互相引用做一个复杂的表单页面会非常痛苦。成熟的GUI框架都会在内部提供一个Context或者Mediator组件让控件的交互事件通过它转发给其他控件。这跟我文章开头那个让我加班的旧项目是一样的场景区别只在于有没有引入一个独立的中介者角色。5. 优点、缺点以及“要不要用”的判断标准5.1 中介者模式的正面收益中介者模式最大的好处前面已经反复说了解耦。同事类之间不再持有彼此的引用新增或者删除一个同事类其他同事类完全不需要感知就这一条就能省下无数改代码的时间。第二个好处是集中控制。所有交互逻辑都集中在一个类中相当于整个模块的“交通规则”都写在了一个显眼的地方。你接手一个老项目时想理解业务流转直接看中介者就好不需要翻遍所有同事类。可维护性和可读性都大幅上升。第三个好处是符合开闭原则。想要增加一种新的交互方式比如从前只有广播现在要支持私聊你只需要在中介者实现类里加逻辑同事类可以一个不动。想要增加一个新的同事类型只需要让它继承抽象同事类然后注册到中介者里。修改是开放的也是封闭的。5.2 绕不开的代价中介者可能变成上帝类任何模式都有代价。中介者模式最明显的风险是中介者类本身容易膨胀。所有逻辑都集中到它身上它很快就从几百行变成几千行成为一个什么逻辑都往里塞的“上帝对象”。这个风险是真实存在的尤其是在缺少良好设计习惯的团队里。还有一个隐患是性能集中。所有通信都绕不过中介者如果中介者里做了太重的操作比如反射、序列化、网络IO整个系统的吞吐量都会被它拖住。线程安全也是问题多个同事类同时发送消息中介者的状态是不是线程安全的在多线程环境下这往往是事故高发点。第三个隐患是调试链路变长。以前A直接调用B断点一打调用链清晰了然。引入中介者之后A - 中介者 - B中间多了一层。如果中介者逻辑再复杂一点定位一个问题的成本反而会上升。这在前端事件总线的场景里特别明显你根本不知道一个事件被多少个监听器处理了。5.3 我个人的判断清单根据我踩过的坑我觉得引入中介者模式前可以问自己四个问题沟通的对象数量是否真的超过三个如果只有两三个对象在交互直接持有引用完全够用引入中介者属于过度设计。交互逻辑是否稳定如果业务规则还在快速变化中介者的集中式管理反而方便你随时修改协调规则这是加分项。是否存在需要复用的交互规则比如多套页面共用同一套联动关系中介者可以把它们抽象成公共组件。团队里的人能不能接受“多跳一层”的调式成本这个纯属工程文化问题但在复杂系统里很现实。网上很多文章会把中介者模式捧得很高但我觉得它本质上是一个“用集中换解耦”的权衡方案。该用的时候果断用不该用的硬套反而会把代码搞复杂。判断标准永远只有一个这套对象之间的交互关系是不是已经复杂到你难以维护了6. 别和这几种模式搞混6.1 观察者模式中介者内部的推进器观察者模式和中介者模式经常被放在一起比较。观察者模式解决的是一个对象状态变化后如何通知多个依赖对象的问题核心是“订阅-发布”中介者模式解决的是多个对象之间复杂的交互如何解耦的问题。实际应用中它们经常配合中介者的具体实现内部大可以通过观察者机制来实现事件分发。比如事件总线它整体上是中介者模式但内部每个事件的监听器注册与触发用的就是观察者模式。简单概括观察者是实现手段中介者是整体架构。6.2 外观模式方向完全相反的“门面”外观模式最容易被误解成中介者。外观模式是给一个复杂子系统提供一个简洁的入口方向是单向的客户调用外观外观调用子系统。子系统不反向调用外观它们也不知道外观的存在。中介者模式的通信是双向的同事类调用中介者中介者反向调用同事类。两边都认识对方。一个是“门面”负责简化访问一个是“协调中心”负责调解关系。“门面往里带路中介者居中传话”这句话我记了很多年非常好用。6.3 命令模式、代理模式和中介者模式命令模式把请求封装成独立对象用于参数化客户端、排队请求、支持撤销等它和中介者模式可以组合命令对象发给中介者由中介者统一调度执行代理模式则是为某个对象提供一个替身以控制访问权限或增强功能它是“对被代理对象的管控”而不是“对多个对象的居中协调”。我在做一个权限系统时曾经把代理和中介者混着用后来发现代理只管每个方法入口的鉴权真正把模块之间的调用串联起来的还是中介者角色。两者职责不同定位也不同。再加一个事件总线。事件总线严格来说不是GoF经典模式中的一员它是中介者模式的变体把“直接调用中介者方法”变成“向总线发布事件总线再回调注册的处理器”。少了直白的类型依赖很多场景下写起来更灵活代价是调用链更隐蔽、类型安全性变差。我之前在某个电商管理后台里就用事件总线替代了原有的模块间硬调用灵活性确实高但出了线上问题排查起来也比原来费劲。7. 实战中踩过的坑与排查思路7.1 坑一广播把自己也发了引发连锁反应还记得第3.2节代码运行结果里那行“小明 收到 小明 的消息”吗真实业务里这可不是无伤大雅的瑕疵。我曾经在一个库存同步模块里遇到过这种情况库存服务发出库存变更事件中介者广播给所有同事结果事件源服务自己也订阅了这个事件于是又触发了一次库存变更接着再发一次事件形成了一个无限递归的通知循环。解决这个问题的标准姿势有两种。一是在广播时排除消息来源方过滤条件写成user ! from跟Demo里我做的处理一样二是给每条消息带上唯一的消息ID接收方在处理前检查这个ID是否已经处理过幂等防重。第二种方式在分布式系统里尤其重要因为消息可能会被重试投递防重不能只靠来源判断。这个坑教会我的事情是中介者的广播逻辑一定要显式考虑“消息是否要回发给发送者”。不要想当然觉得“发出去的又收回来也无所谓”在带状态变更的业务环境里这一步漏掉就是线上事故。7.2 坑二中介者膨胀成“上帝类”怎么拆集中控制的好处和坏处是同一个特性。用了中介者模式半年后某个系统的中介者类已经堆到三千多行了什么订单流转、权限校验、消息通知全往里塞。后来几个模块改需求每次都在这一个类里改代码冲突频繁到令人崩溃。我的经验是不要让一个中介者管所有事情按业务域拆分。订单域一个中介者支付域一个中介者消息域一个中介者各管各的跨域的交互再通过消息队列这个更上层的“中介者的中介者”去衔接。拆完之后每个中介者的职责回到一个可控的范围内既保留了集中管理的好处又避开了上帝类陷阱。这里的判断标准是如果你发现中介者里的分支逻辑需要根据调用来源写一堆if-else说明这个中介者的职责已经超出了“协调交互”的范围它正在变成业务逻辑的垃圾箱。该拆就拆别等。7.3 坑三循环依赖与初始化顺序问题同事类在构造时通常需要接收中介者引用而中介者又要维护同事对象列表。这里天然存在一个“先有鸡还是先有蛋”的依赖顺序问题。如果你在中介者的构造器里去初始化所有同事类而同事类又在构造器里调用中介者的注册方法很容易出现构造一半就调用了尚未初始化的对象。我常用的解法是把“创建同事类”和“注册到中介者”分成两步先创建中介者创建各个同事类但不让它们在构造器里主动注册所有同事类创建完成后再统一执行注册。这样注册时刻一定是中介者完全初始化完成的时刻顺序问题就可以消除。如果是依赖注入框架管理的对象建议让框架负责依赖顺序程序员不要手动依赖构造器里的间接副作用。7.4 疑难杂症速查表症状可能原因解决思路消息被重复处理广播未排除发送者或消息重复投递统一排除来源对象引入消息ID幂等机制中介者类持续膨胀把业务逻辑硬塞给了中介者按业务域拆分为多个中介者职责单一化同事类构造失败中介者与同事类初始化顺序冲突先建中介者再建同事类最后统一注册调用链太深排查困难中介者内部嵌套了多层if-else分支给消息加全局请求ID日志按ID聚合检索多线程环境下状态异常中介者内部持有可变共享状态且未加锁明确中介者的线程安全策略必要时用并发集合一个事件触发全部监听者广播粒度太粗过滤条件不足评估是否该拆分事件类型让订阅关系更精细这张表算是我中介者模式实战经验的一个压缩包。每次在新项目里决定引入中介者之前我都会对着表自查一遍这些风险我是不是已经想好了应对方案。想好了再动手写出来的代码通常会稳很多。关于中介者模式我最后想说的写到这里中介者模式的核心内容基本都覆盖了。如果你问我什么时候最该用它我的回答是当你代码里出现三个以上的对象在互相监听、互相传参、互相调用而且你发现新增一个需求要同时改好几个文件的时候就是该停下来思考引入一个中介者的时候。我个人在实际项目中的体会是中介者模式的关键从来不在代码复杂度上而在于你是否愿意接受“多一层间接”。这一层间接有时候看着多余但它换来的是对象之间真正的互不感知。尤其在做中大型系统时这种互不感知的收益会随着规模增长被不断放大。等到系统膨胀到几十个模块时再想引入改造的痛感会成倍上升但如果系统还很小只有几个对象我建议就不要硬套了。设计模式是解决问题的工具不是写进简历的装饰品。最后分享一个小技巧。如果你在看完这篇文章后想马上实践可以先从你手头最乱的那一小块逻辑开始不要一上来就动整个系统。挑三个最常见的联动对象用中介者模式重构成一个小Demo跑通一遍感受一次“A不认识B但A的消息被B收到了”的完整链路。这一步想通之后中介者模式就真正变成你自己的东西了。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。