☰
C++中介者模式实战:从对象网状耦合到星型架构的工程重构指南
2026/10/9 4:23:23 网站建设 项目流程

以前我在做一个跨模块的消息分发组件时,各个模块之间的调用关系已经乱到改一个接口要牵连五六个地方的程度。A模块要通知B和C,B又要回传给A,C还要往D那边塞数据。排查问题的时候沿着调用链走一半,自己都忘了是从哪里进来的。后来我重构的时候引入了中介者模式(Mediator Pattern),把互相纠缠的网状调用改成了"大家都只跟调度中心说话"的星型结构,整个系统的可维护性直接上了一个台阶。

这篇文章我会从痛点场景开始讲,拆解中介者模式在C++里该涉及的几个核心角色,然后直接给一份能复用的实现代码,最后再用一个聊天室消息分发器的实战例子串一遍整个流程,同时把我在实际工程里踩过的几个坑也一并写出来。适合对设计模式有基础了解、想把它落地到C++项目中的开发者参考。

1. 从一团乱麻到中介者:对象网状耦合的痛点

1.1 一个让人头疼的真实场景:GUI控件互相联动

我们先回到最经典的场景——对话框控件联动。假设你维护一个设置界面,里面有复选框A("启用高级选项")、下拉框B("日志级别")、文本输入框C("日志文件路径")和按钮D("保存配置")。

业务规则是这样的:A被勾选后,B和C才允许操作;B选择了不同级别,C里的提示文字要跟着变;C的内容合法时,D才会亮起;A取消勾选,B和C恢复禁用,D变灰。

如果你直接在控件类里互相持有对方的指针去调用,代码会长成什么样子?大概是下面这样:

void Checkbox::onToggled(bool checked) { dropdown_->setEnabled(checked); textbox_->setEnabled(checked); if (!checked) { saveButton_->setEnabled(false); } } void Dropdown::onSelectionChanged(int index) { if (index == 0) { textbox_->setPlaceholder("默认路径"); } else { textbox_->setPlaceholder("自定义路径"); } saveButton_->setEnabled(!textbox_->getText().empty()); } void Textbox::onTextChanged(const std::string& text) { saveButton_->setEnabled(!text.empty()); }

这段代码看起来还行,问题出在它只是冰山一角。真实项目里一个对话框可能有十几个控件,联动规则比这复杂得多。A可能依赖B的某个状态,B又依赖C和D,D还会反向影响A。每个控件类里头都要塞三四个"外部伙伴"的指针,初始化的时候还得逐个set进去。

1.2 网状耦合的本质:为什么直接引用会失控

我们把控件看成节点,把"直接调用另一个控件方法"看成一条连线。N个控件两两交互,最坏情况下会产生 N×(N-1)/2 条连线。四个控件是6条,十个控件就是45条。这种结构在代码层面的直接后果有三点:

第一,编译依赖被打散又被迫集中。本来一个控件只需要关心自己,现在为了调用别人,头文件里要么include别人的完整定义,要么频繁用前置声明+指针。改了某个控件的接口,所有跟它相连的类重新编译。

第二,可读性急剧下降。控件和控件之间的业务规则散落在各自的实现里,你根本说不清"A勾选后B和C要禁用"这条完整规则到底写在哪一个类里。新人接手代码,只能沿着引用一个一个点进去看。

第三,复用基本没戏。你想把这套控件搬去另一个对话框,发现它们每一个都耦合了其他控件的具体类型,只能一起搬走,连带着搬走一堆用不上的依赖。

1.3 中介者的核心思路:对象间不再"私聊",统一"群聊"

中介者模式的思路其实特别朴素:不让他们两两打电话了,先打到总机,由话务员转接。

每个控件只认识中介者对象Mediator,自己状态改变了就发一条事件通知给中介者;中介者内部维护了一条完整的业务规则表,知道"这个事件来了,接下来要触发谁、调用什么"。这样N×(N-1)/2条连线直接降为N条。

