C++/Qt 设计模式 · 第 16 天

Day 16:中介者模式(Mediator Pattern)

用一个"协调中枢"封装对象间的交互,让同事对象之间不再互相认识——网状依赖变星形依赖。

①今日主题

中介者模式(Mediator Pattern)定义:用一个中介对象来封装一组对象之间的交互方式,使各对象之间不再显式地互相引用,从而降低它们之间的耦合度,并且可以独立地改变它们之间的交互规则。

一句话:对象之间不直接对话,所有沟通都经过一个"协调中枢"。它把"多对多"的复杂交互网络,改造成"一对多"的星形结构——每个参与者只认识中介者,不认识其他参与者。这是行为型模式家族中专门解决"对象间通信失控"问题的模式。

本课是行为型模式的第 4 站。我们已经学过责任链(Day 13,把请求沿着链传递)、命令(Day 14,把请求封装成对象)、迭代器(Day 15,统一遍历方式)。今天的中介者与它们不同:它不改变请求的形态,而是改变对象之间通信的组织方式。学完今天的内容,你会理解为什么 Qt 项目里大量 UI 联动代码都适合收敛到一个"连接集中地"。

②为什么需要这个模式?

先看一个真实的软件场景:开发一个多人聊天室。最直觉的写法是——每个用户对象都持有其他所有用户的指针,直接给对方发消息:

// 坏味道版本:没有中介者,用户对象之间直接互相引用
class User {
public:
    User(std::string name) : name_(std::move(name)) {}

    // 想给谁发消息,就要拿到谁的指针
    void sendTo(User* other, const std::string& msg) {
        std::cout << name_ << " -> " << other->name() << ": " << msg << "\n";
        other->receive(this, msg);
    }

    void receive(User* from, const std::string& msg) {
        std::cout << name_ << " 收到: " << msg << "\n";
    }

    const std::string& name() const { return name_; }

private:
    std::string name_;
};

这段代码有四个致命问题:

再看一个 Qt 场景:一个空调控制面板,滑块、数字输入框、模式下拉框、状态栏四者需要联动——滑块拖动要同步数字框,模式切换要改变温度范围。如果让控件之间直接 connect 成网状,连接线会随着控件数量平方级增长,且牵一发动全身。

两个场景的共性:对象之间的交互规则正在变得复杂,而交互规则和对象本身的逻辑纠缠在一起。这时候需要一个"协调中枢"把通信收拢起来——这就是中介者模式出现的时机。

💡 判断信号:当你的代码里出现大量"对象 A 调用对象 B 的方法,B 又回调 A",或"新增一个对象就要改动 N 个旧对象"时,就是在提示你:通信需要集中管理了。

③核心思想

中介者模式的核心思想就一句话:引入一个独立的"协调中枢",让所有参与者只跟它说话,由它来决定消息怎么流转。

用生活类比理解:

回到软件工程,中介者模式的核心分工是:

为什么能降低耦合?因为耦合的本质是"知道的太多"。原本每个对象要知道其他所有对象的存在和接口,现在只需要知道一个中介者。对象之间的"多对多"关系被压缩成"每个对象对中介者"的"一对多"关系。通信的复杂度没有消失,但它被集中到了唯一一个地方,其他地方都是干净的。

⚠️ 需要注意:中介者模式是把"对象间的耦合"换成了"对象与中介者之间的耦合"。如果交互规则太复杂,中介者会膨胀成"上帝对象"——这是它的固有代价,后面第 ⑩ 节会专门讨论如何避免。

④UML / 角色关系

中介者模式的角色结构(用文字版类图表示,箭头表示"依赖/调用"方向):

┌────────────────┐       ┌─────────────────────┐
│  Mediator      │       │  Colleague(抽象)    │
│  (抽象中介者)   │       │  + setMediator()    │
│  + notify()    │◄──────│  + send()           │
└────────────────┘       │  + receive()        │
        ▲                └─────────▲───────────┘
        │                          │ 继承
