☰
中介者模式实战:从网状依赖到星状依赖的重构指南
2026/10/15 12:43:54 网站建设 项目流程

1. 一个让我改到崩溃的聊天室,才真正理解中介者存在的意义

先说个真实经历。某年我在做一个内部协同工具,里面有个群聊模块,一开始图省事,所有消息都走Client直连Client,客户端持有其他所有客户端的引用。需求刚上线时只有十几个人用,一切正常。后来业务方要求加入“消息已读”“@提醒”“文件传输进度通知”,我改一次要动三四个类,每次联调都像拆炸弹——A 同学说“你改我的MessagePanel干嘛”,B 同学说“你的FileTransfer怎么又回调我”。

真正让我崩溃的是需求方说:加一个“全员禁言”。这个功能本质上是系统管理员向所有客户端广播一个状态,但在我当时的直连架构里,AdminPanel得知道每个Client的实例,得维护一份在线列表,还得处理客户端中途掉线的异常。代码越写越长,MainFrame里塞了六七个addListener,我自己看着都觉得反胃。

后来我去翻设计模式相关的资料,看到“中介者模式(Mediator Pattern)”这个词,第一反应是:这不就是把所有对象之间的网状依赖,改成星状依赖吗?当时觉得“这不就是加个中间层嘛,有什么稀奇的”。但真正动手重构之后,才发现这个模式解决的不只是“依赖多”的问题,它解决的是“依赖关系变化频繁”的问题。一个稳定的中间层,远比一堆随时可能被改动的对象引用要可靠得多。

如果你现在也遇到类似情况——对象之间两两通信,改一个功能牵连一片,联调时谁都在改谁的接口——那这篇内容就是写给你的。我会从一个真实项目的重构过程出发,把中介者模式的适用场景、核心结构、实现细节、常见误用和性能边界一次讲清楚。我默认你至少写过一段时间面向对象代码,知道类、接口、继承是什么,但不需要你已经背熟 GoF 那本书。

2. 中介者模式到底在解决什么:从网状依赖到星状依赖的思维转变

2.1 对象之间“两两认识”带来的维护噩梦

在没有中介者的情况下,多个对象要协作,最常见的方式就是互相持有引用。比如一个订单系统里,OrderService要通知PaymentService去扣款,扣完款要通知InventoryService去减库存,减完库存要通知LogisticsService去发货,还要通知MessageService给用户发短信。

如果这些服务之间都直接调用,那个依赖图就是一张网:

OrderService -> PaymentService OrderService -> InventoryService PaymentService -> InventoryService InventoryService -> LogisticsService OrderService -> LogisticsService OrderService -> MessageService ...

这还只是五个对象。每多一个对象,最坏情况下要新增 n 条依赖。更可怕的是,这些依赖是双向的:InventoryService减完库存之后可能还要回调OrderService更新订单状态,PaymentService失败时也要回调OrderService做补偿。每个对象都成了“万金油”,既要管自己的核心业务,又要知道别人怎么用。

这种设计在初期看起来“效率很高”——想通知谁,直接 new 出来调用就行。但一旦进入维护期,痛点会集中爆发:

  • 改动一个对象的行为,可能要连带改它认识的所有对象;
  • 新增一个参与者,要改所有需要感知它的地方,不是加一个类那么简单;
  • 对象之间的耦合让单元测试变得很难做,想单测PaymentService,你得先 mock 掉它引用的一堆东西。

2.2 中介者的核心主张:让对象只和“协调者”说话

中介者模式的做法是:把原本对象之间的直接通信,全部收拢到一个中介者对象上。所有参与者(Colleague)只认中介者,不直接认识彼此。中介者负责转发消息、编排流程、维护状态。

用订单系统的例子来说,就是让OrderService只通知OrderMediator“订单创建了”,然后由OrderMediator决定:要不要调PaymentService、调完PaymentService要不要调InventoryService、这些调用失败之后怎么处理。OrderService不需要知道PaymentService的接口长什么样,PaymentService也不需要知道OrderService的存在。