上面那个对话框例子,重构之后就变成:

  • Checkbox只告诉Mediator:"我改变了,当前状态是已勾选"
  • Mediator收到后,根据规则去调Dropdown::setEnabled(true)、Textbox::setEnabled(true)
  • Dropdown也只发事件,后续改哪里的文字由Mediator决定

所有控件仍然彼此不直接引用。你删掉任何一个控件类型,其他控件类连编译错误都不报,只要Mediator里少写一行调用就行。这种"解耦带来的改动局部化",正是中介者模式最核心的价值。

2. 中介者模式的核心角色拆解:谁在当"调度中心"

2.1 四个参与者的职责边界

在C++里落中介者模式,你至少要有四个角色,我直接用前面的对话框例子说明它们的对应关系:

角色抽象/接口具体类职责
中介者接口Mediator声明所有同事对象共用的事件入口,通常叫notify或者onEvent定义"同事与调度中心通信"的契约
具体中介者ConcreteMediator对话框本身保存所有同事的引用,承载业务联动规则,决定事件如何分发
同事基类Colleague所有控件的公共基类持有中介者的指针,提供sentEvent这类受保护方法给子类使用
具体同事类Checkbox、Dropdown等具体的控件类在自身状态变化时发事件,不关心其他同事是否存在

这里最容易被忽略的是Colleague基类。很多人图省事直接在控件类里各自写一个mediator_->notify(...)的调用,结果每个控件都要手动保存中介者指针,代码重复。提供一个基类把这些公共逻辑收拢,后续新增控件类型就能少写很多样板代码。

2.2 为什么C++里中介者接口更适合抽象基类而非std::function

有朋友问过我一个问题:中介者的通知入口就一个notify,搞这么多接口是不是小题大做?直接用std::function<void(Colleague*, const std::string&)>传给每个控件不行吗?

在回调数量很少的场景下,std::function确实可以。但中介者模式里"事件分发"往往是多路分支的——不同事件类型对应不同处理逻辑,后续大概率还要加"查询状态""获取某个同事"之类的同步方法。std::function只解决了"调用入口"这一个问题,后面的分支还是得落在某个地方。抽象基类的好处是,事件入口和其他交互方法可以一起放进接口统一约束,编译器帮你兜底,子类漏实现某个方法会直接报错。

我的建议是:如果只是几个回调,std::function可以;一旦发现需要在多个地方分发多种事件,就规规矩矩用抽象基类。别混着来,混到最后接口边界极其模糊。

2.3 同事对象如何"认识"中介者:注册与通知流程

同事对象要联系上中介者,常见做法有两种。

一种是构造函数注入:

class Checkbox : public Colleague { public: explicit Checkbox(Mediator* mediator) : Colleague(mediator) {} };

另一种是setter注入:

class Checkbox : public Colleague { public: void setMediator(Mediator* mediator) override { mediator_ = mediator; } };

工程上我更偏好构造函数注入,理由很实际:它强制对象在创建时就必须把中介者绑定好,不可能出现"忘了调setMediator导致通知为空指针"的运行时问题。唯一需要setter的场景是中介者本身创建得比较晚,或者同事对象要用工厂统一构建,没法在构造时传入,这种情况才退而求其次。

整个通知流程是单向的:状态变化 → 调mediator_->notify(this, event)→ 中介者根据this和event查规则表 → 调用目标同事的公开方法。注意事件里要把this带上,否则中介者无法区分事件是哪个同事发出来的。

3. 一份可直接复用的C++中介者实现:从接口设计到生命周期管理

3.1 完整代码实现:接口与基类

下面这份代码是我从一个实际项目里提炼出来的标准形态,C++11以上都可编译。先把接口和基类亮出来。

