
1. 为什么你的系统会越改越烂中介者模式到底在解决什么问题先说一个我见过无数次的场景。一个项目早期功能简单几个类之间互相调用看着还挺清爽。比如一个模拟聊天室的Demo聊天窗口要通知用户列表刷新用户列表要通知消息面板更新消息面板又要回调给主窗口。前期每个类持有对方两三个指针互相new一下、调用一下功能就通了。但随着需求迭代这种“万物互联”的网状依赖会迅速失控。每增加一个新窗口你得先搞清它要和哪些已有模块通信每次改动一个类的内部接口所有直接调用它的地方全都要跟着改。代码编译时间越来越长改一个按钮的文案都要担心会不会影响十万八千里外的某个模块。这个时候你再回头看当初“简单直接”的互相调用实际上已经变成了一张谁也理不清的蜘蛛网。中介者模式就是冲着这个痛点来的。它的核心思想非常朴素把对象之间的多对多交互从网状结构压扁成星形结构。所有对象不再直接互相持有和调用而是统一通过一个“中介者”对象进行消息转发和协调。用大白话说以前是一屋子人扯着嗓子互相喊现在多了一个前台所有人有话都找前台说前台再帮忙转达。设计模式的书里通常把它归为行为型模式但我在实际项目里的体感是它更像是架构层面的治理手段。它关心的不是某一个算法怎么实现而是整个系统的对象之间“怎么说话、谁和谁说话、说话时遵循什么规则”。这种治理价值在项目中期、后期会体现得格外明显。适合看这篇文章的人我分三类第一类是刚接触设计模式、想搞懂中介者模式到底在什么场景下用的初学者第二类是已经在用中介者模式但觉得C实现起来有点别扭、想看看别人怎么处理接口和内存问题的开发者第三类是正在重构一个耦合严重的旧模块、想评估要不要引入中介者的同学。这篇文章我会从一个实际项目的演进过程切入把设计思路、C实现细节、常见坑和排查方法都过一遍。提示如果你现在的系统里类与类之间的直接调用已经超过三层嵌套或者每次加需求都要改动五六个文件的头文件依赖那这篇文章尤其适合你。2. 从网状到星形中介者模式的设计思路拆解2.1 没有中介者的时候代码是怎么腐化的我不太喜欢一上来就摆类图先看一段真实演进过度的代码结构你就明白我在说什么了。假设我们要做一个窗口程序有三个主要模块ChatWindow聊天主窗口负责展示会话列表和消息内容ContactList联系人列表负责展示在线用户MessagePanel消息输入和展示面板第一版代码ChatWindow要显示某联系人发来的消息于是它直接持有ContactList和MessagePanel的指针。ContactList想知道某个用户是否在线又需要回调ChatWindow去查状态。MessagePanel发送消息后要通知ChatWindow刷新会话列表。如果再加一个通知模块NotificationPanel那它又得和前面三个都建立关联。这种结构在功能少的时候每个类都“知道”其他类的存在互相调用确实方便。但问题在于耦合太深任何一个类的头文件改动可能导致所有依赖它的类都要重新编译。C不像某些动态语言头文件依赖是编译期的硬约束改一个接口声明整个项目的增量编译时间成倍上升。复用困难你想在另一个项目里复用ContactList模块结果发现它依赖ChatWindow和MessagePanel根本没法独立使用。排查问题费劲一个消息从发送到显示中间经过了七八次直接调用每次调用都可能改状态。出了问题你根本说不清是哪个环节丢了数据。这就是典型的“多对多”交互带来的复杂度爆炸。每一个连线都代表一份隐式的依赖约定系统里的连线越多维护成本越高。2.2 中介者模式的核心结构三个角色中介者模式把参与交互的对象分成三类我结合实际代码职责给你说明白中介者接口Mediator定义对象之间通信的统一接口。在C里它通常是纯虚基类声明一个通用的消息分发方法比如Notify(Component* sender, Event event, void* data)。具体中介者Concrete Mediator真正维护对象列表、实现消息路由逻辑的类。它知道“谁关心什么消息”收到消息后按规则分发给目标组件。这是整个模式里最有分量的一个类。组件类Colleague/Component实际的业务模块比如上面的ChatWindow、ContactList。它们不再互相持有对方指针而是持有一个中介者的指针需要通信时调用中介者接口自己不关心消息会被谁处理也不需要知道其他组件的存在。用一张图来打比方没有中介者时每个组件像路由器一样直接连到其他组件有了中介者后所有组件只和服务总线中介者连接组件之间没有任何直接连线。以前你改一条链路要检查两端的接口现在你只需要检查组件和中介者之间的“单条约定”就够了。2.3 为什么有人觉得中介者模式是“反面模式”如果只是把互调改成转发那中介者很容易变成一个“上帝对象”——所有消息都往它身上堆它的代码量越来越大最后反而成为新的维护地狱。这是中介者模式被诟病最多的地方也是初学者最容易踩的大坑。我的经验是这个问题的根源不在于模式本身而是你对“中介者的职责边界”没有划清楚。靠谱的做法是中介者只负责转发和仲裁不负责业务逻辑实现。比如消息内容怎么渲染、联系人列表怎么排序这些逻辑必须留在组件内部。中介者只做一件事收到A的消息判断B和C谁需要知道然后把消息原样转过去最多附加一些上下文信息。负责转发的代码是不会膨胀到不可维护的。还有一个很关键的判断标准如果两个组件之间确实只有一对一的关系且未来不太可能扩展就完全没必要硬塞一个中介者进去。中介者的价值在于“多对多”场景用它治理单一关系反而会徒增一层间接调用。这种“模式滥用”的问题我在第6节会专门展开讲。3. C实现中的关键环节从接口设计到消息路由3.1 组件基类让所有组件有一个共同的身份在C里实现中介者模式第一步是把“组件”抽象成一个公共基类。这个基类的设计直接决定了整个体系的灵活性我建议至少包含下面两个要素#include memory #include cstdint // 前置声明避免头文件循环依赖 class Mediator; // 组件基类所有可交互模块的公共接口 class Component { public: explicit Component(std::shared_ptrMediator mediator) : mediator_(std::move(mediator)) {} virtual ~Component() default; // 组件收到消息时的回调入口子类按需重写 virtual void OnMessage(uint32_t messageId, void* data) { // 默认空实现只有关心的组件才重写 } protected: // 组件主动向中介者发送消息由中介者负责路由 void Send(uint32_t messageId, void* data); std::shared_ptrMediator mediator_; };这里有几个C特有的设计点我逐个解释为什么要用前置声明class Mediator;而不是直接#include因为Component和Mediator是互相引用的关系如果两边都直接包含对方的头文件就会形成循环依赖。前置声明把对Mediator的依赖推迟到了实现文件里头文件间的依赖关系就被解耦了。这是C里做这类相互引用设计时的基本操作务必养成习惯。为什么持有shared_ptrMediator而不是裸指针这里涉及到生命周期管理的问题。Mediator通常由最外层的组合根创建然后传给各个组件。如果组件只持有裸指针一旦外层提前析构了Mediator组件调用Send时就悬空了。用shared_ptr可以在所有组件释放之前保住Mediator。如果你对生命周期有严格的控制力也可以考虑裸指针或weak_ptr但新手阶段用shared_ptr最省心。send函数为什么是protected因为组件才是消息的发送方外部不应该绕过组件的封装直接调Send。这个细节能守住封装边界防止其他模块乱发消息破坏系统状态。3.2 中介者接口怎么设计一个够用的消息协议中介者接口是组件之间通信的语言规范设计得是否合理直接决定了这套体系能不能顺畅运转。我给一个经过多次迭代验证过的版本#include memory #include functional // 消息ID采用无符号整数避免字符串比较的开销也便于调试 enum class EventId : uint32_t { kUserLogin, kUserLogout, kMessageReceived, kContactListUpdated, kPanelVisibilityChanged }; class Component; class Mediator { public: virtual ~Mediator() default; // 组件向中介者注册自己让中介者知道有哪些组件存在 virtual void Register(std::shared_ptrComponent component) 0; // 核心转发接口source是消息发送方eventId是事件类型 // data是一个void*入参具体含义由eventId决定 virtual void Notify(Component* source, EventId eventId, void* data) 0; };接口设计上要注意的一个核心取舍是消息参数的表达方式。C社区对这种场景有两种主流做法强类型接口方案给每种消息专门定义一个结构体比如struct UserLoginEvent { std::string username; uint64_t timestamp; };然后让Notify变成模板函数或者重载。优点是编译期就能查出类型不匹配缺点是需要写很多样板代码。对中小型项目来说这种方案会让中介者接口膨胀得很快。弱类型void*方案就是上面代码里的写法事件ID决定data怎么解释。优点是接口稳定、扩展成本低缺点是类型安全性靠人来保证。我个人的实践是中小型项目优先用事件ID void*写一个文档或者枚举注释说明每种事件的data实际类型就行。等系统规模大到枚举值超过几十个、经常传错参数的时候再考虑升级成强类型方案。早期追求类型安全反而会被繁琐的接口定义拖慢迭代速度。注意用void*传递数据时组件的OnMessage实现里必须做显式的类型转换。转换时要格外小心最好的做法是封装一个模板化的辅助函数自动做类型检查而不是到处裸转换。这相当于牺牲部分类型安全换扩展性但要靠纪律把风险压到最低。3.3 具体中介者实现注册、路由与状态维护下面是一个最简单的具体中介者实现我基于聊天室的场景写了一个可运行的骨架#include unordered_map #include vector #include algorithm class ConcreteMediator : public Mediator { public: void Register(std::shared_ptrComponent component) override { // 用组件原始指针作为key避免shared_ptr比较带来的混淆 components_.push_back(component.get()); // 同时保存shared_ptr保证组件生命周期 owners_.push_back(std::move(component)); } void Notify(Component* source, EventId eventId, void* data) override { // 遍历所有组件把消息转发给除source之外的所有组件 // 这是一种广播式路由适合“谁关心谁响应”的场景 for (auto* comp : components_) { if (comp ! source) { comp-OnMessage(static_castuint32_t(eventId), data); } } } private: std::vectorComponent* components_; // 用于消息路由的裸指针集合 std::vectorstd::shared_ptrComponent owners_; // 用于生命周期托管的shared_ptr集合 };这种广播式路由是最简单、最容易理解的一种策略。它的逻辑是“不管谁发的消息所有其他组件都看一眼关心就处理不关心就忽略”。好处是实现简单、组件之间的关系天然松散坏处是消息可能会被多个组件重复处理而且无法精准投递。针对不同的业务需求路由策略可以调整成不同的形态精准路由中介者内部维护一张“事件ID - 感兴趣组件列表”的路由表Notify时先查表只给订阅了该事件的组件发消息。这样消息不会满天飞但需要维护一套订阅注册机制。优先级路由同一种事件可能有多个处理器但它们的优先级不同比如UI刷新优先级最低数据持久化优先级最高。中介者需要维护一个有序的处理链。异步路由如果消息传递过程可能耗时较长或者需要跨线程投递中介者可以把消息放到任务队列里由专门的线程调度执行。实际项目里这三种策略常常组合使用。我们的聊天室项目用的是“广播为主、精准为辅”的混合路线系统级事件比如用户上线走广播界面刷新类事件走精准路由。关键是要在建系统之初就把路由规则定清楚否则后期改路由策略所有组件都会受影响。3.4 一个亲测过的完整示例模拟聊天室模块把上面的代码拼起来我们来搭一个最小可运行的聊天室Demo。这个例子里的组件有三个UserListPanel用户列表、MessageLogPanel消息记录、NotificationPanel通知栏。它们之间不直接互相持有指针所有通信都通过ConcreteMediator转发。#include iostream #include string #include mediator.h // 具体业务字段定义在事件data里 struct UserLoginData { std::string username; }; // 用户列表组件 class UserListPanel : public Component { public: using Component::Component; // 继承构造函数直接初始化mediator void OnUserLoggedIn(const std::string name) { std::cout [UserList] name 加入列表\n; // 通知中介者用户列表更新了其他组件可以刷新 UserLoginData data{name}; Send(static_castuint32_t(EventId::kUserLogin), data); } void OnMessage(uint32_t messageId, void* data) override { auto id static_castEventId(messageId); if (id EventId::kUserLogout) { auto* p static_caststd::string*(data); std::cout [UserList] *p 移除列表\n; } } }; // 消息记录组件 class MessageLogPanel : public Component { public: using Component::Component; void OnMessage(uint32_t messageId, void* data) override { auto id static_castEventId(messageId); if (id EventId::kMessageReceived) { auto* p static_caststd::string*(data); std::cout [MessageLog] 收到消息: *p \n; } } void PublishMessage(const std::string text) { sendMessage(text); } private: void sendMessage(const std::string text) { // 发送者用自己的逻辑构造消息然后通知中介者 Send(static_castuint32_t(EventId::kMessageReceived), const_caststd::string*(text)); } }; // 通知栏组件 class NotificationPanel : public Component { public: using Component::Component; void OnMessage(uint32_t messageId, void* data) override { auto id static_castEventId(messageId); if (id EventId::kUserLogin) { auto* p static_castUserLoginData*(data); std::cout [Notification] 欢迎 p-username 上线\n; } else if (id EventId::kMessageReceived) { auto* p static_caststd::string*(data); std::cout [Notification] 新消息提醒: *p \n; } } }; int main() { auto mediator std::make_sharedConcreteMediator(); auto userList std::make_sharedUserListPanel(mediator); auto messageLog std::make_sharedMessageLogPanel(mediator); auto notification std::make_sharedNotificationPanel(mediator); mediator-Register(userList); mediator-Register(messageLog); mediator-Register(notification); // 模拟用户上线 userList-OnUserLoggedIn(Alice); // 模拟发消息 messageLog-PublishMessage(大家好); return 0; }这段代码运行后的输出会是[UserList] Alice 加入列表 [Notification] 欢迎 Alice 上线 [MessageLog] 收到消息: 大家好 [Notification] 新消息提醒: 大家好注意一个细节用户列表里Alice上线时UserListPanel调用了Send发送kUserLogin事件。中介者广播后NotificationPanel响应了而UserListPanel作为消息源头被跳过了所以自己不会收到自己广播的事件。这个“自己不发给自己”的逻辑是中介者实现里的默认约定能避免组件自我干扰。4. 什么时候该用它典型应用场景与收益评估4.1 界面系统最常见的主战场中介者模式在GUI程序里的应用可以说是教科书级别的。界面系统天然是“多对多”交互的重灾区按钮要修改状态栏、状态栏要刷新列表、列表要通知菜单栏、菜单栏要切换面板。如果所有控件都互相持有指针界面代码会快速变成一团乱麻。在一个桌面端的复杂表单项目里我处理过一个经典问题表单里十几个输入框彼此之间有复杂的联动规则——勾选了某个条件A另一个输入框B要禁用选择了某个下拉选项C一组单选按钮D要变色填完必填项提交按钮才可点击。如果用传统写法每个输入框的变更事件都要去调用其他控件的接口十几个控件互相引用光是理清依赖就花了一个下午。后来用中介者重构所有输入框只关心自己的值变化然后把“值变了”这个事件发给中介者中介者持有整个表单的校验状态收到事件后重新计算联动条件再调度目标控件更新。重构之后新增一个联动规则只需要改中介者一个类表单控件本身完全不用动。这个场景是中介者模式价值体现最明显的地方。4.2 多模块协作任务解耦复杂流程除了界面中介者模式也非常适合那种“多个模块共同完成一个流程”的业务场景。比如一个订单流程涉及库存模块、支付模块、消息通知模块、物流模块。每个模块处理完自己的部分后需要通知下一个模块继续。传统做法是库存模块直接调支付模块的接口支付模块又直接调通知模块……流程加到五六个节点后模块之间的调用链会变得极难维护。用中介者后每个模块处理完自己的事只需要发一个“我完成了”的事件。中介者根据流程状态机的定义决定下一个激活哪个模块。这个模式下即使未来要插入一个风控模块也不会影响已有模块的代码——你只需要让风控模块实现Component接口在流程里注册它然后在中介者的路由规则里加上一条转发。这种扩展方式对旧代码的侵入性几乎为零。4.3 多窗口或插件系统控制通信协议还有一个容易被忽略的场景是插件/扩展系统。如果宿主程序要向插件暴露通信能力直接让插件持有其他组件的指针是危险的——插件崩溃可能导致整个宿主崩溃插件之间也会因为竞态互相干扰。通过中介者模型宿主只向插件暴露一个Mediator接口插件之间不能直接访问所有的访问都由宿主仲裁。这既隔离了插件的风险也提供了一组可控的通信协议。在某个模拟项目X里我帮别人设计过一套音频插件系统。宿主提供Mediator插件只向宿主请求“播放”“暂停”“读取参数”等能力。一个插件出了问题宿主可以直接把这个组件标记为异常其他插件不受影响。这种隔离机制在直接互相调用的架构里是做不到的。4.4 什么时候不要硬套中介者和很多设计模式一样中介者最容易被误用的是“什么场景都想套”。下面几种情况我建议你谨慎使用甚至不用只有两个类需要通信一对一直接调用比绕一圈中介者更直白、性能更好。多加一层转发不会带来任何收益只是徒增调试成本。组件数量很少且不会扩展如果系统稳定、交互简单中介者的价值无法体现反而让本来就简单的逻辑变得曲折。你希望保留对象间特殊的性能通道某些高频交互比如每帧刷新的数据交换如果每次都经过中介者转发调用栈会变深影响性能瓶颈场景下的优化空间。这种情况最好把高速通道留在组件内部只在低频交互上引入中介者。我在项目里的一条原则是先用直接依赖把功能跑通等到第三个类加入交互网络时才开始考虑引入中介者。过度设计比设计不足更头疼这一点怎么强调都不为过。5. 实操过程中常见的坑与排查技巧5.1 内存和生命周期管理的问题C里用中介者模式最常见的坑是组件和中介者的生命周期不匹配导致悬挂引用。比如组件持有的是shared_ptrMediator但某个组件析构后它的裸指针还留在中介者的路由表里。下一次其他组件发消息中介者遍历列表时访问了已释放的组件程序就崩了。解决这个问题的思路有两种智能指针托管组件让Mediator用shared_ptr持有所有组件。组件析构时Mediator里对应的智能指针会自行释放且该组件会从路由表中移除。前提是确保不存在其他裸指针长期悬空的情况。显式解绑接口Mediator提供一个Unregister(Component*)方法组件析构前主动告诉中介者“我要走了”。这样路由表里就不会残留悬空指针。这个方案更可控代价是每个析构函数都要多写一行代码。我通常两个方案混合用组件向Mediator注册时Mediator保存shared_ptr作为生命周期兜底组件析构时若需要立刻从路由表摘除则调用Unregister。核心思想是绝不在路由表里留下既没有shared_ptr保障、又没有主动解绑的裸指针。5.2 消息数据类型传错void* 的安全实践void*是灵活性和安全性之间的传统矛盾。在示例代码里我把用户登录事件的数据定义成UserLoginData结构体而消息接收事件的数据是std::string。如果一个组件收到kUserLogin时误把 data 强转成std::string*程序马上就会发生未定义行为——轻则读到乱码重则直接段错误。我用的防护手段有三个保持事件ID和数据结构有一一对应的映射写注释的时候把对应关系写清楚最好在枚举定义旁边注释。封装统一转换函数每次从void*到具体类型的转换集中在一两个函数里做不要分散到各处裸转。在组件内部加一层守门员逻辑收到不认识的ID直接忽略不要尝试处理未知事件。template typename T T* SafeCastEventData(EventId id, void* data, EventId expectedId) { if (id ! expectedId || data nullptr) { return nullptr; } return static_castT*(data); }这种函数虽然简单但如果每个组件都用它做转换就能把类型错误拦截在入口处而不是让错误悄悄蔓延到业务逻辑里。5.3 广播风暴消息被反复处理导致性能问题广播式路由最大的隐患是消息风暴。当系统中组件数量达到十几个、事件频率又高时每条消息都要遍历所有组件无谓的OnMessage调用会拖慢整体性能。特别是UI系统里一个按钮点击可能会引发两三个级联事件级联事件再触发各组件刷新瞬间调用量就上去了。排查和优化的路径先加日志在Mediator的Notify入口打印eventId和source以及每个组件的处理耗时。观察哪类消息被处理次数最多、耗时最重。改精准路由把“所有组件都广播”改成“只发给订阅了该事件的组件”。增加一个Subscribe(EventId, Component*)方法Mediator内部用哈希表维护事件到组件列表的映射。事件合并像界面刷新这种高频低优事件可以让Mediator在同一个事件循环里合并多次触发只统一刷新一次。这个做法需要Mediator维持一个脏标记或者队列实现复杂度会上升但收益也很明显。以我开发的一个图像标注工具为例鼠标拖动时画布组件每秒会产生几十次CanvasChanged事件。同步广播给缩略图、状态栏和图层列表缩略图和图层列表的刷新会造成明显卡顿。改成精准订阅后缩略图只订阅画布变化的低频摘要事件状态栏订阅的是统计信息事件卡顿直接消失。5.4 组件间顺序依赖这个事件必须先于那个事件处理中介者模式带来的一个隐性陷阱是因为组件之间不再直接调用某些隐含的处理顺序可能被打乱。比如用户上线时用户列表需要先更新通知栏才应该弹欢迎如果消息路由顺序不对就会出现“欢迎已经弹出来了但列表还没更新”的错乱。处理顺序问题一般有三个解法在中介者内部维护优先级给每个注册组件设置一个优先级数值Notify遍历时按优先级排序后投递。这样能保证系统事件总是先走数据更新、再做UI展示。组件内部做状态依赖检查在OnMessage入口检查前置条件如果依赖的数据还没准备好先把自己需要的数据缓存起来等条件满足后再处理。这种方案会让组件逻辑变复杂但能解决部分乱序问题。把事件拆细比如将kUserLogin拆成kUserLoginDataReady和kUserLoginUIUpdate两个阶段。数据层组件订阅前者UI层组件订阅后者。这样流程的语义更清晰排错也方便。我个人最常用的是优先级方案因为改动集中在中介者不会散落到各个组件里。但要注意大量使用优先级会让中介者的路由逻辑越来越“重”所以要在组件数量和消息类别增长到一定程度时及时评估重构。6. 进阶实践结合C特性的几种扩展形态6.1 基于模板的强类型中介者前面提到void*的类型安全问题如果你的项目对类型安全要求极高可以考虑用C模板来构造强类型的中介者接口。核心思路是让Notify变成模板方法根据事件类型自动推导数据结构#include type_traits template typename EventType void Notify(Component* source, EventType event) { // 事件类型自带ID特征通过模板推导避免void*强转 auto id EventType::kId; // ... 内部用id做路由用std::forward传递数据 }这种方案的优点是编译器帮你保证数据类型匹配错误在编译期就爆发。缺点也很明显每种事件都要定义一个类型消息定义的前置工作量大而且C模板在头文件里的展开会导致编译时间上升对中型项目来说可能要权衡。6.2 异步中介者与跨线程消息随着项目复杂度的提高组件可能分散在不同线程中比如后台网络线程收到消息、UI线程需要刷新界面。此时中介者不能直接调用组件的OnMessage因为那会跨线程操作UI导致崩溃或数据竞争。一个实用的做法是Mediator内部每个组件关联一个任务队列。Notify时不是直接同步调用而是把回调丢进目标组件的队列由目标线程消费。这个思路在图形界面框架里非常常见——所有跨线程的UI更新都投递到UI线程的事件队列里由UI线程统一处理。实现时需要注意线程安全问题队列的push和pop要加锁或使用无锁队列。我一般用std::mutex配合std::condition_variable把消息投递代码和消费代码分开封装。这个技术细节如果展开讲能再写一篇长文这里先点到为止如果组件之间有跨线程通信需求中介者天然适合作为线程隔离的边界但这需要你额外设计好消息队列的并发安全。6.3 与观察者模式的取舍对比很多人会把中介者模式和观察者模式弄混这里说清它们的区别。观察者模式是“一个被观察对象变化所有观察者收到通知”它解决的是一对多的依赖关系。中介者模式解决的是多对多的协作关系它的核心不是订阅机制而是协调和控制。如果系统只需要“某个状态变化很多人要更新”用观察者模式就够了。一旦交互变成了“多个模块互相更新、互相影响”观察者模式就退化成了一张复杂的订阅网这时候采购中介者模式更合适。实际系统中两者常常共存中介者内部可以用观察者的思路来维护订阅表但对外暴露的是中介者的统一路由接口。7. 结尾我把这套模式用崩过之后的几点体会中介者模式听着简单实际用起来还是有不少门道的。我最早在一个项目里因为懒得多写几个类把路由逻辑直接塞进了业务代码里结果中介者变成了一个几千行的上帝类比之前直接互调还要痛苦。后来我彻底重构了一次才真正理解这个模式的边界在哪。我个人的实操体会可以总结成三条第一中介者只做路由和仲裁永远不要在里面写业务逻辑。哪怕你只是顺手在Notify里加了一个“如果消息是谁谁谁的就先改一下数据”这个“顺手”就会成为中介者腐化的开始。业务逻辑写在组件里中介者保持“薄”和“通用”这个模式才能持续健康。第二吞掉所有不认识的旧代码但别急着全部改造。如果你是在重构一个已有系统不要试图一夜之间把所有直接调用都改成中介者。先让新的交互模式跑起来旧的依赖留一段时间等验证新方案稳定后再逐步迁移。重构最忌讳的就是爆炸式修改带来的大量回归测试。第三设计模式是工具不是教条。当你发现中介者的维护成本开始超过它解决的问题时果断退回去用直接调用或者换别的模式不要因为“设计模式书上说这样好”就硬撑。代码是给人看的模式是给人服务的本末倒置了就是灾难。聊到这儿中介者模式的原理、C实现、场景、坑和进阶方向基本都过了一遍。最后再分享一个小技巧调试中介者模式驱动的新系统时记得在Mediator的Notify里打一条结构化的日志包含source、eventId和target。这套日志在系统出现诡异交互问题时能省下你一半的排查时间。我自己就是用这套日志定位过好几次“某个消息被多个组件重复处理”的问题直到现在都很依赖这个习惯。