┌───────┴────────┐        ┌────────┴────────────┐
│ ConcreteMediator│──────▶│ ConcreteColleague A │
│ (具体中介者)     │ 转发    │ ConcreteColleague B │
│ - colleagues[] │        │  ...                │
└────────────────┘        └─────────────────────┘

四个角色的职责:

依赖关系要点:

⑤最小 C++ 示例:聊天室

用 C++17 实现第 ② 节里的聊天室,用 ChatRoom 作中介者。这个示例完整可编译:

// mediator_chat.cpp —— 编译: g++ -std=c++17 mediator_chat.cpp -o mediator_chat
#include <iostream>
#include <memory>
#include <string>
#include <vector>

class ChatRoom;   // 前向声明:User 只需要"知道有这么一个中介者"

// ---------- 同事基类:所有参与聊天的对象 ----------
class User {
public:
    User(std::string name, ChatRoom* room)
        : name_(std::move(name)), room_(room) {}
    virtual ~User() = default;

    // 发消息:自己不找任何人,只把消息交给中介者
    void send(const std::string& message) {
        std::cout << "[" << name_ << "] 发送: " << message << "\n";
        room_->broadcast(this, message);   // 唯一对外出口
    }

    // 收消息:由中介者回调
    virtual void receive(const User* from, const std::string& message) {
        std::cout << "[" << name_ << "] 收到 [" << from->name()
                  << "] 的消息: " << message << "\n";
    }

    const std::string& name() const { return name_; }

protected:
    std::string name_;
    ChatRoom* room_;   // 只依赖中介者,不依赖任何其他 User
};

// ---------- 具体同事:管理员(对收到的消息有额外处理) ----------
class AdminUser : public User {
public:
    AdminUser(std::string name, ChatRoom* room)
        : User(std::move(name), room) {}

    void receive(const User* from, const std::string& message) override {
        std::cout << "[管理员 " << name_ << "] 审阅 [" << from->name()
                  << "] 的消息: " << message << "\n";
    }
};

// ---------- 具体中介者:持有全部同事,集中实现通信规则 ----------
class ChatRoom {
public:
    void join(User* user) { users_.push_back(user); }

    // 通信规则集中在这里:广播给除发送者以外的所有人
    void broadcast(User* sender, const std::string& message) {
        for (User* u : users_) {
            if (u != sender) {
                u->receive(sender, message);
            }
        }
    }

private:
    // 只登记不拥有:User 的生命周期由外部(main 的栈)管理,
    // 中介者用裸指针避免循环引用和所有权混乱
    std::vector<User*> users_;
};

int main() {
    ChatRoom room;                       // 1. 先有中介者
    User alice("Alice", &room);          // 2. 创建同事,只认识中介者
    AdminUser bob("Bob", &room);
    room.join(&alice);                   // 3. 向中介者注册
    room.join(&bob);

    alice.send("大家好,我是 Alice");      // 4. 同事间通信全部走中介者
    bob.send("欢迎 Alice!");
    return 0;
}

逐段解释:

程序输出:

[Alice] 发送: 大家好,我是 Alice
[管理员 Bob] 审阅 [Alice] 的消息: 大家好,我是 Alice
[Bob] 发送: 欢迎 Alice!
[Alice] 收到 [Bob] 的消息: 欢迎 Alice!

对比第 ② 节的坏代码:新增一个用户,现在只需 join() 一次,所有旧代码零改动——OCP 得到满足;通信规则收进 ChatRoom 一个类,SRP 得到满足。

⑥Qt 实战示例:空调控制面板

Qt 的信号槽天然支持对象间通信,但如果控件之间直接互相 connect,连接线仍会形成网状,且联动规则散落各处。本示例用 ControlMediator(继承 QObject)把所有控件信号收拢到一个类里:滑块、数字框、模式下拉框、状态栏彼此不认识,全部由中介者协调。

