☰
中介者模式:如何将混乱的对象协作压成星形结构
2026/10/3 4:41:17 网站建设 项目流程

1. 让代码失控的,往往是“大家都认识大家”

我第一次意识到需要中介者模式,是在接手一个持续迭代了两年的老项目时。业务本身不复杂:一个带联动校验的表单页面。但你在地毯上随便掀开一块,底下都是密密麻麻的引用关系——数量输入框改变后,要通知摘要区刷新、通知优惠码输入框重新校验、通知提交按钮更新可用状态;配送方式单选改变后,要通知价格摘要重新计算、通知预计送达时间刷新、通知提交按钮重新评估;优惠码应用成功后,不光要改摘要,还要改变按钮文案……页面上一共七八个组件,我数了数它们之间的直接调用关系,快接近二十条了。这种代码不能说不能跑,但每改一个小逻辑,都要像排雷一样顺藤摸瓜,最后大概率还是踩了雷。

这个场景放到架构设计里特别典型:多个对象之间形成了多对多依赖。对象数量一多,谁的变更都可能影响别人,于是互相注册、互相回调、互相 setState。表面上看,每个组件“很自由”,谁都能直接联系到谁;但这种自由其实是债务,因为每多一个对象,系统里就要增加 N-1 条依赖关系。七个组件两两互相调用,就是 21 条直接关联。哪天来个新人把某个引用删错了,编译能过,运行时的联动却静悄悄地断了。

你会问,这问题到底怎么来的?大多数项目并不是一开始就这样。最初只有两个组件联动,写起来很爽,直接互相调用。后来加了第三个,你又不想让前两个改太多,就在中间加了个“事件分发器”,让组件通过事件互相解耦。再后来组件变多了,事件名越来越长,回调链越来越深,你发现分发器开始变成一个大杂烩,谁的事都管,谁也说不清这块逻辑的完整链路。依赖关系从“组件间直接引用”换了个形式,变成了在分发器里堆分支,本质依旧是网状结构,只是把线藏进了同一个文件。

这种时候,真正需要的是把整个交互网络重新“压扁”。所有组件不直接认识对方,而是都只认识一个中间人,由中间人负责协调他们之间的任何消息。这种玩法,就是中介者模式。

1.1 一个真实到有点痛的现场

我举个例子,你要是调过类似 bug,大概会心一笑。某个页面上,有一个商品数量输入框,一个优惠码输入框,一个配送方式选择区,一个提交按钮。一次正常的下单流程是:

  • 用户输入数量,页面刷新金额摘要
  • 用户选择配送方式,金额摘要再加上运费
  • 用户输入优惠码,金额摘要再打折
  • 所有条件合法后,提交按钮才允许点击

没有中介者的时候,最朴素的写法就是各组件互相持有对方。数量输入框里写deliveryPanel.refresh(); summaryPanel.refresh(); submitButton.refresh();,优惠码输入框里也写类似三行,配送区再写三行…… 每加一个组件,所有老组件都得跟着加引用。更麻烦的是,刷新顺序一变,界面就会出现一瞬间的“错误中间态”。你调这个 bug 的时候,甚至说不清它到底是哪个组件刷新完了才触发的问题。

我曾经为了修一个“优惠码输入太快导致摘要少了运费”的问题,花了整整半天。最后发现原因是:输入框触发刷新时,配送区还没把最新选择同步给摘要区,摘要区先读了配送区的旧值,然后配送区又刷新了一次,但有个条件分支提前 return 了,最终金额就一直少一截。类与类之间的时序耦合,比引用关系本身更难维护,因为编译器帮不了你,只有运行到那一步才炸。

1.2 拆解之后,“就是关系没地方放”

那段时间我把代码捋了几遍,意识到真正的问题不是某个组件写错了,而是这些协作关系没有一个统一的“家”。组件本身负责输入动作,这是它的本职工作;但“数量变了之后别人应该怎么更新”,这是协作关系,本不应该散落在每个组件里。散落了,就会重复、会漏改、会互相覆盖。你平时总觉得这类代码“也能跑”,但等到需求变成“数量为 0 时不光按钮置灰,还要弹提示,并且配送区也禁用”,你就得挨个改动方案,很容易漏。

