拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

C++中的中介者模式详解

C++中的中介者模式详解

前言

在真实的软件系统里,"对象之间互相调用"这件事往往比算法本身更容易失控。设想一个聊天室:如果每个User对象都持有其他所有User对象的指针,那么每新增一个用户,就要让所有人都知道它的存在,代码里会出现 O(N²) 条调用关系。更糟的是,任何一个人改名、下线、切换状态,都要通知一圈人。

中介者模式(Mediator Pattern)要解决的就是这个问题:把对象之间多对多的网状依赖,收敛成一对多的星型依赖,让协作逻辑集中到一个中介者对象里,各个对象只跟中介者打交道,彼此不再直接引用。

本文不只讲"怎么写",更会讲"为什么这么写",并给出一个可以编译运行的完整示例,最后整理出几个真正会踩的坑。

一、什么是中介者模式

GoF(Gang of Four)对它的定义是:

用一个中介对象来封装一系列的对象交互。中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。

拆开看有三个要点:


  1. 封装交互:交互规则从中散落在各个对象里,改成集中在中介者里。

  2. 消除显式引用:同事类(Colleague)不再持有彼此的指针。

  3. 交互可独立变化:改规则只改中介者,不动同事类。


它本质上是一种控制反转:原本由 A 决定"我要通知谁",现在由中介者决定"谁该被通知",同事类只负责"我发生了什么变化"。

二、为什么需要它:从网状到星型

假设有 N 个对象需要两两通信。

不使用中介者时,通信链路数是:

N × (N - 1) / 2

N = 10 时是 45 条,N = 100 时是 4950 条。每一条链路都意味着一个编译期依赖、一个运行期指针、一次潜在的空指针崩溃点。

使用中介者后,链路数是:

N

每个对象只依赖中介者一个。这就是中介者模式的核心收益:把 O(N²) 的耦合降到 O(N)。

用表格对比更直观:

维度无中介者有中介者
通信链路数N×(N-1)/2N
编译依赖同事类互相包含头文件只依赖中介者接口
新增对象成本修改所有相关对象只在中介者注册
交互逻辑位置分散在各对象中集中在中介者
可测试性需要构造整个对象网络可注入 Mock 中介者
主要风险耦合爆炸、难以维护中介者膨胀为"上帝对象"

三、角色组成

角色英文职责
抽象中介者Mediator定义同事类与中介者通信的接口
具体中介者ConcreteMediator协调各同事类,持有它们的引用
抽象同事类Colleague持有中介者的引用,定义自身行为
具体同事类ConcreteColleague实现自身行为,通过中介者与其他同事通信

关键的设计约束是:同事类知道中介者,中介者知道所有同事,但同事之间互不知道。这条约束一旦被打破(比如同事类里出现了dynamic_cast去访问另一个同事),中介者模式就名存实亡了。

四、代码实战:一个可运行的聊天室

下面这个例子完整可编译(C++17),包含私聊、广播、以及一个刻意的"纪律检查"逻辑,用来展示中介者如何集中承载规则。

