从中介者模式到多Agent系统:Java实现与架构演变
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 ListUser 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给storestore里的reducer处理完状态变化后再通知所有订阅的组件更新。组件与组件、组件与状态之间完全通过store中转。3.2 热词观察多Agent设计里的“主从模式”与中介者最近“多Agent系统”这个概念特别火尤其在AI应用开发领域。我在看多Agent设计相关资料时注意到智能体设计模式里经常提到“主从模式”一个主Agent负责调度多个子Agentsubagent各司其职。然后有人提出一个很有洞察的看法主从模式本质上就是把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系统设计我看到那些主打成模式的时候第一反应就是这不就是中介者模式在新的技术形态下换了件衣服吗技术的形态一直在变但设计思想是稳定的。学会透过现象看模式比背下二十三个模式的代码要重要得多。这也是设计模式真正值得花时间学习的原因。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →