☰
中介者模式详解:从网状耦合到星型解耦的架构实践
2026/10/5 0:28:12 网站建设 项目流程

早几年写业务代码的时候,我经常在一个页面里塞十几个回调,A组件改状态要通知B,B要联动C,C又要刷新D,最后改一个需求,牵一发动全身,线上出 bug 都找不到是哪一层传串了。后来系统学了一遍 GoF 的 23 种设计模式,做到第 18 个——中介者模式(Mediator Pattern)的时候,才真正想通:好架构不是"大家都能直接对话",而是"大家都别说话,让一个中间人统一传话"。

中介者模式属于行为型设计模式,核心思想通俗到不行:把对象之间的多对多交互,改造成对象与一个中介者之间的多对一交互。这个中介者负责协调各个对象的消息流转,对象之间不再互相持有引用,改而只认中介者一个"上级"。这篇文章不绕理论,直接讲清楚它解决什么问题、代码怎么写、真实场景怎么落、以及那些文档里不会告诉你的坑。写代码的、做架构的、做游戏开发的,或者正在赶设计模式期末大作业的同学,都能照着思路直接改造自己的项目。

1. 中介者模式到底在解决什么问题

1.1 先看一段让程序员上火的代码

先回想一下没有中介者的日子。假设你在做一个下单页面,用户点击"提交订单"后,至少有三个模块要协作:订单模块要创建订单、库存模块要扣减库存、消息模块要发送通知。如果让这些模块直接互相调用,代码大概会长这样:

public class Order { private Inventory inventory; private Notifier notifier; public void submit() { boolean ok = inventory.deduct(); if (ok) { notifier.send("下单成功"); } } } public class Inventory { private Order order; private Notifier notifier; public void restore() { order.markCancelled(); notifier.send("库存已恢复"); } }

看到没有,Order 知道 Inventory,Inventory 反过来又知道 Order,两个类还必须都认识 Notifier。这还只是三个模块,要是再加一个优惠券模块、一个物流模块、一个积分模块,这些类之间的依赖连线就变成了一个蜘蛛网。谁改了构造方法,所有引用它的地方全部编译报错,改一个需求嘴上说是五分钟,实际上是一下午在修耦合。

这种网状交互结构的问题不只是"代码丑",它让模块无法独立测试、无法替换、无法复用。Order 想单独单测,得先 mock 掉 Inventory、Notifier 和半张依赖图。这场景是不是特别熟悉。

1.2 从网状耦合到星型解耦

中介者模式的思路就是把上面的蜘蛛网捋直,所有对象不直接通信,而是把消息交给一个中介者,由中介者决定转发给哪些对象。结构从"多对多"变成"多对一",调用关系从网状变成星型。

用个生活类比就好懂了:以前没有租房中介的时候,你得自己联系房东、联系物业、联系维修工、联系室友,每个人你都得记电话、谈条件、处理纠纷。有了中介公司之后,你只需要面对一个中介,房子有问题找中介,合同有疑问找中介,房租交给中介。你和其他房源方、房东、维修服务商之间没有直接联系,所有信息都在中介这里中转。

对应到系统设计上,"房东""维修工""室友"就是各个业务模块,"中介公司"就是 Mediator 对象。每个业务模块只需要持有中介者的引用,不需要知道别的模块的存在。后续增减模块,你只需要改中介者内部的转发逻辑,不用动其他同事类。这个思想在很多成熟框架里都有影子,比如 MVC 里的 Controller 就充当了 View 和 Model 之间的中介者,MQ 消息中间件本质上也是让各服务通过消息队列间接通信。

2. 模式的核心结构与角色职责

2.1 四个角色的分工

中介者模式的标准类图里坐着四个角色,我在项目里习惯这样理解它们的职责:

  • Mediator(抽象中介者):定义通信接口,声明一个通用的消息转发方法,比如send(String message, Colleague colleague)。
  • ConcreteMediator(具体中介者):持有所有同事对象的引用,实现具体的协调逻辑,核心的"转发规则"都写在这。
  • Colleague(抽象同事类):定义业务方法,同时持有一个中介者引用,需要和其他对象通信时,调用中介者的方法。
  • ConcreteColleague(具体同事类):实现自己的业务逻辑,通过中介者与其他同事类交互。