// mediator_chatroom.cpp // 编译:g++ -std=c++17 -Wall -Wextra -o chatroom mediator_chatroom.cpp #include <algorithm> #include <iostream> #include <memory> #include <string> #include <unordered_map> #include <vector> class User; // ---------- 抽象中介者 ---------- class ChatRoom { public: virtual ~ChatRoom() = default; // 用户上线 / 下线 virtual void join(std::shared_ptr<User> user) = 0; virtual void leave(const std::string& name) = 0; // 核心:转发消息。from 为空表示系统消息 virtual void send(const std::string& from, const std::string& to, const std::string& msg) = 0; }; // ---------- 抽象同事类 ---------- class User { public: User(std::string name, std::shared_ptr<ChatRoom> room) : name_(std::move(name)), room_(std::move(room)) {} virtual ~User() = default; const std::string& name() const { return name_; } // 发送消息:只跟中介者交互,不关心谁收到 virtual void send(const std::string& to, const std::string& msg) { if (auto room = room_.lock()) { // 避免中介者先于本对象销毁 room->send(name_, to, msg); } } // 接收消息:由中介者回调 virtual void receive(const std::string& from, const std::string& msg) { std::cout << "[" << name_ << "] 收到来自 " << (from.empty() ? "系统" : from) << " 的消息: " << msg << "\n"; } protected: std::string name_; std::weak_ptr<ChatRoom> room_; // 弱引用打破循环依赖 }; // ---------- 具体中介者 ---------- class ConcreteChatRoom : public ChatRoom, public std::enable_shared_from_this<ConcreteChatRoom> { public: void join(std::shared_ptr<User> user) override { const std::string n = user->name(); if (users_.count(n)) { std::cout << "[系统] 用户 " << n << " 已在房间中\n"; return; } users_[n] = std::move(user); // 集中式的规则:新用户上线,广播给所有人 broadcast("系统", n + " 加入了聊天室"); } void leave(const std::string& name) override { if (users_.erase(name)) { broadcast("系统", name + " 离开了聊天室"); } } void send(const std::string& from, const std::string& to, const std::string& msg) override { if (to.empty()) { broadcast(from, msg); // 群发 return; } if (to == from) { // 规则集中在中介者:禁止私聊自己 std::cout << "[系统] 不能给自己发私聊\n"; return; } auto it = users_.find(to); if (it != users_.end()) { it->second->receive(from, msg); } else { // 目标不存在,回执给发送者 if (auto s = users_.find(from); s != users_.end()) { s->second->receive("系统", "用户 " + to + " 不存在"); } } } private: void broadcast(const std::string& from, const std::string& msg) { for (auto& [name, user] : users_) { if (name != from) { // 广播不回显给自己 user->receive(from, msg); } } } std::unordered_map<std::string, std::shared_ptr<User>> users_; }; int main() { auto room = std::make_shared<ConcreteChatRoom>(); auto alice = std::make_shared<User>("Alice", room); auto bob = std::make_shared<User>("Bob", room); auto carol = std::make_shared<User>("Carol", room); room->join(alice); room->join(bob); room->join(carol); std::cout << "---- Alice 群发 ----\n"; alice->send("", "大家好"); std::cout << "---- Bob 私聊 Carol ----\n"; bob->send("Carol", "晚上一起 review 代码?"); std::cout << "---- Alice 私聊自己(被中介者拦截)----\n"; alice->send("Alice", "喂"); std::cout << "---- Carol 私聊一个不存在的人 ----\n"; carol->send("Dave", "在吗"); std::cout << "---- Bob 下线 ----\n"; room->leave("Bob"); return 0; }

这段代码里,最值得玩味的是规则的归属:


  • "不能私聊自己"、"目标不存在要回执"、"广播不回显"、"新用户上线要广播"——这些规则全部集中在ConcreteChatRoom里。

  • User完全不知道房间里有几个人、谁在线、规则是什么。它只会两件事:send和receive。


这正是中介者模式的价值:当产品经理说"把广播改成只发给在线用户"时,你只改一个函数,不用碰 User 类。

五、中介者 vs 观察者

很多人会把这两个模式混起来,其实它们的关注点不同:

维度中介者 Mediator观察者 Observer
通信方向双向,多对多单向,一对多
核心目的降低同事间耦合状态变化通知
谁知道谁中介者知道全部同事主题知道所有观察者
典型场景对话框、聊天室、塔台事件系统、MVC 的 Model-View
组合方式中介者内部常用观察者实现观察者可被中介者管理

一句话区分:观察者解决"我怎么通知别人",中介者解决"别人之间怎么联系"。事实上一个成熟的中介者内部往往就是用观察者/信号槽实现的。

六、常见坑点

坑点 1:中介者持有同事的 shared_ptr,形成循环引用(内存泄漏)

这是 C++ 里最致命的问题。如果中介者用shared_ptr<User>持有同事,而同事也用shared_ptr<ChatRoom>持有中介者,两者的引用计数永远降不到 0,对象永远不会析构。

❌ 错误写法:

class User { std::shared_ptr<ChatRoom> room_; // 强引用中介者 }; class ConcreteChatRoom { std::vector<std::shared_ptr<User>> users_; // 强引用同事 }; // 结果:User 与 ChatRoom 互相保命,valgrind 报 definitely lost

✅ 正确写法(二选一,关键是打破一条边):

class User { std::weak_ptr<ChatRoom> room_; // 同事弱引用中介者 // 使用时先 lock(),见上文完整示例 }; // 或者反过来:中介者用 weak_ptr 持有同事(适用于同事生命周期由外部管理的场景) class ConcreteChatRoom { std::vector<std::weak_ptr<User>> users_; };

选择哪条边打破,取决于谁的生命周期更长、谁在外层被拥有。通常是"同事的生命周期由外部/中介者管理",所以让同事弱引用中介者更自然。

坑点 2:通知过程中同事被销毁(悬垂指针 / 迭代器失效)

广播时如果某个receive回调里调用leave()把自己从users_里删掉,就会在遍历unordered_map的中途修改容器:

❌ 错误写法:

void broadcast(const std::string& from, const std::string& msg) { for (auto& [name, user] : users_) { // 回调里可能 erase,迭代器失效 → UB user->receive(from, msg); } }

✅ 正确写法:先快照,再遍历。

void broadcast(const std::string& from, const std::string& msg) { std::vector<std::shared_ptr<User>> snapshot; snapshot.reserve(users_.size()); for (auto& [name, user] : users_) { snapshot.push_back(user); // 拷贝一份 shared_ptr,延长生命周期 } for (auto& user : snapshot) { if (user->name() != from) { user->receive(from, msg); // 即使回调中 erase 也不影响本快照 } } }

快照同时解决了两个问题:迭代器失效,以及"回调中被销毁对象的悬垂指针"。因为shared_ptr拷贝保证对象在本次遍历期间存活。

坑点 3:递归通知导致栈溢出或死循环

同事 A 的receive里又调用send,中介者再转发,很容易形成无限递归。例如"收到消息自动回复",两个机器人互回。

❌ 错误写法:无任何防护,A→B→A→B... 直到stack overflow。

✅ 正确写法:加一个递归深度/重入保护。

class ConcreteChatRoom : public ChatRoom { void send(const std::string& from, const std::string& to, const std::string& msg) override { if (depth_ > kMaxDepth) { // 例如 kMaxDepth = 8 std::cout << "[系统] 消息转发层级过深,已丢弃\n"; return; } struct Guard { // RAII 保证异常安全地恢复 int& d; explicit Guard(int& x) : d(x) { ++d; } ~Guard() { --d; } } guard(depth_); // ... 原有转发逻辑 } private: static constexpr int kMaxDepth = 8; int depth_ = 0; };

坑点 4:中介者膨胀成"上帝对象"

中介者模式最著名的反模式后果:所有逻辑都往里塞,最后中介者变成一个两千行的巨型类,圈复杂度爆炸,比原来的网状依赖更难维护。

预防手段:


  • 按职责拆分多个中介者,而不是一个全知全能的中介者(比如ChatRoom和PrivateMessageRouter分开)。

  • 把通用规则抽成独立的策略/规则对象,中介者只负责调度。

  • 给中介者加单元测试的难度做指标——如果测一个函数要 mock 十几个同事,说明该拆了。


坑点 5:多线程下的数据竞争

users_这个容器被多个线程同时join/send时是数据竞争(data race),这是 UB,TSAN 会直接报错。

✅ 正确写法:用互斥量保护容器操作,但不要在持锁时回调同事,否则回调里再调中介者就死锁。

void broadcast(const std::string& from, const std::string& msg) { std::vector<std::shared_ptr<User>> snapshot; { std::lock_guard<std::mutex> lk(mtx_); // 只在拷贝快照时持锁 for (auto& [name, user] : users_) snapshot.push_back(user); } for (auto& user : snapshot) { // 锁外回调,避免死锁 if (user->name() != from) user->receive(from, msg); } }

总结

中介者模式的核心思想只有一句话:把对象之间的网状依赖收敛为星型依赖,让协作规则集中、可改、可测。

用之前想清楚三件事:


  1. 中介者会不会变成上帝对象?如果会,先想清楚怎么拆分,再动手。

  2. 引用关系怎么打破循环?C++ 里记住:中介者与同事之间,总有一条边要用weak_ptr或裸引用。

  3. 通知过程中会不会改容器、递归重入、多线程并发?快照遍历 + 深度保护 + 锁外回调,这三招能挡住绝大多数线上事故。


模式的本质是权衡,不是教条。当对象数量少(比如只有 3 个)时,直接互相引用反而更简单清晰——不要为了用模式而用模式。

返回列表