// mediator.h #pragma once #include <string> #include <memory> // 前置声明,避免头文件互相include class Colleague; class Mediator { public: virtual ~Mediator() = default; // 同事发事件统一走这个口 virtual void notify(Colleague* sender, const std::string& event) = 0; }; class Colleague { public: explicit Colleague(Mediator* mediator) : mediator_(mediator) {} virtual ~Colleague() = default; // 禁止拷贝,避免出现两份同事对象指向同一个中介者导致的混乱 Colleague(const Colleague&) = delete; Colleague& operator=(const Colleague&) = delete; protected: void sendEvent(const std::string& event) { if (mediator_) { mediator_->notify(this, event); } } Mediator* mediator_; };

要点有两处。Colleague的拷贝构造我直接delete掉了,这是用实际教训换来的——同事对象如果被拷贝一份,拷贝体里的mediator_还是那个指针,但它自己跑去注册进中介者的事件表,就会出现"一个事件被处理两次"的怪异现象。某些同事类是值语义的,真要拷贝就要显式重新绑定中介者,别让编译器帮你悄悄生成默认拷贝。

sendEvent是受保护方法,好处是只有子类自身能发起通知,外部代码无法绕过中介者直接触发某个同事的事件流,分发的统一性就保住了。

3.2 具体中介者:把业务规则集中到一个地方

接着来看具体中介者和同事类的实现。这里我用对话框的简化版示范,注意中介者里直接持有同事的裸指针。

// dialog_mediator.h #pragma once #include "mediator.h" class Checkbox; class Dropdown; class Textbox; class SaveButton; class DialogMediator : public Mediator { public: void setCheckbox(Checkbox* cb) { checkbox_ = cb; } void setDropdown(Dropdown* dd) { dropdown_ = dd; } void setTextbox(Textbox* tb) { textbox_ = tb; } void setSaveButton(SaveButton* sb) { save_button_ = sb; } void notify(Colleague* sender, const std::string& event) override; private: Checkbox* checkbox_ = nullptr; Dropdown* dropdown_ = nullptr; Textbox* textbox_ = nullptr; SaveButton* save_button_ = nullptr; };

实现文件里就是具体规则:

// dialog_mediator.cpp #include "dialog_mediator.h" #include "controls.h" void DialogMediator::notify(Colleague* sender, const std::string& event) { if (sender == checkbox_ && event == "toggled") { bool checked = checkbox_->isChecked(); dropdown_->setEnabled(checked); textbox_->setEnabled(checked); if (!checked) { save_button_->setEnabled(false); } return; } if (sender == dropdown_ && event == "selection_changed") { textbox_->setPlaceholder(dropdown_->currentItem()); save_button_->setEnabled(!textbox_->getText().empty()); return; } if (sender == textbox_ && event == "text_changed") { save_button_->setEnabled(!textbox_->getText().empty()); return; } }

这个中介者的notify看起来就是一堆if,但它把所有联动规则集中到了一个文件里头。以后要改规则,只需要打开dialog_mediator.cpp,对照着改,不用在四个控件类之间来回跳。某个控件的事件不需要分发时,直接不写那个分支就行,其他控件完全感知不到改动。

代码风格上我刻意保持朴素,没在notify里写多个tab页的巨大switch,也没上反射。因为中介者这种"集中式规则表"本质上面积不会太大,非要提取得过分抽象反而增加阅读负担。

3.3 为什么我不在实现里用shared_ptr持有同事对象

这里必须解释一个关键选择:DialogMediator里我用的是裸指针,不是shared_ptr或unique_ptr。

原因有三个。第一,中介者并不拥有同事对象的生命周期。在对话框的场景中,控件通常由界面布局对象持有,或者直接创建在栈上。中介者只是"借用"这些指针来调用方法,一旦生命周期所有权交给中介者,析构顺序就很难控制,容易造成二次释放。

第二,如果中介者持有shared_ptr而同事对象又持有中介者的shared_ptr,就形成了循环引用,两个对象都析构不掉,内存泄漏。想打破循环就得引入weak_ptr,系统复杂度直接翻倍。