这四个角色落在一个典型例子里就是:聊天室是具体中介者,每个用户是具体同事,用户发消息时不是直接发给别人,而是发给聊天室,聊天室再把消息广播给其他所有用户。

我把这种结构对比写出来,方便你做项目时直接套用:

角色核心职责对应业务例子
Mediator定义转发消息的抽象接口聊天室接口、调度中心接口
ConcreteMediator维护同事引用,实现转发规则具体聊天室、具体调度中心
Colleague持有中介者引用,触发通信用户基类、组件基类
ConcreteColleague各自独立的业务逻辑具体用户、具体 UI 组件

2.2 关键点:同事对象之间的"盲盒"通信

很多初学者上手会犯一个理解误区:觉得中介者模式就是把通信逻辑从同事类里抽出来,放到中介者类里,仅此而已。这只是表面。最核心的点在于:具体同事类之间完全不感知对方的存在。

什么意思?A 同事发消息给中介者时,它并不知道这条消息会被转发给谁;B 同事可能接收,C 同事可能无视,这些都取决于中介者内部的逻辑。这让同事类变成了一个"盲盒",发给中介者就完事,不用关心消息的最终去向。这种"盲"带来的是极高的解耦度:新增一个同事类,不需要改任何现有同事类,只需要在前面的中介者里注册一下、调整转发规则就行。

我在实际项目里最直观的感受是,重构前 UI 组件互相引用,每次加一个新页面组件都战战兢兢,生怕遗漏了哪条联动线;改成中介者后,新组件只管往中介者注册自己,中介者统一决定消息怎么分发,心态瞬间就不一样了。当然,"盲"也带来了代价:中介者内部逻辑会变复杂,这个矛盾后面我专门讲怎么缓解。

3. 动手实现一个迷你聊天室

3.1 用 Java 写一个最小可运行的例子

理论讲十遍不如代码跑一遍。我写一个最经典的聊天室案例,直接在本地新建一个ChatRoomDemo.java就能跑,非常适合设计模式大作业参考或者自己练手。

先定义抽象中介者和抽象同事类:

// 抽象中介者 public abstract class Mediator { // 注册同事到中介者 public abstract void register(Colleague colleague); // 转发消息:colleague 是消息发送者 public abstract void send(String message, Colleague colleague); } // 抽象同事类 public abstract class Colleague { protected Mediator mediator; protected String name; public Colleague(String name, Mediator mediator) { this.name = name; this.mediator = mediator; } // 通过中介者发送消息 public abstract void send(String message); // 接收消息 public abstract void receive(String message); }

再写具体中介者——聊天室:

import java.util.ArrayList; import java.util.List; public class ChatRoom extends Mediator { private List<Colleague> colleagues = new ArrayList<>(); @Override public void register(Colleague colleague) { colleagues.add(colleague); System.out.println(colleague.name + " 加入了聊天室"); } @Override public void send(String message, Colleague sender) { // 广播给除了发送者之外的所有成员 for (Colleague colleague : colleagues) { if (colleague != sender) { colleague.receive(message); } } } }

最后写两个具体同事类,模拟两个用户:

public class User extends Colleague { public User(String name, Mediator mediator) { super(name, mediator); } @Override public void send(String message) { System.out.println("[" + name + "] 发送消息:" + message); // 自己不关心消息发给谁,交给 mediator 处理 mediator.send(message, this); } @Override public void receive(String message) { System.out.println("[" + name + "] 收到消息:" + message); } public static void main(String[] args) { Mediator chatRoom = new ChatRoom(); User alice = new User("Alice", chatRoom); User bob = new User("Bob", chatRoom); User carol = new User("Carol", chatRoom); chatRoom.register(alice); chatRoom.register(bob); chatRoom.register(carol); bob.send("大家好,我是 Bob"); } }

运行后输出效果是:Bob 发送消息,聊天室广播给除 Bob 外的 Alice 和 Carol,两个人都能收到。整个过程里 Alice、Bob、Carol 三个对象之间没有互相持有引用,谁发消息都只和自己的中介者打交道。

3.2 这段代码里容易被忽略的三个细节

这段示例虽然短,但有三个细节值得细品。

第一,send方法里的colleague != sender判断,本质上是中介者的路由规则。不同业务里这个规则完全不同:可能是按用户 ID 定向转发,可能是按角色转发到特定人群,也可能像上面一样广播给所有人。中介者的价值就是把这段路由规则集中在一个地方,而不是散落在各个同事类里。

第二,Colleague构造的时候就把mediator传进去了,这说明同事对象依赖的是抽象中介者而非具体聊天室。面向抽象编程以后,替换中介者实现(比如改成 VIP 过滤版的聊天室)不需要改任何同事类代码。

第三,注册动作mediator.register(user)是显式进行的。中间者必须拿到同事对象的引用才能真正转发,这一步漏了就是经典的"发消息没反应"故障。我在实际项目里见过有人漏掉注册环节排查了半天,后来建议把注册逻辑写到工厂方法里,从源头保证"创建即注册",比靠业务调用方自觉要稳得多。

4. 真实项目中的应用场景,不只是聊天室

4.1 机场塔台调度:中介者模式最形象的类比

聊天室这个例子太常用了,以至于很多人以为中介者模式只适合"广播消息"这类场景。实际上最具代表性的案例是机场塔台调度——几乎每本设计模式书上都会提到这个例子。

想一想机场的运行逻辑:机场里有客机、货机、地勤车辆、跑道保养车、廊桥操作员,如果每架飞机降落前要自己跟地勤车确认有没有冲突、跟廊桥确认对接位置、跟其他飞机确认间距,那整个机场的通信链路会爆炸。而现实世界中,所有飞机和车辆只跟塔台对话,塔台统一安排降落顺序、分配跑道、协调地面车辆。飞机之间不用互相通信,地勤车也不用知道哪些飞机会来。

对应到代码架构,飞机、地勤车就是同事类,塔台就是中介者,塔台的调度算法就是中介者内部的消息路由规则。这个例子给你一个很重要的启发:当一组对象的行为彼此存在先后依赖和资源冲突时,中介者模式特别好使。比如任务调度系统里,多个任务抢占同一个资源,与其让任务之间互相同步协调,不如让一个调度中心统一仲裁。

4.2 UI 组件联动:全选按钮和复选框的经典联动

如果你做过前端或桌面端 UI,一定处理过"全选/全不选"这个需求:页面里有一个全选按钮、一行表头的选择框、十行列表的选择框。手动写联动逻辑时,全选按钮状态变化要通知十个 checkbox,十个 checkbox 中任何一个变化又得通知全选按钮和表头,还要判断是否全部选中。

不用中介者,这二十几个组件的状态同步逻辑会散落在每个组件的 change 事件回调里;用中介者,思路就变成这样:

class CheckBoxMediator { private CheckBox selectAllBox; private List<CheckBox> itemBoxes; void onSelectAllChanged(boolean checked) { for (CheckBox box : itemBoxes) { box.setChecked(checked); } } void onItemChanged() { boolean allChecked = itemBoxes.stream().allMatch(CheckBox::isChecked); selectAllBox.setChecked(allChecked); } }

每个复选框只感知中介者,不感知其他兄弟组件。当你后续要新增一个"反选"按钮,或者一个"仅看未选"的筛选开关,不需要去修改每一个旧组件,只需要在中介者里增加分支逻辑,扩散到其他组件线上的改动就是零。

我实际做过一个电商后台的筛选面板,十几个筛选条件彼此联动,当时没有中介者,改一处联动逻辑要顺着事件链翻三个组件文件。后来重构时把筛选条件全部收敛到一个FilterMediator里,新增筛选条件只在这个类里加一行开关判断,爽了很多。

4.3 微服务编排中的"中介者"思想

把中介者模式放大到系统架构层面,你会发现很多系统都在用这个思想,只是没叫这个名字。

比如微服务拆分之后,订单服务、支付服务、库存服务之间不该直接点对点调用,否则服务间的通信依赖会变成一团乱麻。常规做法是引入一个编排层或消息中心(Kafka、RabbitMQ 那一类),订单服务把"订单已创建"这个事件丢到消息队列,支付服务、库存服务自己去订阅关心的消息。服务和消息中心之间依然是"多对一"的关系,消息中心承担了中介者的角色。这个思想被叫做"事件驱动架构",本质上是中介者模式在分布式场景下的升级版。

所以中介者模式对你的价值不止是"写一个类"这么简单,它是建立一套"间接通信"的思维框架。理解了中介者,再去理解事件驱动的发布订阅系统,会发现很多概念都是相通的,学习成本会直线下降。

5. 常见问题与避坑指南

5.1 当心"上帝中介者":中介者膨胀了怎么办

中介者模式最遭人诟病的缺点,就是具体中介者容易变成"上帝对象"(God Object)——所有交互规则都堆在这里,几千行代码,改一处怕碰坏三处,耦合全从同事类转移到了中介者内部,等于换了个地方堵。

这个问题我的处理原则是:中介者里只放"路由"和"协调"逻辑,不放业务实现。比如聊天室,中介者只负责"把消息转发给谁",具体的消息持久化、敏感词过滤、消息格式化,应该委托给专门的服务类,中介者只调用这些服务,不亲自写几百行过滤规则。再细分的话,如果中介者内部的分支实在太多,可以考虑按业务维度拆成多个中介者,分别协调不同领域的通信,避免单一中介者承载过重。

判断膨胀有一个很实用的标准:如果中介者里的一个if else分支超过三四个,或者你写方法时不得不用非常长的注释来解释"这步是干什么的",基本就该拆了。中介者的方法体应当短小,入口进来、分析消息、打到对应处理服务、返回,保持每段逻辑寥寥数行,这样职责边界才清晰。

5.2 中介者和观察者模式别搞混

学设计模式的人百分之百会在这两个模式上面犯迷糊:中介者模式里,同事对象调中介者的方法,中介者再调用其他同事的方法;观察者模式里,发布者通知订阅者。两者都是对象间通信的解耦方案,到底啥区别?

我的记忆办法很简单:

  • 观察者模式是单向广播:发布者不关心谁在听,订阅者接收通知后自己处理。典型的"消息来了,大家各自干各自的事"。
  • 中介者模式是双向协作:中介者不仅负责转发,还承担了"协调者"的角色,它决定哪条消息该去哪个同事,甚至可以编排多个同事的调用顺序,比如"先扣库存、再发通知"。

换句话说,观察者模式解决的是"通知"问题,中介者模式解决的是"协作"问题。中介者内部完全可以用观察者模式来实现,组合使用非常常见。在聊天室例子里,如果把中介者改成事件总线,让每个用户注册订阅事件,代码可以写得和中介者模式效果相似,但设计意图上"谁来控制通信规则"的区别还是能看出来的——中介者是主动协调,事件总线是被动广播。

5.3 消息粒度高导致的方法名过泛,该怎么收场

中介者的接口方法如果只定义成通用形式,比如send(String message),消息里塞的内容太杂,会出现一种尴尬局面:同事对象把消息发出去,中介者打开消息一看,发现消息里既没有目标标识,也没有消息类型,根本不知道该转发给谁。这就是"过泛接口"的坑。

实操中我给中介者接口加一个"消息类型"参数,让路由规则有判断依据:

public void notify(String eventType, Object data, Colleague sender);

同事对象发送时注明事件类型,比如mediator.notify("ORDER_CREATED", order, this),中介者里用switch (eventType)决定去协调哪些同事完成后续动作。这样接口看起来还是通用的,但消息内容有了结构化语义,路径分发的代码就好写很多。

还有一种做法是先把消息封装成事件对象,把事件类型、来源、负载全部装进去,类似:

public class Event { public String type; // 事件类型 public Colleague source; // 来源同事 public Object payload; // 携带的数据 }

这种做法我见过在游戏开发中尤其常用,因为游戏对象之间的交互类型非常多,通用事件对象能够灵活承载各种"触发信号"。你看,这也是设计模式与游戏完美开发结合起来比较典型的实践:游戏里的 AI 系统、音频系统、动画系统之间交互频繁,用一个全局事件中介者统一分发,各系统都能保持独立演进。

5.4 测试和调试的两个独家经验

最后分享两个踩过坑换来的实操经验。第一个是关于单测的。中介者模式把通信逻辑集中了,测试同事类反而变得很轻松,创建 mock 中介者就行,同事只跟 mock 交互,断言 mock 收到了正确的调用。但测试具体中介者时要格外小心:中介者内部持有真实的同事列表,测试时传 mock 同事进去,要确认注册顺序不会影响路由结果,确保中介者逻辑不依赖列表的插入顺序,避免测试偶发失败。

第二个调试经验,场上出现"消息莫名其妙没人处理"的 bug 时,优先检查两个地方:先查同事对象有没有成功注册进中介者,再查中介者收到消息后走的是哪个分支,很多问题都是注册漏掉或者事件类型拼写不一致引起的。给事件类型定义常量或枚举,而不是散落字符串字面量,这一类问题能少一大半。

6. 中介者模式和其他模式的联动玩法

6.1 中介者 + 观察者,把对象关系理得清清楚楚

前面提过中介者可以和观察者组合,这里展开细说一下。我做过的项目里,比较舒服的组合方式是"中介者做骨干,观察者做细节":

中介者负责确定规则:A 模块的事件需要通知哪些模块、先后顺序是什么;每个模块内部再用观察者机制监听中介者派发过来的事件。同事类只向中介者注册一次,具体怎么处理收到的消息,是模块内部的事,可以用观察者订阅和解耦内部的多个子组件。

举个实际例子,我在写一个 FPS 小游戏时,角色血量变化会牵动 HUD 血条显示、音效播报、成就系统判断、以及对战逻辑。我没有让角色直接调用这四个系统,而是做了一个GameEventMediator,角色只发"HP_CHANGED"事件给中介者,中介者按注册关系把事件派发给四个系统;四个系统内部各自有观察者监听事件。新增一个"死亡回放系统"时,只需要在中介者里注册它并声明关心哪些事件,角色类和已有系统完全不用动。

6.2 中介者 + 工厂模式,保证同事注册不漏

上面提过的"创建即注册"思路,搭配简单工厂可以形成一套非常顺滑的代码结构。让我用一个更完整的示意来讲这个组合:

假设一个窗口系统里有主窗口、工具栏、面板、状态栏四个组件,每个组件都在构造时要求传入中介者,同时中介者必须主动注册该组件。如果每个组件的创建都散落在各处,漏注册几乎是必然的。把创建逻辑收归到一个工厂方法里:

class ComponentFactory { public static UserPanel createUserPanel(Mediator mediator) { UserPanel panel = new UserPanel(mediator); mediator.register(panel); return panel; } }

这样"创建组件并注册中介者"就变成一个原子操作,调用方拿到的组件天然可用,不需要记"创建完之后手动注册"这个步骤。这个组合模式对小组件特别友好,是我多次实践后比较推荐的构造方式。

6.3 什么时候真的不该硬套中介者

中介者模式虽好,但也不是所有场景都适用的。我见到过反模式案例:只有两三个对象交互,也被强行介入一个中介者类,结果业务逻辑被拆得零零碎碎,读代码要跳三个文件才能看明白一段完整流程,这就是过度设计。

我的判断标准很朴素:当对象之间的交互是"一对一""点对点"且关系稳定时,直接持有引用比引入中介者更简单;当交互对象超过三个且相互牵扯、行为存在先后协调时,才值得考虑中介者。简单场景直接连线,不搞花活;复杂场景才需要做信息收口。设计模式的价值不是"越多越好",而是"多到刚好顺手"。

另外,如果你的交互场景变更非常频繁,其实可以先考虑用事件总线(EventBus)或者消息队列这种现成的"中介者",不要自己造轮子。在单体应用里引入一个成熟的轻量 EventBus 库,往往比自己实现一个中介者更省心,因为它已经处理好了线程调度、解耦注册、缓存这些边角问题。中介者模式的"手工版"更适合需要自定义复杂路由编排的场景,或者教学阅读、设计模式大作业这类需要展示模式本身结构的场合。

7. 最后的实战心得

这篇文章写下来,是我对中介者模式的完整复盘。从最初一提到"多个对象间通信"就反射性地加中介者,到后来学会先画交互图、数连线,再决定该不该用,整个过程重新走了一遍。我个人的体会是:中介者模式真正的价值不是提供某个"最佳实践方案",而是逼迫你在设计组件交互时,先想清"谁跟谁需要说话、消息由谁来决定去向",逼你走出事件间直接调用的舒适区,把依赖梳理清楚。

最后的提示是——动手写一次聊天室,或者上手重构一个自己项目里最乱的多组件交互页面,比你读十篇设计模式文章都有用。把这段逻辑跑通,你就能切实感受到"网状变星型"之后改动成本骤降带来的爽感。后面我会继续把剩余的几个行为型模式逐一拆解,如果有哪篇能帮你在实际开发或期末作业里少走一段弯路,这篇码字就值了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询