【设计模式精讲】22.中介者模式(Mediator)
【摘要】:一个登录对话框,用户名变了要刷新按钮状态,按钮按下要冻结所有输入框,回调返回又要逐一解冻——每个控件都持有并调用其他所有控件,五个控件织出十条网线,加一个验证码框全体返工。本文从这张依赖网讲起,给出中介者的 GoF 意图:用一个对象封装一组对象的交互,网状收拢成星状——同事只认识中介者,规则集中成一处可单测的代码;C++98 版用 widgetChanged 单入口实现,现代版再用 std::function 回调槽让同事连中介者接口都不必认识。本文重点辨析中介者与观察者——同样是星型,一个是有意志的协调者、一个是无意志的广播台;文末对照 POCO 通知中心与 AOSP 的 ServiceManager/AMS。读完你能识别网状依赖,并决定让谁来做调度中心。
【关键词】:中介者、同事对象、星型拓扑、交互解耦、事件协调、UI 联动
【代码基准】:C++17
1. 一个登录框,把所有控件缝在一起
登录对话框的联动规则再普通不过:用户名与密码都非空,登录按钮才可用;点击登录后,全部控件禁用、提示「登录中…」;回调返回后按结果解冻或关窗。第一版代码几乎必然这样长出来:
// 说明性片段// ❌ 每个控件都认识其他所有控件voidUserNameEdit::onChanged(){loginBtn_->setEnabled(!text_.empty()&&!pwd_->text().empty());}voidLoginBtn::onClick(){name_->setEnabled(false);pwd_->setEnabled(false);remember_->setEnabled(false);hint_->setText("登录中…");auth_->asyncLogin(name_->text(),pwd_->text());}voidAuthCallback::onResult(boolok){// 回调又要反向摸回每一个控件……}数一数耦合:用户名框持有按钮和密码框,按钮持有三个控件加网络层,回调再摸回全部——五个控件织出十条引用边,理论满配是n(n−1)/2。变化的账单随之而来:加一个「验证码输入框」,按钮状态规则改三处、禁用清单改四处、回调补一处;「用户名框」想复用到注册对话框?它 include 的全是登录框专属控件,复用性为零;最糟的是没有一处代码能回答「这个对话框的行为规则是什么」——它散布在每个控件的每个事件里。
病灶:对象之间直接对话,交互逻辑无处安放。每个控件既是规则的执行者又是规则的存放地。GoF 原书的动机示例正是这类对话框(DialogDirector协调一组控件互操作)——把「谁跟谁说什么」从控件身体里抽出来,交给一个专职的中间人。
2. 模式意图与定义
- 一句话定义:用一个中介对象来封装一系列对象的交互,中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。
解决的问题:网状的对象互引用(n² 级边数)与无处集中管理的交互规则。 - GoF 原文意图:Define an object that encapsulates how a set of objects interact. Mediator promotes loose coupling by keeping objects from referring to each other explicitly, and it lets you vary their interaction independently.(定义一个封装了一组对象如何交互的对象。)关键词encapsulate how they interact——封装的不是数据、不是行为,是「怎么互动」这件此前无处安放的事。
- Refactoring Guru 的表述:中介者模式能减少对象之间混乱无序的依赖关系;它限制对象之间的直接交互,迫使它们通过一个中介对象进行合作——网状变星状是 RG 给出的最直观意象。
三条定性:
- 同事(Colleague)只认识中介者——每个控件持有一个中介者引用,同事之间零 include、零指针;
- 交互规则集中一处——「何时谁启谁停」成为中介者内一段可以单独单测的代码(对照组:第 1 节里它散布在每个控件里);
- 中介者是双向枢纽——它听同事报告(
widgetChanged)、向同事发令(setEnabled),这与外观(第 14 篇)的单向入口、观察者(第 24 篇)的单向广播构成三种「中间人」气象,第 6 节正面辨析。
3. UML 图 + 结构说明
四个参与者(GoF 命名):
- 中介者(Mediator)接口:声明同事向它报告的通道(
widgetChanged); - 具体中介者:
LoginDialog——唯一知道全体同事面目的对象,交互规则全部住在它这里; - 同事(Colleague)类:
Widget及其派生——只知道中介者,互相不认识; - 同事基类:替所有同事保管中介者引用,
changed()把「我变了」递上去。
拓扑一变,账就变了:第 1 节的网状 n(n−1)/2 条边,收成n 条(每个同事一条边指向中介者)。新增「验证码框」= 新同事挂上星型 + 中介者规则里加一行判断——旧同事零改动。值得盯着图认清的另一点:中介者持有全体同事,同事只持有中介者——信息不对称是设计使然:规则需要全景,零件不需要。
4. 传统 C++ 写法(C++11 之前)
GoF 原书时代的形态:同事基类持中介者裸指针、changed()单入口上报,具体中介者集中实现规则:
// C++98/03 写法#include<cstdio>#include<string>// ---- 中介者接口 ----classWidget;classDialogMediator{public:virtual~DialogMediator(){}virtualvoidwidgetChanged(Widget*w)=0;protected:DialogMediator(){}private:DialogMediator(constDialogMediator&);DialogMediator&operator=(constDialogMediator&);};// ---- 同事基类:只认识中介者 ----classWidget{public:explicitWidget(DialogMediator*m):mediator_(m),enabled_(true){}virtual~Widget(){}voidsetEnabled(boolon){enabled_=on;}boolenabled()const{returnenabled_;}protected:voidchanged(){// 单入口上报mediator_->widgetChanged(this);}DialogMediator*mediator_;boolenabled_;private:Widget(constWidget&);Widget&operator=(constWidget&);};// ---- 具体同事:找不到任何别的控件 ----classEditBox:publicWidget{public:EditBox(DialogMediator*m):Widget(m){}voidsetText(conststd::string&t){text_=t;changed();// 我变了,规则归中介者}conststd::string&text()const{returntext_;}private:std::string text_;};classButton:publicWidget{public:Button(DialogMediator*m):Widget(m){}voidclick(){changed();}// 点击也是“变了”};// ---- 具体中介者:规则的唯一住所 ----classLoginDialog:publicDialogMediator{public:LoginDialog():name_(this),pwd_(this),login_(this){}voidinit(){evaluate();}EditBox name_;EditBox pwd_;Button login_;private:voidwidgetChanged(Widget*w){(void)w;// 简化:统一重算evaluate();}voidevaluate(){// 规则:可单测的纯逻辑boolready=!name_.text().empty()&&!pwd_.text().empty();login_.setEnabled(ready);printf("登录按钮:%s\n",ready?"可用":"禁用");}};intmain(){LoginDialog dlg;dlg.init();dlg.name_.setText("alice");// 按钮仍禁用dlg.pwd_.setText("hunter2");// 按钮变可用return0;}三条传统写法的铁律:同事之间零引用——EditBox的头文件里找不到Button,谁违反谁就把网又织回去;规则只写在中介者——evaluate()是「对话框行为」的全部答案,控件少于十来个时「统一重算」是最稳的起步(谁变了不重要,重算一遍即可),规模大了再按w精确分派;装配与创建归中介者——同事出生即拿到中介者指针(本例成员初始化里的this),GoF 原书也指出中介者常兼任同事的创建者——先有枢纽,再挂零件。
5. 现代 C++ 进阶写法
升级零:规则重算函数化,中介者可单测。把「行为」从控件操作中剥离成纯函数:输入是各同事的状态快照,输出是应施加的动作——中介者的核心价值就凝成这样一个可以离线测试的函数:
// 节选:对话框行为 = 纯函数 + 应用动作structDialogState{boolnameEmpty;boolpwdEmpty;boolloggingIn;};structDialogActions{boolloginEnabled;};DialogActionsevaluate(constDialogState&s){// 纯函数return{!s.nameEmpty&&!s.pwdEmpty&&!s.loggingIn};}登录中的冻结、回调后的解冻,全部是往DialogState增加一个字段的事——规则的演化从「改散布各处的事件」变成「改一个结构体的字段」。
改进一:std::function回调槽——同事连中介者接口都不认识。再进一步解耦:同事不实现任何中介者协议,只暴露「我发生了什么」的回调槽,由中介者装配时订阅:
#include<functional>#include<string>classEditBox{public:std::function<void()>onChanged;// 回调槽voidsetText(std::string t){text_=std::move(t);if(onChanged)onChanged();}conststd::string&text()const{returntext_;}private:std::string text_;};// 中介者侧的装配:// name_.onChanged = [this] { evaluate(); };// pwd_.onChanged = [this] { evaluate(); };// login_.onClick = [this] { startLogin(); };这已是「中介者 + 观察者」的合体:同事是发布者(只管喊),中介者是订阅者(听懂并指挥)。同事的可复用性达到上限——它是一块纯粹的积木,谁都可以来订它的槽。
改进二:防膨胀——中介者的三条护栏。中介者吸走全部复杂度后自己会膨胀成上帝对象(第 6 节缺点,GoF 原话警告)。三条护栏:状态机化——交互含明显的阶段(编辑中/登录中/成功),把阶段显式建模,规则按状态分组(第 25 篇状态模式的正式方案);拆子中介者——对话框过大时按面板拆成几个小中介者,顶层只协调面板间交互(与第 14 篇附加外观同构);规则表化——「谁变了 → 谁该动」写成数据表,引擎统一执行,规则修改不再过编译。
展望:事件总线 / 消息中心是「去中心化的中介者」——广播取代指挥,适合无规则的松耦合通知;但只要交互里存在顺序、互斥、优先级这类真规则,显式的中介者就无可替代。C++ 没有反射,声明式 UI 绑定只能靠代码生成或宏,这也是 Qt 的 moc 存在的原因之一。
6. 优缺点与适用场景
- ✅ 优点(GoF 后果清单):同事彻底解耦——网状 n(n−1)/2 条边收成 n 条,同事互相不认识,可独立开发、独立复用;交互集中——「一组对象怎么配合」成为一处可读、可单测、可演化的代码;交互关系可独立变化——换一套联动规则,同事零件原封不动。
- ❌ 缺点(GoF 原话警示):中介者会变成上帝对象——它吸走了全部交互复杂度,稍不节制就长成无所不管的
GodManager(与第 14 篇外观同病,且更重);单点高扇入,调试多一跳(从控件跳进中介者再跳回控件);细粒度事件下的消息风暴——每敲一个键都触发全量重算,需要节流或精确分派。 - 🎯 适用场景:一组地位对等的对象需要互相联动且规则复杂——UI 控件联动(GoF 主例)、聊天室(服务器居中转发)、系统服务间协调、多方协议握手(谁先谁后、谁等谁);对象集合会动态增减;同事需要跨场景复用。
〔辨析〕中介者 vs 观察者(第 24 篇)——同样是星型,看谁做调度中心。结构都星型,气质相反:中介者是有意志的协调者——它知道业务规则,听同事报告后主动指挥多个对象(双向:收报告、发指令,收发内容都具体);观察者是无意志的广播台——主题只喊「我变了」,不关心谁在听、听众拿信息去做什么(单向:通知出去,后果自负)。判别问题:事件之后需要「决策并指挥多个对象」吗?需要,规则有顺序互斥——中介者;只是「发生了,感兴趣的自便」——观察者。两者常组合(第 5 节改进一正是观察者给中介者当通信底座)。另两条:中介者 vs 外观(第 14 篇)——外观是单向的「客户端进、子系统出」,中介者是双向的「让一组同事互操作」;中介者 vs 责任链(第 18 篇)——链上无中心、逐个传递找认领者,中介者居中收发、统一裁决。
7. 开源项目中的身影
先交代一个诚实现象:中介者管理的是业务交互规则,多住在项目的领域层,通用库里出现得少——库里常见的是它的器官(事件中心、服务注册表、系统协调者)。挑三个器官看:
POCO:NotificationCenter,广播式的协调中心。Poco::NotificationCenter是进程内的事件集线器:任何对象注册观察者,任何代码投递通知,中心负责分发——它是「无规则中介者」,站在中介者与观察者的连续谱正中:
// 说明性片段(需链接 PocoFoundation,// 签名有简化)Poco::NotificationCenter nc;nc.addObserver(Poco::Observer<Clock,Poco::Notification>(clock_,&Clock::onTick));nc.postNotification(newPoco::Notification);// 广播点评:它不指挥任何对象,只做转发——恰好证明第 6 节的分界:中心没有意志,就是观察者;中心长了规则,才是中介者。你的代码在它上面再叠一层决策逻辑,后者才是模式主体。
AOSP:ServiceManager,服务发现的居中人。Binder 体系里,各系统服务(SurfaceFlinger、ActivityManager……)互不持有,统一向ServiceManager注册,客户端与别的服务经getService("name")按名索骥——一个 native C++ 实现的名字中介者:
// 说明性片段(节选自 AOSP,简化)android::sp<android::IBinder>raw=android::defaultServiceManager()->getService(android::String16("SurfaceFlinger"));// 拿到的是远端服务的代理(第 16 篇),// 而发现这件事由中介者完成点评:没有它,每个服务都要认识所有潜在调用者(或被认识)——又是一张 n² 的网;有了它,服务之间只共享一个名字。第 16 篇曾把defaultServiceManager当工厂函数读过,本篇给它恢复完整身份:发现即中介。
AOSP:AMS,把中介者做到系统级。ActivityManagerService是 Android 最重的中介者:四大组件的启动、调度、优先级、进程回收全部经它居中裁决,各组件进程互相不直接对话。它也是本模式缺点的活标本——数万行的 AMS 被戏称「Android 的大管家」,上帝对象化的压力全靠持续拆分(各种*Helper/子服务)来对抗。工程寓意直白:中介者的规模,就是它省下的网状复杂度的账单。
三份代码合看:NotificationCenter 做无规则的转发、ServiceManager 做按名发现、AMS 做有意志的裁决——调度中心的「意志含量」从零到满,正好铺出从观察者到中介者的整条谱系。
本篇小结
一组对象互相联动时,别让它们互相认识:给交互一个专职的中间人,同事只认识它、只向它报告,网状的 n² 条边收成星型的 n 条,联动规则从散布各控件变成一处可单测的代码。写中介者的纪律:同事之间零引用、规则只写在中枢、装配归中介者;养中介者的护栏:规则重算函数化、事件改回调槽、大了就按状态机与子中介者拆。与观察者的分水岭一句话记牢:中心有意志(决策并指挥)是中介者,无意志(只广播)是观察者——两者还常常合体。至此行为型的「互动」讲完,下一篇备忘录模式转向「状态的历史」:对象经历的变化,如何在封装不破的前提下存档与回滚。
本文模式定义与角色划分参考了 Refactoring Guru《设计模式》中文版「中介者」一章,意图译文、对话框示例与上帝对象警示参考了 GoF《Design Patterns》第 5 章 Mediator 一节。