中介者模式的思想,说白了就是:把协作关系收拢到一个专门的对象里,让所有组件只跟这个对象说话。组件不知道谁在关心自己的变化,也不知道自己的变化最终会触发多少动作,它们只负责发出“我变了”这个信号。中间人拿到信号,进行下面这些摆布:谁该刷新、谁该禁用、谁该显示提示。这样一来,网状结构就被压成了星形结构。每个组件面前只有一个中间人,中间人面前有所有组件。

2. 中介者模式的设计意图:把网状结构压成星形结构

听到“中央控制”这四个字,很多人第一反应是抗拒,因为我们都讨厌臃肿的上帝类。但中介者模式里的“控制中心”恰恰不是为了管一切,而是为了“把多对象间的协作复杂度集中管理”。它牺牲了“对象间直接沟通”的自由,换来了“关系变化中的可控性”。

你想象一个小区没有物业,每家要沟通都得自己上门。十户人家、二十户人家还行,百户人家就乱了:你不知道该找谁,找错人还要被推皮球。中介者就是物业中心,大家有事都打总机,总机记录需求、分发给相关人员。这个类比虽然简单,但很接近模式本质。

2.1 五个角色,一条不能破的规则

标准定义里,中介者模式有五个参与者:

角色职责在我那个结算页案例里的对应物
Mediator(抽象中介者)定义通信接口,声明组件之间如何交互定义中介接口,方法是notify(...)之类的转发行为
ConcreteMediator(具体中介者)实现协调逻辑,持有所有具体组件,维护组件之间关系结算页协调器
Colleague(抽象同事)定义组件抽象行为,持有对中介者的引用抽象的 UI 组件基类
ConcreteColleague(具体同事)实现自己的业务行为,在状态变化时通知中介者数量输入框、配送方式选择器、优惠码输入框、提交按钮
Client(客户端)创建具体中介者和具体同事,维护它们的依赖关系组装页面的人

有一条规则几乎可以当口头禅:具体同事之间禁止互相引用。也就是说,数量输入框里不允许出现deliverySelector.doSomething();配送方式区也不允许直接去改提交按钮上的文案。想做什么,都通过mediator.notify(...)说一声,剩下的事由中介者决定。

这条规则是整个模式的生命线。我们平时看别人写的“简易版”中介者,有的已经不叫 Mediator,改叫 EventBus、Dispatcher、Store 之类的名字了,但只要核心逻辑仍符合“所有交互经过中间对象”,本质还是中介者思想的变体。

2.2 和观察者模式、门面模式的区分

经常有人把中介者模式和观察者模式搞混。说实话,这俩在日常项目里经常同时出现,我自己也混过。区分清楚之后,选型会果断很多:

  • 观察者模式解决的是“一对多通知”。被观察者变化后,自动通知所有观察者,观察者只做被动响应。它关心的是通知的传播方向,传播关系是单向的、没有中心协调。
  • 中介者模式解决的是“多对多协调”。多个参与者之间本来就存在复杂的相互影响,每个参与者的变化都可能影响其他多个参与者。它关心的是“谁的变化影响了谁、怎么影响”,需要一个中心来承载这套规则。
  • 门面模式经常被拿来一起比,但门面做的是“给一堆复杂接口提供一个简单入口”,它把内部细节藏起来,调用方不需要知道背后有很多类。它更像一个前台接待,只负责把你的请求转交给后面的人,不协调后面对面的关系。

从代码感上说,观察者模式适合那种“一个事件源,多个监听器”的场景,比如按钮点击发布事件。中介者模式更适合“多个对象互相咬合,刷新顺序和条件都强相关”的场景,比如表单联动、多人协作、复杂状态同步。实践中它们常常搭配使用:组件作为观察者订阅中介者的事件,中介者作为事件中心处理业务规则,既保留通知能力,又集中协调。后面我给的案例里就考虑到了这种组合,但不是必须,先理解最简形态就好。

3. 用购物结算页做一个拿得出手的案例

