C++中介者模式降低耦合度

2026-04-11 23:05:32 1944阅读 0评论

用中介者模式“拉群聊”,让C++对象不再私聊到崩溃

写过中大型C++项目的人都懂:当一个模块里 WidgetA 要通知 ServiceBServiceB 又得同步状态给 LoggerC,而 LoggerC 偶尔还要反向触发 WidgetA 的刷新——这时候类之间的头文件互相 #include,构造函数里传指针像串糖葫芦,改一行代码得编译八分钟,还总在运行时崩在某个没人碰过的回调里。这不是耦合,这是“对象社交绑架”。

中介者模式不是新概念,但很多人把它当成教科书里的摆设:画个UML图,写个空泛的 Mediator 接口,再塞两个 Colleague 类就交差。真正让它在C++里活起来的,是它如何把“谁该知道谁”这件事,从编译期硬依赖,变成运行期软协调。

举个实在例子:一个嵌入式设备的控制面板,有三个核心部件——物理按键(KeyPanel)、LCD显示驱动(LcdDriver)、蜂鸣器控制器(BuzzerCtrl)。按下音量键,LCD要刷新图标,蜂鸣器要响半秒;长按进入设置模式,LCD切界面,蜂鸣器连响两声;而当系统检测到低电量,又得让LCD标红、蜂鸣器慢闪——如果让 KeyPanel 直接持有 LcdDriver*BuzzerCtrl*,那它就不再是“按键”,而是半个调度中心。

中介者真正的价值,不在“多了一层抽象”,而在“砍掉所有横向指针”。
我们定义一个轻量级中介者:

class ControlMediator {
public:
    void setKeyPanel(KeyPanel* k) { key_ = k; }
    void setLcd(LcdDriver* l) { lcd_ = l; }
    void setBuzzer(BuzzerCtrl* b) { buzzer_ = b; }

    void onVolumeUp() {
        if (lcd_) lcd_->updateIcon(ICON_VOLUME_UP);
        if (buzzer_) buzzer_->beep(500);
        // 注意:这里不调用 key_->xxx,因为按键本身不关心后续反应
    }

    void onLowPower() {
        if (lcd_) lcd_->highlightWarning();
        if (buzzer_) buzzer_->slowBlink();
    }

private:
    KeyPanel* key_ = nullptr;
    LcdDriver* lcd_ = nullptr;
    BuzzerCtrl* buzzer_ = nullptr;
};

关键来了:KeyPanel 类里不再 #include "LcdDriver.h",也不存任何其他组件的指针。它只做一件事——检测按键事件,然后把意图告诉中介者

class KeyPanel {
public:
    explicit KeyPanel(ControlMediator& mediator) : mediator_(mediator) {}

    void handleInterrupt() {
        if (isVolumeUpPressed()) {
            mediator_.onVolumeUp();  // 不是“我要让LCD刷新”,而是“我触发了音量上调”
        }
    }

private:
    ControlMediator& mediator_; // 引用即可,无需智能指针——生命周期由上层统一管理
};

看到区别了吗?KeyPanel 不再知道 LCD 怎么刷新、蜂鸣器怎么响;它甚至不知道 LcdDriver 这个类名是否存在。它只表达“发生了什么”,中介者决定“接下来做什么”。解耦的本质,是把“怎么做”和“谁来做”彻底分开。

有人会问:那中介者自己不就成了上帝类?确实可能——但注意,这个风险是可控的。我们刻意让 ControlMediator 只处理跨组件的协调逻辑,绝不侵入具体业务实现。比如音量上调时要不要记录日志?那是 Logger 自己的事,中介者不碰;LCD刷新要不要加动画?那是 LcdDriver 内部封装的细节。中介者只做三件事:接收意图、判断上下文、触发已知接口。它不 new 对象,不读配置,不处理异常——这些都交给各组件自己扛。

更进一步,我们可以用 std::function 或信号槽机制(如 sigc++)让中介者更松散:

class FlexibleMediator {
public:
    using Action = std::function<void()>;
    void registerAction(const std::string& event, Action action) {
        actions_[event] = std::move(action);
    }

    void trigger(const std::string& event) {
        if (auto it = actions_.find(event); it != actions_.end()) {
            it->second();
        }
    }

private:
    std::unordered_map<std::string, Action> actions_;
};

这时 KeyPanel 只需 mediator.trigger("volume_up"),而谁响应、响应几次、是否异步,全由注册方决定。这种写法在需要热插拔模块(比如调试时临时加个USB日志上报)时特别管用。

当然,中介者不是万能膏药。如果两个类本就只在单个函数里短暂协作(比如 ParserTokenizer),硬加中介者反而绕路。它的适用边界很清晰:当三个及以上对象存在交叉通知、且通知逻辑随业务变化频繁时,中介者才真正开始省钱。 编译时间降下来,单元测试也容易了——测 KeyPanel,Mock 一个空中介者就行;测协调逻辑,单独喂数据进中介者验证输出。

最后说个容易被忽略的实践细节:中介者的生命周期必须严格长于所有同事对象。 常见坑是局部创建中介者,结果同事还在用野指针。建议在应用主类或单例中持有,或用 std::shared_ptr 配合 weak_ptr 管理(但别滥用——多数嵌入式场景用引用更轻量)。

写完这个模式,我常想起以前修打印机:每次卡纸,维修手册都让你先检查进纸轮、再看传感器、最后动电机。可现实是,你一通乱按后发现——原来只是盖子没合严。中介者模式就像那个“合盖”动作:它不替代轮子、传感器或电机,但它让整个系统有了统一的响应节奏。对象们不必再彼此凝视、互相猜疑,只要面向中介者说话,世界就安静了。

文章版权声明:除非注明,否则均为Dark零点博客原创文章,转载或复制请以超链接形式并注明出处。

发表评论

快捷回复: 表情:
验证码
评论列表 (暂无评论,1944人围观)

还没有评论,来说两句吧...

目录[+]