ARTICLE DETAIL

资讯详情

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

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

C++中的中介者模式详解 前言在真实的软件系统里对象之间互相调用这件事往往比算法本身更容易失控。设想一个聊天室如果每个User对象都持有其他所有User对象的指针那么每新增一个用户就要让所有人都知道它的存在代码里会出现 O(N²) 条调用关系。更糟的是任何一个人改名、下线、切换状态都要通知一圈人。中介者模式Mediator Pattern要解决的就是这个问题把对象之间多对多的网状依赖收敛成一对多的星型依赖让协作逻辑集中到一个中介者对象里各个对象只跟中介者打交道彼此不再直接引用。本文不只讲怎么写更会讲为什么这么写并给出一个可以编译运行的完整示例最后整理出几个真正会踩的坑。一、什么是中介者模式GoFGang of Four对它的定义是用一个中介对象来封装一系列的对象交互。中介者使各对象不需要显式地相互引用从而使其耦合松散而且可以独立地改变它们之间的交互。拆开看有三个要点封装交互交互规则从中散落在各个对象里改成集中在中介者里。消除显式引用同事类Colleague不再持有彼此的指针。交互可独立变化改规则只改中介者不动同事类。它本质上是一种控制反转原本由 A 决定我要通知谁现在由中介者决定谁该被通知同事类只负责我发生了什么变化。二、为什么需要它从网状到星型假设有 N 个对象需要两两通信。不使用中介者时通信链路数是N × (N - 1) / 2N 10 时是 45 条N 100 时是 4950 条。每一条链路都意味着一个编译期依赖、一个运行期指针、一次潜在的空指针崩溃点。使用中介者后链路数是N每个对象只依赖中介者一个。这就是中介者模式的核心收益把 O(N²) 的耦合降到 O(N)。用表格对比更直观维度无中介者有中介者通信链路数N×(N-1)/2N编译依赖同事类互相包含头文件只依赖中介者接口新增对象成本修改所有相关对象只在中介者注册交互逻辑位置分散在各对象中集中在中介者可测试性需要构造整个对象网络可注入 Mock 中介者主要风险耦合爆炸、难以维护中介者膨胀为上帝对象三、角色组成角色英文职责抽象中介者Mediator定义同事类与中介者通信的接口具体中介者ConcreteMediator协调各同事类持有它们的引用抽象同事类Colleague持有中介者的引用定义自身行为具体同事类ConcreteColleague实现自身行为通过中介者与其他同事通信关键的设计约束是同事类知道中介者中介者知道所有同事但同事之间互不知道。这条约束一旦被打破比如同事类里出现了dynamic_cast去访问另一个同事中介者模式就名存实亡了。四、代码实战一个可运行的聊天室下面这个例子完整可编译C17包含私聊、广播、以及一个刻意的纪律检查逻辑用来展示中介者如何集中承载规则。// mediator_chatroom.cpp // 编译g -stdc17 -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_ptrUser 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_ptrChatRoom 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_ptrChatRoom room_; // 弱引用打破循环依赖 }; // ---------- 具体中介者 ---------- class ConcreteChatRoom : public ChatRoom, public std::enable_shared_from_thisConcreteChatRoom { public: void join(std::shared_ptrUser 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_mapstd::string, std::shared_ptrUser users_; }; int main() { auto room std::make_sharedConcreteChatRoom(); auto alice std::make_sharedUser(Alice, room); auto bob std::make_sharedUser(Bob, room); auto carol std::make_sharedUser(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_ptrUser持有同事而同事也用shared_ptrChatRoom持有中介者两者的引用计数永远降不到 0对象永远不会析构。❌ 错误写法class User { std::shared_ptrChatRoom room_; // 强引用中介者 }; class ConcreteChatRoom { std::vectorstd::shared_ptrUser users_; // 强引用同事 }; // 结果User 与 ChatRoom 互相保命valgrind 报 definitely lost✅ 正确写法二选一关键是打破一条边class User { std::weak_ptrChatRoom room_; // 同事弱引用中介者 // 使用时先 lock()见上文完整示例 }; // 或者反过来中介者用 weak_ptr 持有同事适用于同事生命周期由外部管理的场景 class ConcreteChatRoom { std::vectorstd::weak_ptrUser 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::vectorstd::shared_ptrUser 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这是 UBTSAN 会直接报错。✅ 正确写法用互斥量保护容器操作但不要在持锁时回调同事否则回调里再调中介者就死锁。void broadcast(const std::string from, const std::string msg) { std::vectorstd::shared_ptrUser snapshot; { std::lock_guardstd::mutex lk(mtx_); // 只在拷贝快照时持锁 for (auto [name, user] : users_) snapshot.push_back(user); } for (auto user : snapshot) { // 锁外回调避免死锁 if (user-name() ! from) user-receive(from, msg); } }总结中介者模式的核心思想只有一句话把对象之间的网状依赖收敛为星型依赖让协作规则集中、可改、可测。用之前想清楚三件事中介者会不会变成上帝对象如果会先想清楚怎么拆分再动手。引用关系怎么打破循环C 里记住中介者与同事之间总有一条边要用weak_ptr或裸引用。通知过程中会不会改容器、递归重入、多线程并发快照遍历 深度保护 锁外回调这三招能挡住绝大多数线上事故。模式的本质是权衡不是教条。当对象数量少比如只有 3 个时直接互相引用反而更简单清晰——不要为了用模式而用模式。
返回列表