controlmediator.h

#ifndef CONTROLMEDIATOR_H
#define CONTROLMEDIATOR_H

#include <QObject>

class QSlider;
class QSpinBox;
class QComboBox;
class QLabel;

// 中介者:集中管理所有控件之间的联动,控件之间零耦合
class ControlMediator : public QObject {
    Q_OBJECT
public:
    explicit ControlMediator(QObject* parent = nullptr);

    // 注册控件:中介者只负责协调,不负责创建控件
    void setControls(QSlider* slider, QSpinBox* spinBox,
                     QComboBox* modeBox, QLabel* statusLabel);

private slots:
    void onSliderChanged(int value);   // 滑块变化 → 同步数字框与状态栏
    void onSpinChanged(int value);     // 数字框变化 → 同步滑块
    void onModeChanged(int index);     // 模式变化 → 调整温度范围

private:
    QSlider*   slider_      = nullptr;
    QSpinBox*  spinBox_     = nullptr;
    QComboBox* modeBox_     = nullptr;
    QLabel*    statusLabel_ = nullptr;
    bool syncing_ = false;  // 防回环标志:避免 滑块→数字框→滑块 死循环
};

#endif // CONTROLMEDIATOR_H

controlmediator.cpp

#include "controlmediator.h"

#include <QComboBox>
#include <QLabel>
#include <QSlider>
#include <QSpinBox>

ControlMediator::ControlMediator(QObject* parent) : QObject(parent) {}

void ControlMediator::setControls(QSlider* slider, QSpinBox* spinBox,
                                  QComboBox* modeBox, QLabel* statusLabel) {
    slider_ = slider;
    spinBox_ = spinBox;
    modeBox_ = modeBox;
    statusLabel_ = statusLabel;

    // ★ 全部信号连接集中在这一个函数里,而不是散落在各控件类中
    connect(slider_,  &QSlider::valueChanged, this, &ControlMediator::onSliderChanged);
    connect(spinBox_, QOverload<int>::of(&QSpinBox::valueChanged),
            this, &ControlMediator::onSpinChanged);
    connect(modeBox_, QOverload<int>::of(&QComboBox::currentIndexChanged),
            this, &ControlMediator::onModeChanged);

    onModeChanged(modeBox_->currentIndex());   // 按初始模式初始化界面
}

void ControlMediator::onSliderChanged(int value) {
    if (syncing_) return;              // 回环保护
    syncing_ = true;
    spinBox_->setValue(value);         // 滑块 → 数字框
    statusLabel_->setText(QStringLiteral("当前温度:%1 ℃").arg(value));
    syncing_ = false;
}

void ControlMediator::onSpinChanged(int value) {
    if (syncing_) return;              // 回环保护
    syncing_ = true;
    slider_->setValue(value);          // 数字框 → 滑块
    statusLabel_->setText(QStringLiteral("当前温度:%1 ℃").arg(value));
    syncing_ = false;
}

void ControlMediator::onModeChanged(int index) {
    // 通信规则集中地:不同模式对应不同温度范围
    // 制冷 16~30,制热 18~32,通风 0~40
    const int minTemp = (index == 0) ? 16 : (index == 1) ? 18 : 0;
    const int maxTemp = (index == 0) ? 30 : (index == 1) ? 32 : 40;

    slider_->setRange(minTemp, maxTemp);
    spinBox_->setRange(minTemp, maxTemp);
    spinBox_->setValue(minTemp);       // 切换模式时重置温度

    statusLabel_->setText(QStringLiteral("模式已切换,温度范围 %1~%2 ℃")
                              .arg(minTemp).arg(maxTemp));
}

main.cpp

#include "controlmediator.h"

#include <QApplication>
#include <QComboBox>
#include <QHBoxLayout>
#include <QLabel>
#include <QSlider>
#include <QSpinBox>
#include <QVBoxLayout>
#include <QWidget>