第三,裸指针配合"先创建同事、再set给中介者"的顺序,配合断言可以尽早发现"中介者里引用了一个已析构对象"的问题。我习惯在notify入口处加一行断言,检查所有指针非空,调试期就能抓到多半问题:

void DialogMediator::notify(Colleague* sender, const std::string& event) { assert(checkbox_ && dropdown_ && textbox_ && save_button_ && sender); // 后续分发逻辑 }

3.4 事件标识的选择:枚举、字符串还是 constexpr 常量

notify的第二参数event我用的是std::string。有经验的C++开发者会立刻问:为什么不用enum class?字符串拼错了怎么办?

说实话,enum class的类型安全确实更好。但中介者的事件标识不一定限定在一个封闭集合里——你可能要对接外部模块传来的一堆字符串消息,事件名会动态出现。用枚举就得提前把所有事件都列出来,灵活性大打折扣。

工程上比较稳的折中方案是:定义一个命名空间常量,把所有事件名集中管理:

// events.h #pragma once namespace events { inline constexpr const char* kToggled = "toggled"; inline constexpr const char* kSelectionChanged = "selection_changed"; inline constexpr const char* kTextChanged = "text_changed"; } // namespace events

使用方写sendEvent(events::kToggled),其实是把字符串的拼写错误风险降低到了和枚举差不多的程度,又保留了字符串的灵活性。我推荐这个方案,既能拿字符串的好处,又不会让事件名散落在代码各处。

4. 实战演练:用一个聊天室消息分发器打通全流程

4.1 从控件联动到消息分发:场景设定

对话框控件的例子是中规中矩的中介者。接下来去掉GUI背景,换个更贴近服务端开发的场景:一个简单的聊天室消息分发器。

我们有三种角色:

  • User:代表一个聊天参与者,可以发消息、收消息
  • SystemBot:自动回复某些关键词消息
  • RoomLog:把每一条消息记录到日志

直接在User之间互相持有对方指针去发送消息,会变成O(N^2)的全连接结构——每来一个新用户,所有人都得知道他的地址。而聊天室天然就应该有一个"中心"来统一管理成员、接收消息、决定消息是广播给所有人还是定向发给某个人。

这个中心,就是中介者ChatRoom。

4.2 核心代码:注册、广播与定向投递

先定义同事基类和事件类型。这里我把User、SystemBot和RoomLog都当作同事来处理。

// chat_room.h #pragma once #include <unordered_map> #include <string> #include <functional> #include "mediator.h" enum class MessageType { Broadcast, // 所有用户都能看到 Direct, // 定向投递,比如私聊 System // 系统级别的通知 }; struct ChatMessage { std::string from; std::string content; MessageType type; }; class ChatRoom : public Mediator { public: void notify(Colleague* sender, const std::string& event) override; void registerUser(Colleague* user, const std::string& name); void unregisterUser(const std::string& name); void deliverMessage(const ChatMessage& msg); private: std::unordered_map<std::string, Colleague*> users_; };

同事类User的声明:

// user.h #pragma once #include <string> #include "mediator.h" class User : public Colleague { public: User(Mediator* mediator, std::string name) : Colleague(mediator), name_(std::move(name)) {} void sendMessage(const std::string& content); void sendDirectMessage(const std::string& to, const std::string& content); void receiveMessage(const ChatMessage& msg); const std::string& name() const { return name_; } private: std::string name_; };

实现关键在User::sendMessage:用户对象不直接遍历其他用户,它只是组装好一个ChatMessage,通过sendEvent发给ChatRoom。

// user.cpp #include "user.h" #include "chat_room.h" void User::sendMessage(const std::string& content) { ChatMessage msg{name_, content, MessageType::Broadcast}; sendEvent("user_message"); // 注意:ChatRoom后续如何拿到msg? // 这里有个设计点,下面会解释。 }

这里卡住了一个常见问题:sendEvent只传了事件名,没传消息体。要解决这个问题,我在Colleague基类里扩展一个方法,让子类能携带自定义数据:

class Colleague { public: // 原有的 sendEvent 保留 protected: template <typename... Args> void sendEventWithArgs(const std::string& event, Args&&... args) { if (mediator_) { mediator_->notifyWithArgs(this, event, std::forward<Args>(args)...); } } };

对应的Mediator接口也要跟上:

class Mediator { public: virtual ~Mediator() = default; virtual void notify(Colleague* sender, const std::string& event) = 0; // 带参数的版本,默认实现转发到 notify template <typename... Args> void notifyWithArgs(Colleague* sender, const std::string& event, Args&&... args) { notify(sender, event); } };

ChatRoom里可以对带参数的版本做特化。用模板特化还是std::any?这里我不引入过重的机制,实际项目里我用的是:让notify接受一个std::shared_ptr<void>类型的负载,分发时再强转回具体类型。

void ChatRoom::notify(Colleague* sender, const std::string& event) override { // 这里只做事件路由,详细消息体从事件里携带 if (event == "user_message") { auto msg = pending_message_; if (!msg) return; if (msg->type == MessageType::Broadcast) { for (auto& [name, user] : users_) { if (user != sender) { static_cast<User*>(user)->receiveMessage(*msg); } } } else if (msg->type == MessageType::Direct) { Colleague* target = users_[msg->to]; if (target) { static_cast<User*>(target)->receiveMessage(*msg); } } } }

这只是一个演示用的简化版本,实际工程里负载用std::any、std::variant或者直接增加不同的notify重载都可以。关键是思路:事件名决定"要不要处理",消息体决定"怎么处理",两者分离。

4.3 测试驱动:怎么验证中介者逻辑是对的

写了中介者之后最怕一个问题:事件满天飞,测试特别难写。好消息是中介者模式天然对测试友好,原因在于你可以往中介者里注入Fake同事对象,然后验证调用。

比如我要测试"User A发广播消息,User B应该收到,User C没登录就不该收到":

// test_chat_room.cpp #include <cassert> #include "chat_room.h" #include "user.h" class MockUser : public User { public: using User::User; int receive_count = 0; std::string last_content; void receiveMessage(const ChatMessage& msg) override { receive_count++; last_content = msg.content; } }; void test_broadcast_message() { ChatRoom room; MockUser alice(&room, "alice"); MockUser bob(&room, "bob"); room.registerUser(&alice, "alice"); room.registerUser(&bob, "bob"); alice.sendMessage("hello, world"); assert(bob.receive_count == 1); assert(bob.last_content == "hello, world"); }

测试的关键在于:有了中介者,你可以直接控制registerUser的顺序,构造各种边界情况,比如"发送者还没注册就发消息""同一个名字注册两次"之类的场景。这些要是没有中介者,你得先构造一堆用户对象的关系,测试代码写得比业务代码还累。

5. 该用和不该用的边界:中介者与观察者/命令模式的取舍

5.1 三种模式各管什么事

中介者模式经常和观察者模式(Observer)、命令模式(Command)混在一起,因为它们都涉及到"事件""回调""解耦"。实际工程里我见过有人把三者揉成一团,最后谁也说不清哪个是哪个。

模式核心用途典型场景关键区别
中介者把多个对象之间的交互逻辑集中到一处对话框控件联动、聊天室消息分发同事对象之间完全隔离,交互由中介者调度
观察者一个对象状态变化,多个订阅者收到通知数据模型变更驱动UI刷新发布者不关心谁订阅,订阅者自主注册
命令模式把一次操作封装成一个对象,支持延迟/撤销/排队编辑器里的Undo/Redo、任务队列重点是操作的封装与可撤销,不是对象间的互相通信

中介者和观察者最大的区别是:观察者里,发布者不知道订阅者具体是什么类型,也不需要知道;中介者则是明确知道每个同事是谁,并且有一套完整的业务规则来决定怎么调度它们。中介者比观察者更"偏中心化",观察者更"偏去中心化"。

5.2 中介者的"上帝对象"危机如何破解

中介者模式被吐槽最多的点是具体中介者容易膨胀成一个啥都干的"上帝对象"。在一个复杂系统里,所有业务规则都堆在ChatRoom::notify里,方法体越写越长,最终变成一个没人敢动的巨无霸。

我在实践中用的做法是:把大型中介者按业务域拆成多个小中介者,每个小中介者只负责一组高内聚的同事对象。比如上面的聊天室,可以拆成:

  • MembershipMediator:负责用户注册、注销、在线状态管理
  • MessageMediator:负责消息广播、定向投递
  • ModerationMediator:负责敏感词过滤、禁言操作

同事发事件时先发给上一层的聚合门面,再由门面决定路由到哪个子中介者。这样每个子中介者的notify体积都被限制在可控范围内。

还有一种更轻的缓解做法:不在notify里写具体的业务逻辑,而是把每个事件的处理函数拆成独立的方法,notify只做路由:

void ChatRoom::notify(Colleague* sender, const std::string& event) { if (event == "user_message") { handleUserMessage(sender); } else if (event == "user_joined") { handleUserJoined(sender); } else if (event == "user_left") { handleUserLeft(sender); } }

这样的好处是每个handle方法都能单独测试,不至于为了测一条消息分发逻辑得先把五十行分支全部mock掉。

5.3 什么时候该上中介者:我的三条判断准则

我给自己定了三条简单的判断标准,满足其一时就值得考虑中介者:

第一,两个以上的对象需要互相感知并联动。如果只是单向通知,观察者就够用了,上中介者属于杀鸡用牛刀。

第二,对象之间存在"改一个就要牵动一串"的规则。你发现每次修改业务规则都要同时改三四个类,说明交互逻辑分散在这些类里了,应该集中到一个中介者里。

第三,你需要在运行时动态调整交互关系。中介者里维护同事列表,可以随时增删,这一点在聊天室这种成员动态进出的场景下非常关键。直接用对象互相引用的话,动态增删一个成员要同时改好多处。

反过来,如果两个类的交互非常固定、很少变化,中介者反而是多余的。硬套中介者只会多出一层间接调用,读代码要多跳一次,维护成本没有降低反而增加。

6. 实现中的常见坑与我的调试心得

6.1 坑一:通知链路里的循环调用

中介者把交互集中之后,一个事件可能触发新的状态变化,新的状态变化又发事件。一旦规则设计不清,就会出现A发事件 → 中介者调B → B内部又发事件 → 中介者调A → A再发事件……递归无限加深,最后栈溢出。

我在一个消息推送系统里真的遇到过:用户上线触发通知,通知模块给中介者发"已通知"事件,中介者又回头查询用户状态并再次推送。

规避方法有两个。第一个是在中介者里加一个"处理中"标志位:

class ChatRoom : public Mediator { private: bool handling_ = false; }; void ChatRoom::notify(Colleague* sender, const std::string& event) { if (handling_) return; // 防重入 handling_ = true; // 处理分发逻辑 handling_ = false; }

但这个方法过于粗暴——它会丢弃重入事件而不是排队处理。第二个更精细的做法是事件去重:给每个事件按"发送者+事件名+序号"做一个短期记录,重复的事件直接忽略。

不过我的经验是,循环调用本质上反映了规则设计有问题:好端端的通知链路不应该出现"反馈回路"。优先考虑的是让事件流变成单向的DAG,而不是在运行期打补丁。

6.2 坑二:中介者持有裸指针,同事析构后悬垂

中介者持有同事的裸指针,最大的风险是同事先于中介者析构。比如某个用户退出聊天室时先销毁了User对象,但ChatRoom的users_里还留着一个悬垂指针。下一次广播消息遍历到那个用户,调用receiveMessage就直接未定义行为。

这类问题排查起来非常痛苦,因为它只在特定时序下出现。我现在的经验是两条腿走路:

一是在同事析构时主动告诉中介者"我要走了"。在聊天室场景里就是统一的leaveRoom接口,在GUI场景里对应"控件从父窗口移除时调用unregister"。这个"注销"动作要贯穿所有脱离中介者管理的路径,包括异常分支。

二是在调试期用智能指针辅助检测。同事对象实际生命周期由外部管理,外部持有它的shared_ptr;中介者里存weak_ptr。分发消息前lock()一下,优秀的编译器在debug模式还能触发一些引用计数检查,能帮我们提前暴露"某处持有了一个即将失效的引用"。

class ChatRoom : public Mediator { private: std::unordered_map<std::string, std::weak_ptr<Colleague>> users_; }; void ChatRoom::deliverMessage(const ChatMessage& msg) { if (msg.type == MessageType::Broadcast) { for (auto it = users_.begin(); it != users_.end();) { auto user = it->second.lock(); if (!user) { it = users_.erase(it); // 顺手清理失效项 continue; } static_cast<User*>(user.get())->receiveMessage(msg); ++it; } } }

这个写法比裸指针安全得多,代价是同事对象创建时必须接受shared_ptr管理。如果同事对象必须栈上分配,那就要严格保证"先创建同事、后释放同事"。我的体会是:别在这种地方省内存操作,悬垂指针的调试成本远高于多写几个weak_ptr。

6.3 坑三:事件标识的拼写错误与难调试

用字符串当事件名,拼写错误是跑不掉的编译期漏网之鱼。sendEvent("message_broadcast")和notify里的"mesage_broadcast"只差一个字母,运行期静默失效,查半天。

我解决这个问题的办法前面提过:集中在命名空间常量里定义。但还有一个更实用的调试技巧——在Mediator::notify入口打印日志,把事件名和发送者关键信息打出来。

void ChatRoom::notify(Colleague* sender, const std::string& event) { log("ChatRoom::notify sender=" + sender->name() + " event=" + event); // 路由逻辑 }

不要小看这一行日志。线上排查"消息没送达"的问题时,如果能看到notify被调用了但后续分发没生效,问题就能定位到ChatRoom内部的规则分支;如果连notify都没被调,那问题在同事对象发事件的那一步,排查范围直接缩小一半。

6.4 关于性能的补充:中介者会不会成为瓶颈

中介者把通信集中到一点,自然会有人担心性能:是不是所有消息都要经过中介者转发,中介者会成为吞吐瓶颈?

通信量不大时,这种担心完全多余,一次中介者调用只是多了一层间接调用,现代C++编译器还会内联,额外开销微乎其微。真正需要担心的是中介者内部处理逻辑太重,比如广播时挨个调用同事方法,如果同事数量很大且每次都有重I/O,那确实该考虑加中间队列或者改异步。

我的经验是:先把中介者做对,做清楚,再考虑性能优化。为了一点"可能存在的开销"放弃模式带来的结构清晰,是典型的本末倒置。若真到了需要优化的时候,有个很自然的路径——让中介者在队列通知和同步分发之间做切换,同事接口不用改,影响面可控。

最后再分享一点实际体会

中介者模式在C++里实现起来门槛不高,难的是掌握"什么时候用、什么时候千万别用"的分寸。我自己吃过亏,也见过别人把本来简单的两个类硬套中介者,结果代码变得更绕。说穿了,中介者是给"多对多复杂交互"准备的,不是给"一对多简单事件"准备的。

如果要在项目里引入它,我建议从聊天室或者对话框这种有明显的"中心节点"形态的业务入手,先让整个链路跑通,再逐步把规则收拢进去。重构过程中把界面、消息模块里互相渗透的控制逻辑一点点剥离出来,你会发现代码的可读性和可测试性慢慢就上来了。中介者不会帮你写出更精妙的算法,但它能让你的项目在长大之后,不至于变成一个牵一发动全身的泥潭。

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

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

立即咨询