从图结构上看,这等于把 n x n 的网状依赖,压缩成了 n x 1 的星状依赖。所有对象都指向中介者,中介者再指向所有对象。代价是中介者本身会变复杂,但换来的是其他对象的“单纯化”。

2.3 用生活化类比理解:群聊和小脑的关系

我经常跟团队里新人说,中介者就像微信群和群主的关系。没有群的时候,你要通知十个人,得一个个私聊,还得自己记着谁看了谁没看。有了群,你只需要往群里发一条消息,群主(或者群规则)来决定谁该响应、谁不用响应。你和别人之间没有直接连接,你只对群负责。

另一个更贴切的类比是人体的“小脑”。小脑不直接产生运动指令,但它接收来自大脑皮层、肌肉、前庭系统的各种信号,然后统一协调。如果让每个肌肉直接跟大脑皮层通信,那大脑要处理的信息量会爆炸。中介者扮演的就是这个“协调中枢”的角色——它不一定是业务核心,但它是通信的核心。

3. 一个订单协同系统的重构实录:从直连调用到 Mediator 的完整落地

3.1 重构前的混乱现场

我拿一个真实做过的模拟项目来讲。这个项目叫“某跨平台订单协同系统”,早期代码里OrderService是这个样子的:

public class OrderService { private PaymentService paymentService; private InventoryService inventoryService; private LogisticsService logisticsService; private MessageService messageService; public void createOrder(Order order) { // 1. 保存订单 save(order); // 2. 扣款 paymentService.pay(order); // 3. 减库存 inventoryService.reduceStock(order); // 4. 通知物流揽收 logisticsService.pickUp(order); // 5. 发消息 messageService.notifyUser(order); } }

乍一看好像没问题,但接着看PaymentService:

public class PaymentService { private OrderService orderService; private InventoryService inventoryService; public boolean pay(Order order) { boolean ok = doPay(order); if (ok) { orderService.markPaid(order); inventoryService.reduceStock(order); } return ok; } }

问题立刻暴露了:OrderService调了PaymentService去付款,但PaymentService又回调OrderService,还越级去调InventoryService。逻辑分散在多个类里,每个类都承担了“流程编排”的责任。结果就是:想调整“先减库存再付款”还是“先付款再减库存”,你得同时改三四个类的调用顺序,改一处漏一处。

更糟的是InventoryService里也有一段类似的逻辑。当时的代码已经被改得没有人能说清楚“完整的下单流程到底是怎么走的”,大家都在靠猜。

3.2 按中介者模式的思路重构

重构第一步,定义中介者接口和参与者接口:

public interface OrderMediator { void handleOrderCreated(Order order); void handlePaymentSuccess(Order order); void handlePaymentFailed(Order order); void handleStockReduced(Order order); void handleLogisticsArranged(Order order); }

然后让各个服务实现一个通用的Colleague接口,持有中介者的引用:

public interface Colleague { void setMediator(OrderMediator mediator); }

OrderService重构后不再关心其他服务,只管保存订单,然后通知中介者:

public class OrderService implements Colleague { private OrderMediator mediator; @Override public void setMediator(OrderMediator mediator) { this.mediator = mediator; } public void createOrder(Order order) { save(order); mediator.handleOrderCreated(order); } // 其他只和订单自身相关的逻辑... }

真正复杂的流程编排全部收拢到OrderMediatorImpl里:

public class OrderMediatorImpl implements OrderMediator { private PaymentService paymentService; private InventoryService inventoryService; private LogisticsService logisticsService; private MessageService messageService; @Override public void handleOrderCreated(Order order) { if (paymentService.pay(order)) { handlePaymentSuccess(order); } else { handlePaymentFailed(order); } } @Override public void handlePaymentSuccess(Order order) { if (inventoryService.reduceStock(order)) { handleStockReduced(order); } } @Override public void handleStockReduced(Order order) { logisticsService.arrangePickUp(order); messageService.notifyUser(order); } @Override public void handlePaymentFailed(Order order) { orderService2.markPaymentFailed(order); messageService.notifyAdmin("支付失败,订单号: " + order.getId()); } // ... }

注意一个细节:OrderMediatorImpl里我保留了paymentService、inventoryService等具体依赖,因为这些参与者本身只负责自己的核心业务,不需要知道别人。中介者是唯一知道“完整业务流程”的地方,正是这个“唯一性”,让流程调整变得可控。

3.3 重构后发生了什么变化

重构完以后,最直观的感受是:各服务的职责边界清晰了。OrderService不再关心扣款成功以后怎么样,PaymentService不再关心是不是要喊别人来减库存。想调整“付款失败时的补偿策略”,只需要去OrderMediatorImpl里改,不再需要翻遍好几个类。

从测试角度看,单元测试也变简单了。每个Colleague只需要依赖一个中介者接口,测试时可以用 mock 的中介者替换真实实现,验证“我发出去的通知对不对”,而不用再去构建一整套上下游依赖。

这里有个表格,可以清楚对比重构前后的差异:

维度重构前重构后
对象间依赖数量服务间两两引用,最多 n×(n-1) 条每个服务只依赖一个中介者接口
业务流程位置散落在各服务内部集中在OrderMediatorImpl
调整流程成本需要找所有相关调用点改中介者内部编排即可
单元测试难度需要 mock 大量协作对象只需 mock 中介者
新增参与者代价可能要改多个关联对象新增一个同事,只注册给中介者
中介者复杂度无中介者成为核心,需要重点测试

不过别高兴太早,重构不是没有代价。OrderMediatorImpl很快就膨胀了,尤其当需求从“下单”扩展到“退款”“换货”“催发货”之后,中介者变成了一个大杂烩。这个坑我在后面专门用一节讲。

4. 代码实现里的关键决策:接口怎么切、同事怎么注册、职责怎么收敛

4.1 中介者到底应该做成大接口,还是按业务域拆小接口

很多人学中介者模式,照着书上的类图画一个Mediator接口,里面放SendMessage、ReceiveMessage两个方法,然后在实现类里写一堆if/else。这种写法在小 demo 里没问题,但真实项目里很快会吃灰。

我踩过的坑是:把中介者接口做得太宽。比如一开始我把“订单”相关的所有方法都塞进OrderMediator,后来又要处理“退款”“售后”,于是又往里加方法,接口越来越长,实现类越来越肥,每个实现方法都得处理很多无关分支。

后来我学到的做法是:按业务域拆分中介者接口。比如拆成OrderCreateMediator、OrderRefundMediator、OrderLogisticsMediator,甚至更细。每个同事类只依赖它实际会用到的那一两个中介者接口。这样做的好处是:参与者之间其实不会因为同一个“订单”就共享所有行为,按域拆可以避免中介者接口腐化。

如果实在不想拆那么多接口,另一个折中方案是使用泛型事件方法。用handle(OrderEvent event)代替一堆handleXxx(),事件对象里带类型和载荷。这样中介者接口很稳定,但代价是少了编译期类型检查,事件类型拼错要等到运行期才能发现。我的建议是:小系统用事件方法,系统一复杂就老老实实拆接口。

4.2 同事类如何持有中介者:构造注入优于 setter 注入

常见的同事类注册方式有三种:

  1. 构造器注入:通过构造函数传入中介者,强制要求同事必须有中介者才能创建。
  2. setter 注入:提供setMediator方法,灵活但容易漏调用。
  3. 延迟查找:同事本身不持有中介者,需要发送通知时从容器里拿。解耦最强,但调试困难。

我第一次重构时,用的是 setter 注入,因为改动最小。但后来发现一个问题:同事对象常常在 Spring 容器里已经被组装完了,结果setMediator没被调用,运行到某个业务分支才抛 NPE,而且报错还不明显,得一路 debug 才能确认是不是中介者没注入。

现在我的习惯是:能用构造注入就用构造注入。如果必须依赖框架的循环依赖限制,就用一个专门的MediatorRegistry来做注册,并提供一个assertAllColleaguesRegistered()的检查方法,在应用启动时把所有同事类拉出来验证一遍,确保没有漏注入。这个检查我建议写成一个@PostConstruct方法,成本极低,但能省掉很多半夜被叫起来排错的麻烦。

4.3 中介者内部到底要不要做“通知模板”

真实业务里,同一个动作往往要触发多个下游的“相似”处理。比如订单创建之后,要扣款、要减库存、要发短信、要记日志。在handleOrderCreated里写成四行顺序调用,没问题。但一旦这些步骤之间需要一致的“成功/失败统计”“耗时监控”“补偿入口”,逐行写就容易漏。

我的技巧是:在中介者内部封装一个执行模板,用模板方法模式把公共行为收敛起来。伪代码大概是:

protected void executeSteps(Order order, List<Step> steps) { for (Step step : steps) { try { metricRecorder.start(step.getName()); boolean ok = step.execute(order); metricRecorder.end(step.getName(), ok); if (!ok) { compensationHandler.handle(order, step); break; } } catch (Exception e) { metricRecorder.fail(step.getName(), e); compensationHandler.handle(order, step); break; } } }

这样中介者里的编排代码会短很多,而且每步的监控、补偿逻辑不会因为手滑而漏写。中介者作为“唯一的流程编排点”,加这种模板很有价值,因为它承担了整个系统里最容易出现“某一步失败但其他步还在跑”的职责。

5. 中介者和观察者、发布订阅模式的边界:为什么它们经常被搞混

5.1 中介者与观察者:一个管“依赖关系”,一个管“通知扩散”

很多人看到中介者有个“通知别人”的职责,就觉得它和观察者模式差不多。但它们解决的是不同层面的问题。

观察者模式解决的是“一个对象状态变化,怎么通知一群感兴趣的观察者”,重点是一对多通知。观察者不需要知道主题内部细节,主题也不需要知道观察者具体是谁。

中介者模式解决的是“多个对象之间复杂的交互逻辑”,重点是交互编排。中介者不仅负责转发,还负责决定消息的流向、顺序、是否需要聚合、失败时怎么办。

一句话区别:观察者让你“订阅即可收到”,中介者让你“我只跟中介说话,它来决定我跟谁协作、怎么协作”。

在实际的订单例子里,如果只用观察者,OrderService会成为主题,下单之后触发一个OrderCreatedEvent,然后PaymentService、InventoryService、LogisticsService都订阅这个事件。看起来也能实现,而且比中介者看起来更“松耦合”。但真正的问题是:观察者模式只负责“把事件推出去”,它不负责“保证整个流程按照预定顺序执行”。如果订单支付失败,后续步骤该不该执行,观察者模式下每个订阅者都要自己判断,或者要再加一个专门的“流程控制器”——而这个控制器,本质上就是中介者。

所以我的结论是:如果交互流程存在明显的顺序性、事务性和补偿需求,用中介者;如果你只是想让一个状态变化被多个地方感知,用观察者。

5.2 中介者与发布订阅模式:事件总线不等于中介者

还有一个常见的混淆是“中介者 = 事件总线”。很多系统里有一个全局的EventBus,各个模块post事件,其他模块subscribe。这确实收拢了通信,但它更像“发布订阅”,因为事件总线的核心是解耦生产者和消费者,它不关心谁消费、消费顺序怎么样。

而中介者模式更强调知道所有同事的状态,并能主动调度。发布订阅里发布者发出事件后就撒手不管了,中介者却通常是全程参与的——它要给参与者发指令、收反馈、决定下一步做什么。

举一个真实例子。某次我把一个系统里的“下单流程”用EventBus重写过,OrderService发完OrderCreatedEvent就结束了,后续全靠PaymentHandler订阅后执行。看起来很好,但有一天产品说要“支付成功后要实时检查库存,如果库存不够要回滚支付”,我在EventBus的架构里改得很痛苦,因为PaymentHandler内部做不了跨步骤回滚,它不知道库存检查在后面的哪个环节,也不知道怎么回头取消已经成功的支付。

如果当初用中介者,这个需求就简单得多:中介者调完paymentService.pay()之后,自己立刻调inventoryService.checkAndReduce(),检查失败就直接走handlePaymentFailed,一个方法搞定闭环。所以记住:中介者是知道完整流程的“导演”,事件总线是只管广播的“广播塔”。

6. 中介者模式的性能损耗与过度设计警戒线

6.1 多一个中间层,真的会慢吗

中介者把所有调用都收拢到一个对象,不可避免会引入一层间接调用。很多人担心性能,实际上这个损耗在绝大多数业务系统里是可以忽略的,多加一个方法调用级别大概在纳秒到微秒量级。真正需要警惕的不是这次间接调用,而是中介者内部如果串行执行大量 I/O 操作,拖慢了整个流程。

比如我重构的订单系统,中介者的handleStockReduced里,logisticsService.arrangePickUp()和messageService.notifyUser()都是网络调用,一个下单流程要顺序等两个网络 RTT。如果用并行的方式,总耗时能减半。所以在实现时,我经常建议在模板里允许某些步骤标记为可并行。

简单的做法:把 step 分成串行阶段和并行阶段,并行阶段用CompletableFuture.allOf()等待结果再继续。伪代码:

CompletableFuture<Void> future1 = CompletableFuture.runAsync(() -> logisticsService.arrangePickUp(order)); CompletableFuture<Void> future2 = CompletableFuture.runAsync(() -> messageService.notifyUser(order)); CompletableFuture.allOf(future1, future2).join();

但要注意:并行后,如果其中一个失败,补偿逻辑会更复杂。你需要决定是“全部成功才算成功”,还是“个别失败不影响整体”。这个决策点最好跟业务方对齐,别自己在代码里拍脑袋。我的建议是:跨服务写入类步骤尽量不要并行,因为回滚时很难保证一致性;纯通知、日志、监控类步骤才适合并行。

6.2 中介者膨胀问题:当“协调中枢”变成“上帝类”

中介者模式最大的反模式,就是中介者慢慢变成什么都管的“上帝类”。因为所有流程都集中在它这里,新需求一进来,开发者最容易的做法是往中介者里再加一个方法。久而久之,中介者的代码比所有同事类加起来还长,耦合从“对象之间的耦合”转移成了“对象与中介者之间的耦合”。

这不是模式的问题,是职责划分的问题。我给自己定了几条规矩:

  • 中介者只做“调度”,不写具体业务实现。扣款、减库存的具体逻辑留在各自的Colleague里,中介者只负责谁先谁后、失败怎么办。
  • 如果中介者的一个方法里出现超过 5 条依次调用的步骤,就要考虑拆流程节点。可以把“下单流程”拆成“订单创建节点”“支付节点”“库存节点”,每个节点是一个独立的类,中介者只负责把这些节点串起来。
  • 如果某个同事类只和另一个同事类交互,并不涉及第三方,那就不该走中介者。中介者的价值在于多方协作,两方直接通信反而更简单。

6.3 什么时候不要用中介者

不是所有多对象通信都需要中介者。我见过更离谱的用法:只有两个类需要单向调用,也硬塞一个中介者进去,美其名曰“为未来扩展做准备”。结果中介者什么也没协调,只是多绕了一圈,代码读起来更费劲。

我的标准很简单:当对象之间的协作关系基本固定、不会频繁变化时,直接依赖比中介者更好。比如LogService和MetricService之间的关系,基本不会变,直接调用足够。当中介者的引入不能明显降低“变更成本”,那它就是过度设计。

另外,如果对象之间天然是层次结构的,比如 Controller -> Service -> Repository,那也不需要中介者。中介者适合的是平级对象之间的网状协作,不是上下级之间的分层调用。

7. 我踩过的坑和行为准则:这些细节,书里不会写

7.1 坑一:把“通知”和“控制流”混在一个方法里

最早实现handleOrderCreated时,我直接把“扣款”“减库存”“发货”的调用都写进去了,看起来没什么问题。但到了要支持“部分退款”时,麻烦来了:部分退款之后不需要重新发货,但需要给用户发一条退款通知。我当时直接复用handleOrderCreated,往中间悄悄加了几个分支,结果“部分退款”流程也走了“减库存”逻辑,库存被扣了两次。

这个教训是:中介者的方法应该按业务事件拆分,不要一个方法包打天下。每个业务事件对应一个清晰的方法,方法内的分支要尽量减少。如果分支太多,说明事件粒度不对,应该拆成不同的事件方法。

7.2 坑二:中介者内部使用了具体类而不是接口

我早期为了省事,在OrderMediatorImpl里直接持有PaymentService的实现类,而不是PaymentService的接口。后来同事把PaymentService从 HTTP 调用改成了消息队列异步处理,整个OrderMediatorImpl跟着大改。

正确的做法是,同事类对外暴露窄接口,中介者依赖这些窄接口,不依赖具体实现。这样即使底层实现变了,中介者的编排逻辑基本不动。这也算是对依赖倒置原则的一次实际应用。

7.3 坑三:循环调用没有设防

同事 A 通知中介者,中介者让同事 B 做某事,B 做完又通知中介者,中介者再让同事 C 做某事。这个链路没问题。但如果中介者方法里不小心把同事 A 再次调动起来,循环就出现了,轻则死循环,重则重复扣款。

我后来在中介者里加了一个“事件去重”或者“状态机”的机制,比如同一个Order在同一个状态的handleXxx只允许被触发一次。这在复杂流程里非常有用。也可以用简单的HashSet<OrderId>记录已经进入过某流程的订单,重复进入直接拒绝。

7.4 我现在的行为准则

把项目里沉淀的经验总结成几条规矩,供你参考:

  1. 先画出对象间真实存在的调用关系图。如果超过 5 个对象存在两两交互,再考虑中介者;如果只有 2~3 个,先忍住。
  2. 中介者接口按业务事件拆分,不要追求一个接口解决所有事情。
  3. 中介者只调度,不做事。所有具体业务实现都留在同事类内部。
  4. 给中介者的核心路径写测试。它是最容易出回归 bug 的地方。
  5. 定期审视中介者的膨胀程度。如果它超过 1000 行,先把它的编排逻辑拆成若干“流程节点类”,再塞回中介者的方法里。

8. 最后一个想说的经验:模式是手段,不是终点

我见过太多人学设计模式,是为了在代码评审的时候“秀”一下——这个类应该是工厂、那个类应该是单例、这个地方不来个中介者都不好意思。但我实际重构那个订单系统之后,最深的感觉是:模式解决的是实际的痛苦,而不是让代码看起来更像教科书。

中介者模式不是银弹,它有自己的代价:复杂性被集中到了中介者身上,中介者本身的测试和维护成本非常高。如果你在系统里感觉到“每个类都在处理通信逻辑、改一个需求要牵连很多类”,那中介者值得作为一个候选方案。如果只是因为“书上说了要低耦合”,就硬套上去,那只会让代码更绕。

到目前为止,我自己的项目里,真正被我长期使用的中介者,都是那些业务规则本身比较清晰的场景——订单流程、审批流、多人协作状态同步。这类场景下,中介者带来的“单一编排点”价值远远大于它的间接层损耗。

最后分享一个具体的判断技巧:下次你准备两个对象互相调用时,问自己一句——“这两个类以后会不会因为第三方加入而改变调用关系?”如果会,就把它们隔开,让中介者将来去协调;如果不会,直接调用,别绕。这个思路比死记模式的 UML 图有用得多。

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

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

立即咨询