int main(int argc, char* argv[]) {
    QApplication app(argc, argv);

    QWidget window;
    window.setWindowTitle(QStringLiteral("空调控制面板(中介者模式)"));

    // 第一行:模式选择
    auto* modeLabel = new QLabel(QStringLiteral("模式:"));
    auto* modeBox = new QComboBox;
    modeBox->addItems({QStringLiteral("制冷"), QStringLiteral("制热"),
                       QStringLiteral("通风")});
    auto* modeRow = new QHBoxLayout;
    modeRow->addWidget(modeLabel);
    modeRow->addWidget(modeBox);
    modeRow->addStretch();

    // 第二行:温度滑块 + 数字输入框
    auto* slider = new QSlider(Qt::Horizontal);
    slider->setRange(16, 30);
    auto* spinBox = new QSpinBox;
    spinBox->setRange(16, 30);
    spinBox->setSuffix(QStringLiteral(" ℃"));
    auto* tempRow = new QHBoxLayout;
    tempRow->addWidget(slider, 1);
    tempRow->addWidget(spinBox);

    // 第三行:状态栏
    auto* statusLabel = new QLabel(QStringLiteral("就绪"));
    statusLabel->setAlignment(Qt::AlignCenter);

    auto* layout = new QVBoxLayout(&window);
    layout->addLayout(modeRow);
    layout->addLayout(tempRow);
    layout->addWidget(statusLabel);

    // ★ 关键:四个控件彼此不认识,所有联动逻辑交给中介者
    ControlMediator mediator;
    mediator.setControls(slider, spinBox, modeBox, statusLabel);

    window.show();
    return app.exec();
}

CMakeLists.txt

cmake_minimum_required(VERSION 3.16)
project(TemperaturePanel VERSION 1.0 LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_AUTOMOC ON)          # 自动处理 Q_OBJECT 元对象编译

find_package(Qt6 REQUIRED COMPONENTS Widgets)   # Qt5: find_package(Qt5 ...)

add_executable(temperature_panel
    main.cpp
    controlmediator.h
    controlmediator.cpp
)

target_link_libraries(temperature_panel PRIVATE Qt6::Widgets)  # Qt5: Qt5::Widgets

Qt 5 / Qt 6 说明:本示例代码在 Qt 5 与 Qt 6 下完全一致(QOverload<int>::of(...) 自 Qt 5.7 起可用,用于消除 valueChanged 这类重载信号的歧义);区别仅在 CMake 的 find_package 与 target 名称。注意 ControlMediator mediator; 是栈对象,其槽函数依赖的所有控件都归 window 父子管理,且窗口关闭后 app.exec() 返回、栈对象随后析构,生命周期安全。

⑦代码执行流程

以 Qt 示例为主,梳理运行时的完整协作流程:

  1. 初始化阶段:main() 创建四个控件并放入布局 → 创建 ControlMediator → 调用 setControls()。此时中介者把三条 connect 全部建立,并调用一次 onModeChanged(0),将界面初始化为"制冷模式 16~30℃",状态栏显示"模式已切换"。
  2. 用户拖动滑块:QSlider 数值变化 → 发射 valueChanged(int) → 信号被中介者的 onSliderChanged() 槽接收 → 槽内 spinBox_->setValue(value) 同步数字框,statusLabel_->setText(...) 更新状态栏。数字框因为被 setValue 赋值也会发射自己的 valueChanged,但 syncing_ 标志让 onSpinChanged() 直接返回,切断回环。
  3. 用户切换模式:QComboBox 发射 currentIndexChanged(int) → 中介者 onModeChanged() 按新模式重新 setRange 滑块和数字框、重置温度、更新状态栏。控件自身完全不知道"模式"这个概念。
  4. 用户直接在数字框输入:QSpinBox 发射 valueChanged → onSpinChanged() 反向同步滑块与状态栏(同样受 syncing_ 保护)。至此,任意一个控件的任何变化,都由中介者统一分发到其余控件。

