c++中介者不应模板化,而应通过抽象基类component和weak_ptr管理组件,notify仅传事件语义,禁用sender参数与dynamic_cast,用notifying_标志防循环调用,坚持虚函数on_event实现零开销解耦。

中介者模式在C++里到底要不要写成模板类
多数人一想“通用中介者”,就立刻套用 template<typename... components></typename...>,结果发现根本没法注册运行时对象、类型擦除成本高、调试信息全丢。C++的中介者不是为了泛型而泛型,而是为了解耦具体组件间的直接调用。真正轻量的做法是:**让中介者持有具体组件类型的裸指针或 std::weak_ptr,不模板化中介者本身,只在组件注册接口上做类型约束**。
常见错误现象:Mediator<button dialog logger></button> 导致每个组合都要实例化一个新类,链接体积膨胀,且无法在运行时动态增删组件。
- 推荐做法:定义抽象基类
Component,所有参与交互的类继承它;中介者内部用std::vector<:weak_ptr>></:weak_ptr>管理成员 - 组件注册时用
mediator.register_component(std::static_pointer_cast<component>(button))</component>,避免虚函数表开销过大的强制转型 - 如果组件间通信有强类型需求(如只允许
Button发ClickEvent),把事件建模为 enum class 或小结构体,在notify()参数中显式传递,而不是靠模板参数推导
notify() 该不该带发送方参数
带发送方参数(void notify(Component* sender, const Event& e))看似灵活,实则埋下隐式依赖:接收方开始检查 sender == button_a,逻辑又悄悄绕回“知道谁发的”——这违背了中介者初衷。轻量级实现应默认**发送方不可见**,只暴露事件语义。
使用场景差异明显:GUI控件协同(如点击按钮后禁用输入框)不需要知道谁点的,只需要“提交动作已触发”;但调试日志或权限校验可能真要溯源。这时更稳妥的做法是把来源信息塞进 Event 结构体里,由事件定义者决定是否携带,而非由中介者接口强制要求。
- 基础版
notify(const Event& e)足够覆盖 80% 场景 - 需要溯源时,定义
struct ClickEvent { std::string source_id; };,让组件自己填充,中介者不解释也不过滤 - 绝对避免在
notify()内部做dynamic_cast判断发送方类型——这等于在中介者里重写组件关系逻辑
如何防止循环通知和重复响应
典型问题:A 通知中介者 → 中介者转发给 B → B 处理中又调用 mediator.notify() → A 再次收到同类型事件 → 死循环或状态错乱。这不是设计缺陷,而是 C++ 对象生命周期和调用栈可见性导致的现实约束。
最轻量的解法不是加锁或消息队列,而是用一个 bool notifying_ = false 成员 + 短路检查:
void Mediator::notify(const Event& e) {
if (notifying_) return;
notifying_ = true;
for (auto& comp : components_) {
if (auto ptr = comp.lock()) {
ptr->on_event(e);
}
}
notifying_ = false;
}
这个方案不阻塞、无额外分配、兼容所有 C++11+ 编译器。但它有个硬性前提:所有 on_event() 必须是同步、非重入的。如果你的组件内部会间接再调中介者(比如日志组件写完日志又触发监控上报),就得改用事件队列 + 单次 flush 模式,那就不算“轻量级”了。
- 禁止在
on_event()回调里直接调mediator.notify() - 若必须链式响应,改为返回
std::optional<event></event>,由中介者统一判断是否继续派发 - 调试时可临时开启
std::stack<const char> call_trace_</const>记录路径,但上线前删掉——它破坏轻量性
为什么不用 std::function 做事件分发
有人尝试把“监听器”做成 std::map<eventtype std::vector event>>></eventtype>,看起来解耦干净,实际掉进三个坑:内存碎片(每次绑定都 new)、类型擦除开销(虚调用)、以及最关键的——监听器生命周期完全失控。组件析构了,std::function 还拿着悬垂引用,一触发就崩。
真正的轻量在于控制权清晰:中介者只管转发,不托管回调;组件自己负责注册/注销,且注销必须显式调用(不能靠 weak_ptr 自动失效,因为 weak_ptr 无法触发中介者清理 map 条目)。
- 坚持用虚函数
Component::on_event(),零开销抽象,编译期可内联 - 注册接口命名明确,如
register_for_events(EventType::SUBMIT),比泛化的subscribe()更易审计 - 如果真要支持 lambda 监听,提供独立的
add_anonymous_listener()并要求传入std::shared_ptr<void></void>作 owner token,否则拒绝注册
轻量的核心不是代码行数少,而是每个对象的职责边界像刀切一样清楚。一旦你开始在中介者里管理回调生命周期、缓存事件、做线程调度,它就已经不是中介者,而是服务总线了。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











