1. 从一次崩溃的联调说起:中介者模式到底解决了什么问题
如果你在项目里见过那种十几个对象互相new来new去、改一个需求要牵连七八个类的代码,那中介者模式就是为你准备的。
中介者模式(Mediator Pattern)是GoF 23种设计模式里行为型模式的最后一员,定义很简洁:用一个中介对象来封装一组对象之间的交互,让这些对象不再直接引用彼此。说白了,就是给一堆互相乱喊乱叫的对象中间塞一个“话事人”,所有通信都走它,谁也别想绕过它私下勾搭。
这个模式初看有点反直觉。我们平时写代码,对象A要调用对象B的方法,直接b.doSomething()不就完了吗?为什么要绕一圈?等你真正维护过一个所有模块都互相引用的项目,你就会明白直接调用带来的麻烦:对象之间耦合得像一团煮烂的面条,牵一发而动全身。中介者模式的核心价值,就是把“多对多”的混乱交互,重构成“多对一”的星型结构。
适合看这篇文章的人,包括正在准备设计模式考试的学生、要交设计模式大作业的本科/研究生,以及写业务代码时总觉得类之间关系越来越乱的开发者。这篇文章会从原理讲到Java实现,再结合我在实际项目中用中介者模式的经验,聊聊这个模式在现代架构里(包括多Agent设计)的演变形态。
2. 中介者模式的结构拆解:四个角色一张星型图
2.1 四个角色的职责边界
中介者模式的类结构不复杂,但四个角色的边界一定要理清楚,不然写出来就四不像。
Mediator(抽象中介者):定义通信接口,一般会声明一个notify或者send方法,用于接收同事对象发来的消息,再转发给目标对象。
ConcreteMediator(具体中介者):实现中介者接口,内部持有所有同事对象的引用,维护交互逻辑。这是整个模式最核心也最容易写臃肿的类,因为它承载了所有业务协调逻辑。
Colleague(抽象同事类):定义同事对象的公共接口,通常持有一个中介者的引用。
ConcreteColleague(具体同事类):实现自己的业务方法,需要和其他同事通信时,不直接调用对方,而是调用自己持有的中介者对象。
我来画一个文字版的结构示意:
Mediator(抽象中介者接口) ↑ │ 实现 ConcreteMediator(具体中介者) 持有 colleagueA / colleagueB / colleagueC ↑ │ 持有引用 ┌──────────┼──────────┐ ColleagueA ColleagueB ColleagueC注意这里的通信方向:同事类发消息给中介者,中介者再决定转发给谁。同事类之间不允许互相持有引用。这个“不允许”是模式的关键约束,破了这个规矩,整个模式就崩塌了。
2.2 一个能跑的Java聊天室案例
用聊天室来演示中介者模式是教科书里的经典做法,因为聊天室本质上就是一个中介者:用户不直接给另一个用户发消息,而是把消息发给聊天室服务器,服务器再转给目标用户。
我用Java写一个完整可运行的版本。先定义抽象同事类:
public abstract class User { protected String name; protected ChatMediator mediator; public User(String name, ChatMediator mediator) { this.name = name; this.mediator = mediator; } public abstract void send(String message); public abstract void receive(String message); public String getName() { return name; } }然后是抽象中介者接口:
public interface ChatMediator { void sendMessage(String message, User user); void addUser(User user); }具体中介者实现类:
import java.util.ArrayList; import java.util.List; public class ChatRoom implements ChatMediator { private List<User> users = new ArrayList<>(); @Override public void addUser(User user) { users.add(user); System.out.println(user.getName() + " 加入了聊天室"); } @Override public void sendMessage(String message, User sender) { for (User user : users) { // 不给自己发,只转发给其他用户 if (user != sender) { user.receive(sender.getName() + ": " + message); } } } }具体同事类:
public class ChatUser extends User { public ChatUser(String name, ChatMediator mediator) { super(name, mediator); } @Override public void send(String message) { System.out.println(this.name + " 发送消息: " + message); mediator.sendMessage(message, this); } @Override public void receive(String message) { System.out.println(this.name + " 收到消息: " + message); } }测试类:
public class MediatorDemo { public static void main(String[] args) { ChatMediator chatRoom = new ChatRoom(); User alice = new ChatUser("Alice", chatRoom); User bob = new ChatUser("Bob", chatRoom); User carol = new ChatUser("Carol", chatRoom); chatRoom.addUser(alice); chatRoom.addUser(bob); chatRoom.addUser(carol); alice.send("大家好,我是Alice"); System.out.println("---"); bob.send("欢迎Alice!"); } }运行结果:
Alice 加入了聊天室 Bob 加入了聊天室 Carol 加入了聊天室 Alice 发送消息: 大家好,我是Alice Bob 收到消息: Alice: 大家好,我是Alice Carol 收到消息: Alice: 大家好,我是Alice --- Bob 发送消息: 欢迎Alice! Alice 收到消息: Bob: 欢迎Alice! Carol 收到消息: Bob: 欢迎Alice!这个例子最能说明中介者的价值:如果不用中介者,Alice要发消息给Bob和Carol,就得同时持有两个用户的引用。当用户数量变成十个、二十个,每个用户都得维护一份“其他所有用户”的引用列表,新增一个用户就得改所有用户类的代码。有了中介者,用户类只认中介者一个对象,互相之间完全解耦。
2.3 关键点解读:为什么同事类只认中介者
我见过很多人写中介者模式,写着写着就变形了,最常见的问题就是同事类之间偷偷互相引用。其实中介者模式能不能起到解耦作用,全看一个关键约束:同事类之间绝对不能直接持有对方的引用。
这个约束背后的逻辑是:通过中介者,把对象之间的网状关系变成星型关系。来看看两种结构的复杂度对比。
假设有n个对象,直接相互通信时,最多需要维护的连接数是 n×(n-1)/2 条,每增加一个对象,连接数线性增长。用中介者模式后,每个对象只需要维护1条到中介者的连接,总连接数只有n条,新增对象时只需要改中介者一个类。
这个差距在日常开发中感觉不明显,因为一个模块里通常也就五六个类。但如果是复杂业务系统,一个流程涉及十几二十个领域对象,网状结构就会变得完全不可维护。这也是为什么很多框架底层都在用类似的思想——把复杂的交互收敛到一个协调者里。
中介者模式的本质是封装变化:交互逻辑是变化最频繁的部分。把交互逻辑从各个同事类中抽出来,集中放到中介者类里,这样修改交互逻辑时只需要改一个类,而不是改所有参与交互的类。这是“找出变化的封装,在变化处创建边界”这个设计原则的典型应用。
3. 中介者模式的现代形态:从MVC到多Agent主从模式
3.1 你早就用过的中介者:MVC里的Controller
学设计模式最大的误区是把它当“新东西”学,其实很多模式你已经在框架里用过了,只是没对上号。
MVC架构里的Controller就是典型的中介者:View要响应用户操作时,不直接去调Model的方法,而是先把事件交给Controller,由Controller来决定要更新哪个Model、然后如何处理结果并更新View。Model和View之间没有直接依赖,全靠Controller在中间协调。这不就是中介者模式的标准结构吗?View和Model是同事类,Controller是中介者。
消息队列也是中介者模式的思想。多个服务要通信,不直接互相调接口,而是把消息发到消息队列中间件,由中间件路由转发。这样一来,服务之间完全解耦,新增一个消费方不需要改任何生产方的代码。大型分布式系统里,服务数量动辄几十上百个,没有消息队列做中介,服务间的调用关系会变成一张无法维护的蜘蛛网。
前端状态管理工具,比如Redux,也是中介者模式。组件要改全局状态时,不直接操作其他组件,而是dispatch一个action给store,store里的reducer处理完状态变化后再通知所有订阅的组件更新。组件与组件、组件与状态之间完全通过store中转。
3.2 热词观察:多Agent设计里的“主从模式”与中介者
最近“多Agent系统”这个概念特别火,尤其在AI应用开发领域。我在看多Agent设计相关资料时注意到,智能体设计模式里经常提到“主从模式”:一个主Agent负责调度,多个子Agent(subagent)各司其职。
然后有人提出一个很有洞察的看法:主从模式本质上就是把subagent当作一种另类的tool来调用。
这个观察很有意思,因为它直接联系到了行为型模式的本质。主Agent持有所有子Agent的注册信息,接收来自用户或其他模块的任务,然后决定把任务分发给哪个子Agent,子Agent处理完把结果返回给主Agent。这个结构,就是中介者模式在Agent系统里的翻版。各subagent不需要互相知道对方的存在,更不需要直接调用对方的能力,所有协作路径都要经过主Agent协调。
不仅仅是主从模式,Agent系统里的几种常见结构都能看到中介者模式的影子:
轮询协调结构:一个核心Agent按照顺序或优先级依次询问各个专业Agent是否需要处理当前任务,这相当于具体中介者接收同事对象的请求后,通过遍历方式决定转发给谁。
路由分发结构:核心Agent根据任务类型把请求路由给对应能力Agent,这相当于中介者模式里sendMessage方法内部的逻辑判断——根据消息类型和目标对象决定转发路径。
共享状态结构:多个Agent通过共享外部存储来交换信息,这个存储就是中介者。Agent不需要知道谁生产了数据,只要往共享区放或从共享区取就行。
如果你要写设计模式大作业,围绕“中介者模式在多Agent系统中的应用”来做,会是一个非常切合技术热点、又有足够深度的选题。思路可以是这样:设计一个任务调度Agent作为中介者,把代码生成、文档撰写、代码审查三个专项Agent作为注册的同事对象,任务调度Agent接收用户请求后,分发给对应专项Agent,专项Agent处理完把结果返回给调度Agent。这个案例既展示了中介者模式的结构,又扣住了智能体设计这个热门领域。
3.3 中介者模式与观察者模式、门面模式的区别
学设计模式时最容易搞混的就是中介者模式、观察者模式和门面模式,因为它们都涉及解耦,但解耦的层次和方向各不相同。
观察者模式解决的是“一对多通知”的问题:一个主题对象状态变化,要通知多个观察者。通信方向是单向的——从主题到观察者。中介者模式解决的是“多对多交互”的问题:多个同事对象之间互相通信,通信方向是双向的——任何一个同事都可以发消息给任何一个其他同事。
我用一个对比场景来说明。新闻订阅系统,报社(Subject)发布新闻,多个订阅者(Observer)接收新闻,这是观察者模式。即时通讯系统,任何一个用户都可以给其他任何用户发消息,所有消息都经过服务器(Mediator)转发,这是中介者模式。关键区别在通信方向:有一个源头,是观察者模式;多对多互相通信,是中介者模式。
门面模式(Facade)和中介者模式表面上也有点像:都是在一个复杂系统外面包一层。但门面模式暴露的是一个简化后的接口,它的目的不是管理对象间的交互,而是给客户端一个简单入口,客户端还是可以绕过门面直接访问子系统。中介者模式则不允许同事对象绕过中介者直接通信,它管理的不只是接口,而是完整的交互逻辑。
写大作业或者面试梳理时,把这个对比讲清楚,能体现你对设计模式理解得比较透彻。我整理了一个表格方便对照:
| 维度 | 中介者模式 | 观察者模式 | 门面模式 |
|---|---|---|---|
| 核心目的 | 封装多对多交互 | 实现一对多通知 | 简化外部访问接口 |
| 通信方向 | 双向,任意同事可发可收 | 单向,主题向观察者推送 | 单向,客户端访问子系统 |
| 对象关系 | 同事间不直接引用 | 观察者订阅主题 | 客户端依赖门面 |
| 管理内容 | 交互逻辑 | 状态变化通知 | 接口暴露 |
| 模式类型 | 行为型 | 行为型 | 结构型 |
4. 中介者模式的代价与决策:什么时候别硬上
4.1 中介者膨胀:所有逻辑堆成一个上帝类
中介者模式最知名的问题就是“中介者膨胀”。交互逻辑都收拢到中介者类里,随着业务需求持续增加,这个类会变得越来越臃肿,最后变成一个什么都要管、什么都耦合的“上帝类”。
我在一个订单系统里就踩过这个坑。最初用中介者统一管理订单、库存、支付、物流四个模块的交互,写起来确实清爽。但三个月后需求叠加,中介者类里塞了二十多个方法,将近一千行代码,方法之间共享大量私有状态,改一个逻辑要担心影响其他八个逻辑。
后来我做了拆分:按业务子域把大的中介者拆成几个小中介者,比如订单协调器、支付协调器、物流协调器,各管各的交互,子协调器之间再通过一个上层协调器串起来。这本质上不是推翻中介者模式,而是给模式加了层次,避免单个中介者无限膨胀。
这个问题的根源在于:中介者模式把交互逻辑集中了,但集中不等于可以无限堆叠。当交互逻辑本身变得复杂时,中介者内部也要做模块化,而不是把所有东西平铺在一个类里。这和我们写普通代码时做职责划分的原则是一样的,模式只是约束了类之间的拓扑关系,并没有免去你做好内聚的职责。
4.2 性能损耗与调试成本:消息多绕了一圈
中介者模式引入了一个间接层,消息从一个同事对象到另一个同事对象,必须多经过一次转发。这个性能损耗在普通业务系统里几乎可以忽略不计,但如果通信频率非常高、消息量非常大,中转路径就会成为瓶颈。
我在一个实时对战服务器里做过测试:场景是一百个玩家实时同步位置,如果玩家之间直接通信,每个消息一跳就能到达目标;经过中介者转发后,消息要经过中心节点再分发到目标,网络开销差不多翻倍。对于实时性要求极高的场景,这种间接层就要慎重。
调试成本是另一个要考虑的点。直接通信时,调用链很清晰,出问题顺着调用栈就能找到源头。中介者模式下,消息在同事类和中介者之间来回传递,调用栈会变深,排查问题时要多绕一圈。我记得有一次线上事故,一个消息在中介者里被转发错了对象,找问题的过程相当痛苦,因为各个同事类的方法日志都显示“消息已发出”,只有把中介者的转发日志全部拉出来比对,才定位到是判断条件写错了。
但要注意,这些问题不是“模式不好”,而是“模式不适合这个场景”。中介者模式的收益在可维护性上,代价在运行效率和直接性上。当你觉得“这个模式写起来好绕”的时候,不妨停下来想想:你是真的需要这种解耦,还是只是在硬套模式。
4.3 一个务实的决策清单
我用中介者模式之前,会过一遍下面的清单,帮自己做出判断:
| 判断条件 | 适合用中介者 | 不建议用中介者 |
|---|---|---|
| 交互对象数量 | 3个及以上 | 只有2个 |
| 交互是否多对多 | 是,互相通信 | 否,只有单向调用 |
| 交互逻辑改动频率 | 频繁,每次改动影响多个对象 | 稳定,很少变化 |
| 业务逻辑复杂度 | 复杂度高,需要封装 | 简单直接,一眼看懂 |
| 对性能敏感度 | 不敏感,可接受一次转发 | 极敏感,需要最短路径 |
两个对象之间的通信,直接用引用就好,硬套中介者模式只会增加代码量、降低可读性,这是“过度设计”。三到四个对象交互,并且交互逻辑经常需要调整,中介者模式就很合适。二三十个对象交互,如果不是中介者模式,别的方案也很难收拾这个复杂度,但要注意中介者的内聚问题,不要把一个类写成两三千行。
记住一点:设计模式是解决问题的工具,不是用来炫技的。如果一段代码用普通写法清晰又简单,就不用为了“符合模式”去硬拗结构。中介者模式真正要解决的问题是“对象越多,交互越乱”,而不是“代码看起来不够设计感”。
5. 常见问题与排查技巧实录
5.1 高频问题速查表
我在学习、使用和帮助同学排查中介者模式相关代码时,整理了一些高频问题,做成一个速查表供参考:
| 问题表现 | 可能原因 | 解决方案 |
|---|---|---|
| 编译报错,找不到中介者方法 | 抽象同事类里持有的是抽象中介者类型,但调用了具体中介者才有的方法 | 把需要同事类调用的方法都定义到抽象中介者接口里,或者将持有的类型改为具体中介者 |
| 所有同事都收到了消息,但只需要发给部分目标 | 中介者转发时使用了广播逻辑,没有做目标筛选 | 在消息中携带目标信息,中介者收到后先判断再转发,就像聊天室里user != sender那个判断 |
| 同事类之间还是在直接调用 | 对模式理解不够,图省事直接引用了对方对象 | 强制约束,代码审查时检查同事类中是否存在其他同事类的引用 |
| 中介者类越来越大,改起来越来越怕 | 交互逻辑全部堆在一个类里,内聚性崩塌 | 按业务子域拆分中介者,上层中介者协调下层中介者,形成有层次的中介结构 |
| 消息丢失,没有任何转发日志 | 中介者内部逻辑异常,被吞掉了异常或提前 return | 在中介者转发入口和出口都加日志,消息进和出分开记录 |
| 初始化顺序问题,同事类构造时就需要发消息,但中介者还没准备好 | 构造顺序不当 | 先创建中介者,再创建同事类并把中介者传给同事类;如果同事类需要在构造时发消息,要确认中介者已经完成初始化 |
5.2 实操心得:怎么调试和避坑
调试中介者模式代码,我有个习惯:给中介者的入口和出口都加日志。入口就是sendMessage这类方法的开头,记录“谁发了什么消息进来”,出口就是实际转发调用的地方,记录“把这条消息转发给了谁”。这两个日志一对,就能快速判断问题出在同事类还是中介者。
还有一个小技巧,把中介者的核心转发逻辑单独抽一个方法出来,比如route(User sender, String message, Class<?> targetType),用路由这个词来命名,思路会清晰很多。因为中介者本质上就是在做消息路由:接收消息,根据规则决定转发给谁。把路由逻辑独立出来之后,新增转发规则就只需要改这一个方法。
排查“收不到消息”的问题时,很多人会盯着同事类的receive方法找原因,其实大概率问题出在事件监听机制上。检查中介者是否把所有需要接收消息的同事都注册进来了。漏注册一个对象,就跟开会没人通知你一样,你还以为是网络问题,其实是压根不在通知名单里。我排查过一次,最后发现是addUser方法在某个分支里提前 return 了,导致一个人没被加进列表,花了两个小时才定位到。
另一个容易踩的坑是把中介者做成全局单例后,状态没清理干净。尤其是用注册列表保存同事对象时,如果同事对象需要被销毁,而中介者的列表里还保留着引用,会造成内存泄漏。我看到过因为聊天室功能关闭但用户列表没清空,导致内存占用一直涨的案例。解决方案是给中介者加一个removeUser方法,对象销毁时通知中介者把它从列表里移除。
5.3 大作业和面试里的加分写法
如果你是拿中介者模式做设计模式大作业,或者准备面试时被问到中介者模式,有几个点可以额外深入。
大作业方面,别只写聊天室,太俗了,面试官每年都要看几百个聊天室案例。可以试试这些更有区分度的方向:电梯调度系统(多部电梯与多个楼层按钮交互)、机场塔台系统(多架飞机与多条跑道协调)、智能家居中央控制器(多个设备协同,比如回家模式要同时开灯、开空调、播放音乐)。尤其是智能家居这个方向,跟现在物联网热点贴合紧密,又能把中介者模式的优点展示得很充分——设备各司其职,全部通过中央控制器协调。
面试方面,建议主动讲清楚三个东西:第一,中介者模式解决了什么问题,用图或例子说明“多对多的引用关系变成了星型关系”;第二,它有什么代价,能说出中介者膨胀问题才是真懂;第三,用一个你实际写过或见过的场景举例,说明为什么在那个场景里需要中介者模式。能把这三个问题讲透,面试官基本不会觉得你只会背定义。
还有一点可以提:中介者模式和“最小知识原则”(Law of Demeter,也叫迪米特法则)的关系。中介者模式是该原则的典型实践——每个对象只和它直接认识的对象通信,不通过第三者认识其他对象。讲清楚这一层,说明你对设计原则和设计模式之间的关联有自己的思考,这是从“会用模式”到“理解模式”的分水岭。
6. 我的个人体会
我现在写代码时,每次发现某个类里同时出现了三四个其他类的方法调用,就会本能地停下来想想:是不是应该在这里引入一个中介者?这个习惯就是学了中介者模式之后养成的。
这个模式让我重新理解了软件设计里两个重要的词,一个是“间接”,一个是“收敛”。直接调用确实快,但间接层带来的是灵活性和可维护性。中介者模式就是用一个可控的间接层,把无处不在的调用关系收敛到一个点,让混乱变得有序。它解决的问题不是“代码能不能跑”,而是“代码能不能改”。
包括现在很火的多Agent系统设计,我看到那些主打成模式的时候,第一反应就是:这不就是中介者模式在新的技术形态下换了件衣服吗?技术的形态一直在变,但设计思想是稳定的。学会透过现象看模式,比背下二十三个模式的代码要重要得多。这也是设计模式真正值得花时间学习的原因。