纯 C++ 聊天室示例的流程同理:alice.send() → ChatRoom::broadcast() 遍历注册表 → 逐个回调 receive()。消息的"路由"完全由中介者决定,同事对象只是被动接收。

⑧为什么这样设计?

从架构角度剖析中介者模式的四个关键设计决策:

⑨不使用中介者会怎样?

回到第 ② 节的坏代码,看需求增长后的演进路径:

共同规律:对象间的交互需求越复杂、参与者越多,无中介者的代码维护成本就越接近平方级增长。而中介者把成本从"平方级分散"变为"线性级集中"——代价只是中介者本身会变复杂,但它是唯一需要复杂的地方。

⑩什么时候应该使用?

✅ 适合使用(3~5 个场景):

❌ 不适合使用(2~4 个场景):

⚠️ 过度设计提醒:中介者模式最大的坑是"中介者上帝化"——交互规则过多导致 ConcreteMediator 膨胀成几千行的万能类。缓解手段:把中介者的职责按子系统拆分(多个小中介者)、把规则本身再委托给策略对象、或改用事件总线(Event Bus)让中介者退化为"纯转发"。

⑪与其他模式的区别

选三个最容易混淆的模式对比:

对比维度中介者 Mediator观察者 Observer外观 Facade
目的集中管理对象之间的交互规则(双向、多对多)建立一对多通知关系(单向:主题 → 订阅者)为复杂子系统提供简化入口(单向:客户端 → 子系统)
消息流向同事 → 中介者 → 同事(可双向、可路由)主题 → 订阅者(单向广播)客户端 → 外观 → 子系统(单向委派)
参与者关系同事之间互相不认识,只认中介者订阅者可以互相不认识,只认主题客户端只认外观,子系统内部随便互相引用
Qt 中的对应把多个控件的 connect 集中到一个协调类QObject 信号槽、QAbstractItemModel 的 dataChanged 通知QNetworkAccessManager(封装 HTTP 细节)、QFileDialog::getOpenFileName 静态函数
一句话区分"所有人找同一个中间人办事""一件事通知所有关心它的人""给我一个简单的门面,别让我碰复杂的内部"

Mediator vs Observer:观察者解决"单向通知",中介者解决"双向/多向交互"。Qt 信号槽是观察者思想的实现——但如果两个控件要互相 connect 成环,就会形成网状;中介者则是把这些连接集中到一个地方管理。现实中两者常结合使用:中介者内部用信号槽与同事通信(本课 Qt 示例正是如此)。

Mediator vs Facade:外观是"简化入口",对象之间的通信在子系统内部照旧;中介者是"通信枢纽",参与者之间的通信必须经过它。外观对子系统内部不做任何约束,中介者则从根本上切断了同事间的直接依赖。另外它与 Day 10 学过的外观模式还有一个差异:外观是结构型模式(组织静态结构),中介者是行为型模式(组织运行时行为)。

Mediator vs Command(Day 14):命令把"请求"封装成对象以便撤销/排队;中介者封装的是"对象间的通信关系"。两者可以组合:命令对象由中介者转发执行(比如塔台向飞机下达"等待"指令)。

⑫Qt 源码 / Qt 框架中的体现

如实说明:Qt 中没有直接命名为 Mediator 的类,但中介者思想深度渗透在 Qt 的架构实践中:

给读者的启示:在 Qt 项目里,"主窗口类"或专门的 XxxCoordinator / XxxController 类经常天然承担中介者职责。当你发现主窗口的构造函数里 connect 越来越多、越来越难维护时,正是考虑"把连接和联动规则抽到一个独立中介者类"的信号。

⑬面试常见问题

Q1:中介者模式和观察者模式有什么区别?在 Qt 中如何选择?