理论说多了发飘,直接上代码。案例我特意选了购物结算页,因为这才是大家每天都会遇到的交互:数量输入框、配送方式、优惠码输入框、提交按钮,四个组件互相联动。这个例子不需要任何框架知识,哪怕你不是 Java 后端,看逻辑也能看懂;同时它也可以被原样改写成 Swing、Android View 或任意你熟悉的 GUI 结构。

3.1 场景定义与为什么选它

订单摘要区域负责显示基础金额、运费和折扣;提交按钮的可用状态,要同时取决于数量是否合法、配送方式是否已选、优惠码是否被接受。若直接在四个组件之间互相调用,大概会出现十几条直接依赖,而且这些依赖会随着“加一个满减规则”之类的需求三天两头变动。

选择这个场景,是因为它在“多对象协调”中足够典型:不同组件的状态互相依赖,刷新顺序直接影响正确性,而且后续大概率要加新规则。中介者最大的优势在这类场景里能完整发挥。

3.2 Java 核心实现

先定义事件类型和一个抽象的中介者接口:

public enum CheckoutEvent { INPUT_CHANGED, DELIVERY_CHANGED, PROMO_APPLIED } public interface CheckoutMediator { void notifyComponentChanged(AbstractComponent component, CheckoutEvent event); }

再看抽象的同事基类。每个具体组件都继承它,内部保存中介者引用,并提供一个“发通知”的方法:

public abstract class AbstractComponent { protected CheckoutMediator mediator; public void bindMediator(CheckoutMediator mediator) { this.mediator = mediator; } protected void notifyMediator(CheckoutEvent event) { if (mediator != null) { mediator.notifyComponentChanged(this, event); } } }

具体组件之一,数量输入框:

public class QuantityInput extends AbstractComponent { private int value; public void setValue(int value) { this.value = value; notifyMediator(CheckoutEvent.INPUT_CHANGED); } public int getValue() { return value; } }

配送方式选择器与优惠码输入框的结构几乎一样,都是状态变化后发出对应事件,不关心外界谁会处理。提交按钮也是普通组件,只提供一个setEnabled(boolean)方法,但那个方法的调用方不是其他组件,而是中介者。

下面是整个模式最核心的一环——具体中介者。它持有所有组件,并集中实现联动规则:

public class CheckoutPageMediator implements CheckoutMediator { private QuantityInput quantityInput; private DeliverySelector deliverySelector; private PromoInput promoInput; private SubmitButton submitButton; public CheckoutPageMediator( QuantityInput quantityInput, DeliverySelector deliverySelector, PromoInput promoInput, SubmitButton submitButton) { this.quantityInput = quantityInput; this.deliverySelector = deliverySelector; this.promoInput = promoInput; this.submitButton = submitButton; // 所有组件只认识中介者 quantityInput.bindMediator(this); deliverySelector.bindMediator(this); promoInput.bindMediator(this); submitButton.bindMediator(this); } @Override public void notifyComponentChanged(AbstractComponent component, CheckoutEvent event) { if (component == quantityInput) { handleQuantityChanged(); } else if (component == deliverySelector) { handleDeliveryChanged(); } else if (component == promoInput) { handlePromoChanged(); } } private void handleQuantityChanged() { boolean quantityValid = quantityInput.getValue() > 0; deliverySelector.setEnabled(quantityValid); promoInput.setEnabled(quantityValid); refreshSummary(); refreshSubmitButton(); } private void handleDeliveryChanged() { refreshSummary(); refreshSubmitButton(); } private void handlePromoChanged() { refreshSummary(); refreshSubmitButton(); } private void refreshSummary() { int quantity = quantityInput.getValue(); double productTotal = quantity * 79.9; boolean hasDelivery = deliverySelector.hasSelected(); double deliveryFee = hasDelivery ? 10.0 : 0.0; // 优惠码这里只做模拟:填对码就打九折 boolean hasProperPromo = promoInput.isApplied(); double discountRate = hasProperPromo ? 0.9 : 1.0; double finalTotal = (productTotal + deliveryFee) * discountRate; System.out.println("订单摘要金额: " + finalTotal); } private void refreshSubmitButton() { boolean submitReady = quantityInput.getValue() > 0 && deliverySelector.hasSelected() && promoInput.isApplied(); submitButton.setEnabled(submitReady); } }

这个写法有几个明显的味道:第一,具体组件代码非常“钝”,它的责任仅仅是维护自己的状态和触发消息,不关心谁听了、听完了要干什么;第二,所有规则集中在CheckoutPageMediator内,调整顺序、增加条件都只改一个类;第三,任何组件想要发起协作,只需要调用notifyMediator(...)一行。

3.3 代码里的取舍与后续扩展空间

有人会说,这种写法好像也没省多少代码。你说得对,如果项目只有四个组件、三条规则,确实没必要上模式。但中介者真正的收益在扩展性上。假设产品同学告诉我:数量为 0 时不要禁用配送区,而是把配送区置为“虚拟配送”,并且优惠码输入框要清空缓存。我只需要在handleQuantityChanged()里修改两行,其他组件完全不动。再假设以后要加库存不足的提示条,组件增加一个,老组件连一行都不用改,只有中介者多维护一个引用。

在单元测试层面,这个结构也很好用。你可以为三类测试伪造一个假的CheckoutMediator,验证组件在setValue时确实发出了消息;也可以直接测试CheckoutPageMediator,给它传入假的组件,断言不同条件下按钮、摘要的最终状态是否符合预期。比起“四个组件互相 set 来 set 去,测试时得全部 new 出来”,友好太多了。

我在这个案例里加了事件枚举,而不是直接让中介者暴露多个方法,原因是事件类型可以继续丰富。像CART_EMPTYED、INVENTORY_LOW这类事件,后续可以直接扩进枚举,而不用改动同事的基类。如果项目中需要异步处理,可以在中介者内部把事件丢进线程池,同事仍然感知不到变化。

3.4 C++ 和 Java 有什么实质差异

搜索引擎里配着“设计模式”最容易出现 C++ 的身影,因为很多教材用 C++ 讲 23 种设计模式。核心思想完全一样,但落地时有三个点要额外注意。第一是生命周期。Java 里组件和中介者都靠 GC 兜底,C++ 里一旦对象生命周期错乱,裸指针很容易悬空。我的实践是:页面容器负责创建并拥有Mediator,组件持有中介者的裸指针,但组件不能在析构时反过来调用中介者的虚函数。第二是所有权归属。中介者如果持有组件,建议用unique_ptr或shared_ptr明确表达“中介者是否拥有组件”,否则代码很难读。第三是消息传递。C++ 中可以用std::function或std::variant把事件做成强类型,比 string 判空安全很多。

伪代码感受一下骨架:

class CheckoutMediator; class UIComponent { public: virtual ~UIComponent() = default; void setMediator(CheckoutMediator* mediator) { mediator_ = mediator; } virtual void onChange() = 0; protected: CheckoutMediator* mediator_ = nullptr; }; class CheckoutMediator { public: virtual ~CheckoutMediator() = default; virtual void notify(UIComponent* sender) = 0; };

后面具体实现就和 Java 版同理了。C++ 使用时的额外成本主要在生命周期管理,而不是模式本身。

4. 什么情况下真别硬上这个模式

中介者模式不是银弹。我见过很多从反面使用它的案例,最后不是变成了“上帝中介者”,就是平白无故给五六个类之间架上了一个大中转站,连最简单的数据流都绕路跑一趟。

4.1 三种“用错现场”比不用更糟

第一种,组件数量不超过三个,且只有一个方向的通知。比如按钮点击后弹提示,压根不需要中介者,“观察者模式”或者直接回调就够了。硬引入中介者,等于为一个从 A 到 B 的直接传递增加了一个 C,除了让链路变长,没任何收益。

第二种,中介者内部全是if/else大杂烩,把所有规则堆到一百个方法里。你会得到一个比“网状依赖”更难维护的“巨型依赖中心”。至少原来的网状依赖还能按组件找代码,现在所有消息从一个 public 方法进,里面三十个分支看瞎眼。

第三种,“为了加一个中介者,强行把本来没有耦合的组件绑在一起”。有些开发刚学会模式,恨不得把一切都中介化。其实如果组件之间根本不沟通,或者只有一层简单通知,中介者就是多此一举。用模式的前提是有问题要解决,不是为了打卡。

4.2 防治“上帝中介者”的手段

真到复杂业务里,中介者确实容易长胖。我的经验是不要一个项目只留一个中介者,把“中介者”按业务模块拆开,每个中介者只负责一组强相关的同事。比如结算页一个中介者,库存校验一个中介者,它们之间再通过应用层的事件总线沟通,避免把中台逻辑全部压进一个类里。

同时,中介者内部的事件分发也可以做得更规整:不要写了十种notify再在方法里猜 sender,事件类型本身就是足够的意图表达。每个事件对应一个处理函数,函数体尽量短小,只做策略调度,不写太多 if 分支。如果某个处理函数超过了十几行,并且还要从多个组件拿数据,说明这块的业务复杂度已经到了应该抽取独立服务类的地步。

5. 我在实际项目中总结的几条实践建议

模式看完代码容易,真正落地到存量项目时,很多人会卡在“从哪下手”上。这节是我踩坑之后整理出来的一些操作建议,不保证覆盖所有项目,但至少能减少返工。

5.1 什么时候值得动手改造存量代码

我判断存量代码该不该引入中介者,主要看四个信号:一是修改一个组件触发的问题牵连到三个以上组件;二是新增组件时老组件频繁改代码;三是有三段以上重复的“同步代码”,也就是多个组件里都写着几乎一样的刷新逻辑;四是协作关系经常变,一个月里改了好几次联动规则。只要占两三条,就可以考虑动工。反之,页面就俩控件,改一个另一个不动,别闲着没事重构。

动手时,我建议小步迁移。选一个相对独立的模块先落地,把原来的“组件互相引用”改成“组件只持有中介者引用”,然后逐个删掉旧引用。不要一次性把全项目所有交互都塞进一个中介者,否则你会同时处理技能学习、业务理解、架构迁移三个任务,很容易爆炸。

5.2 写完中介者之后的自检清单

有一份清单我每次都会走一遍,分享出来:

  • 具体同事里还有没有直接 new 对方,或者直接调用对方 public 方法的?有就是不合格。
  • 中介者是否能仅凭接口理解全部协作规则?能不能在一个类里看清“谁变引起了谁变”?
  • 新增一个组件时,老同事需要改吗?如果还需要,说明边界没切干净。
  • 中介者会不会太胖?如果它的工作职责超过“协调”本身,比如还包含复杂计算,说明业务对象没抽够。
  • 事件枚举和具体通知方法是不是言简意赅?命名模糊的doUpdate比不用模式还难维护。

我在实际项目里碰到过一种“半吊子中介者”:同事们确实不互相引用了,但中介者接口里塞了六个notifyXxx方法,每个都有不同入参,同事像猜谜一样想自己该调哪个。我的做法是回到更通用的事件转发,让事件本身承载参数,规避了这类选择困难症。

至于性能问题,老实说,大多数业务场景都不需要担心中介者的开销。多一层调用,无非是多个栈帧,现代语言编译器优化得非常快。真正需要担心的是线程模型。如果状态更新在主线程,消息转发也在主线程,那么中介者内部的刷新顺序就是同步且确定的,这反而是优点;如果同事更新发生在不同线程,你必须在进入中介者前把数据快照处理好,否则中介者协调到的就是一个被并发改乱的状态。

我之前遇到过一个真实案例:支付回调线程刷新余额,主线程刷新页面,中介者把两个线程的消息都接住了,结果有一次余额明明已经变,页面摘要却用旧快照算了一遍,看起来像 bug。后来我在中介者入口统一改成线程切换,所有消息都投递到主线程再处理,问题就消失了。这个经验告诉我,把多并发问题挡在中介者外面,别指望中介者内部靠加锁解决一切。

说到底,中介者模式的精髓不是“有个中间人”,而是“通过让很多根线都集中到一点,换来了每个参与者的简单”。它非常适合那些真正被多对象协作折磨过的场景:多人协作、表单联动、复杂的异步通知。如果你正在经历一个“谁都在 public 接口里被别人开着玩笑”的类群,先把断点理顺,再看看这个模式,会比你逐层打补丁省心很多。我自己实践下来的感受是:设计模式教学里最容易被低估的,恰恰就是它这种不刺激却很实用的模式。

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

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

立即咨询