中介者模式实战:从网状依赖到星状依赖的重构指南
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); } }乍一看好像没问题但接着看PaymentServicepublic 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 注入常见的同事类注册方式有三种构造器注入通过构造函数传入中介者强制要求同事必须有中介者才能创建。setter 注入提供setMediator方法灵活但容易漏调用。延迟查找同事本身不持有中介者需要发送通知时从容器里拿。解耦最强但调试困难。我第一次重构时用的是 setter 注入因为改动最小。但后来发现一个问题同事对象常常在 Spring 容器里已经被组装完了结果setMediator没被调用运行到某个业务分支才抛 NPE而且报错还不明显得一路 debug 才能确认是不是中介者没注入。现在我的习惯是能用构造注入就用构造注入。如果必须依赖框架的循环依赖限制就用一个专门的MediatorRegistry来做注册并提供一个assertAllColleaguesRegistered()的检查方法在应用启动时把所有同事类拉出来验证一遍确保没有漏注入。这个检查我建议写成一个PostConstruct方法成本极低但能省掉很多半夜被叫起来排错的麻烦。4.3 中介者内部到底要不要做“通知模板”真实业务里同一个动作往往要触发多个下游的“相似”处理。比如订单创建之后要扣款、要减库存、要发短信、要记日志。在handleOrderCreated里写成四行顺序调用没问题。但一旦这些步骤之间需要一致的“成功/失败统计”“耗时监控”“补偿入口”逐行写就容易漏。我的技巧是在中介者内部封装一个执行模板用模板方法模式把公共行为收敛起来。伪代码大概是protected void executeSteps(Order order, ListStep 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()等待结果再继续。伪代码CompletableFutureVoid future1 CompletableFuture.runAsync(() - logisticsService.arrangePickUp(order)); CompletableFutureVoid 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只允许被触发一次。这在复杂流程里非常有用。也可以用简单的HashSetOrderId记录已经进入过某流程的订单重复进入直接拒绝。7.4 我现在的行为准则把项目里沉淀的经验总结成几条规矩供你参考先画出对象间真实存在的调用关系图。如果超过 5 个对象存在两两交互再考虑中介者如果只有 2~3 个先忍住。中介者接口按业务事件拆分不要追求一个接口解决所有事情。中介者只调度不做事。所有具体业务实现都留在同事类内部。给中介者的核心路径写测试。它是最容易出回归 bug 的地方。定期审视中介者的膨胀程度。如果它超过 1000 行先把它的编排逻辑拆成若干“流程节点类”再塞回中介者的方法里。8. 最后一个想说的经验模式是手段不是终点我见过太多人学设计模式是为了在代码评审的时候“秀”一下——这个类应该是工厂、那个类应该是单例、这个地方不来个中介者都不好意思。但我实际重构那个订单系统之后最深的感觉是模式解决的是实际的痛苦而不是让代码看起来更像教科书。中介者模式不是银弹它有自己的代价复杂性被集中到了中介者身上中介者本身的测试和维护成本非常高。如果你在系统里感觉到“每个类都在处理通信逻辑、改一个需求要牵连很多类”那中介者值得作为一个候选方案。如果只是因为“书上说了要低耦合”就硬套上去那只会让代码更绕。到目前为止我自己的项目里真正被我长期使用的中介者都是那些业务规则本身比较清晰的场景——订单流程、审批流、多人协作状态同步。这类场景下中介者带来的“单一编排点”价值远远大于它的间接层损耗。最后分享一个具体的判断技巧下次你准备两个对象互相调用时问自己一句——“这两个类以后会不会因为第三方加入而改变调用关系”如果会就把它们隔开让中介者将来去协调如果不会直接调用别绕。这个思路比死记模式的 UML 图有用得多。