A:观察者模式是单向的"一对多"通知(主题 → 订阅者),订阅者不回应主题;中介者模式是"多对多"的双向交互管理,参与者通过中介者互相通信,通信规则集中在中介者中。Qt 中,如果只是"某个状态变了通知一批控件刷新",用信号槽(观察者)足够;如果多个控件之间存在复杂的双向联动(A 变影响 B,B 变又影响 A,且规则随模式切换),就应该把连接收拢到一个中介者类中管理,避免网状 connect。

Q2:中介者模式的主要缺点是什么?如何避免"上帝中介者"?

A:缺点是中介者可能承担过多交互逻辑而膨胀,成为难以维护的上帝对象;同时引入了一层间接调用,牺牲少量性能。避免手段:①按子系统拆分成多个小中介者,每个只负责一组相关交互;②把复杂规则委托给独立的策略/规则对象,中介者只做编排;③若交互主要是"广播通知"而非"定向路由",改用事件总线,让中介者退化为纯转发器。

Q3:中介者模式如何保证对象生命周期安全?C++ 中同事与中介者谁拥有谁?

A:中介者通常不拥有同事对象(只登记引用),同事的生命周期由外部管理,这样避免循环引用和所有权混乱。若同事动态创建:方案一,同事持有 std::shared_ptr<Mediator>,中介者持有 std::vector<std::weak_ptr<Colleague>>,同事销毁后中介者自动跳过失效项;方案二,明确约定销毁顺序(先销毁所有同事再销毁中介者)。在 Qt 中,控件归 parent 父子管理,中介者若也挂在同一父对象下,则利用 Qt 的对象树统一析构,天然安全。

Q4:中介者模式和外观模式都"包装"了一堆对象,它们本质区别是什么?

A:方向与职责不同。外观(Facade)是给客户端一个简化入口,客户端 → 外观 → 子系统,子系统内部照常互相通信;中介者(Mediator)是切断参与者之间的直接通信,所有消息必须经中介者转发。外观侧重"简化使用",不改变对象间关系;中介者侧重"集中交互规则",从根本上重构了对象间的关系。实践中可以同时使用:外观对外,中介者对内。

Q5:在 Qt 中实现中介者时,如何处理信号回环(A 更新 B,B 又触发 A)?

A:常用三种手段:①syncing_ 布尔标志(本课示例采用),在同步期间让反向槽直接返回;②使用 QSignalBlocker 临时屏蔽某个控件的信号(QSignalBlocker blocker(spinBox););③blockSignals(true) / blockSignals(false) 手动控制。推荐优先用 QSignalBlocker(RAII 自动恢复),比手写标志更不易出错。注意:回环保护是中介者模式在信号槽环境下的必修课,面试中常作为考察细节的点。

⑭今日练习

练习:登录表单联动(Qt,建议 20~30 分钟)

用中介者模式实现一个登录对话框:

设计要求:LoginMediator : QObject 持有全部控件指针,所有 connect 与规则都在它里面;对话框只负责创建控件和布局。

💡 提示 1:先想清楚哪些信号需要收集——QLineEdit::textChanged 与 QPushButton::clicked;提示 2:规则③的"只读状态"可以用 setReadOnly(),注意按钮在只读期间也应禁用。

(想挑战纯 C++ 版?实现一个"拍卖行":拍卖师作中介者,多个买家通过拍卖师出价,拍卖师决定当前最高价并广播。主要锻炼所有权设计——思考中介者如何安全持有动态创建的买家。)

⑮今日总结

一句话记忆:对象之间不直接对话,有事找中介——网状依赖变星形依赖,交互规则集中一处。

看到以下代码特征时,可以考虑中介者模式:

明日预告:Day 17 我们将学习备忘录模式(Memento Pattern)——它把对象的状态快照封装起来,实现"存档/读档"。它与 Day 14 的命令模式是天生一对:Qt 的 QUndoCommand 撤销框架底层正是"命令 + 备忘录"思想的结合。学完明天,你就能从原理上理解 Qt 撤销/重做机制了。