中介者模式:当对象们需要“中间人”
说实话,我第一次接触中介者模式,是在一个多人协作的聊天室项目里。那时候我还在用最原始的方式——每个用户对象直接持有其他所有用户的引用。结果呢?代码耦合得像一团乱麻,加一个新用户,得改十几个地方。后来我重构了代码,引入了一个“聊天室中介者”,世界瞬间清净了。
中介者模式,说白了就是给一堆互相调用的对象找个“中间人”。所有对象只跟中介者通信,不直接互相调用。这样,对象之间的多对多关系,就简化成了对象与中介者的一对多关系。
为什么需要中介者?
你想想看,在一个复杂的系统中,如果每个对象都直接知道其他对象的存在,会发生什么?
- 耦合爆炸:A要调用B,B要调用C,C又要调用A……改一个地方,牵一发动全身。
- 复用困难:你想把某个对象拿到另一个项目里用,结果发现它依赖了一堆其他对象,根本拆不出来。
- 调试噩梦:一个操作触发了一连串的调用链,你根本不知道问题出在哪一环。
中介者模式就是来解决这些问题的。它把对象间的交互逻辑集中到一个地方管理。每个对象只需要知道中介者,不需要知道其他对象的存在。
核心思想:用一个中介对象来封装一组对象的交互。中介者使各对象不需要显式地相互引用,从而使其耦合松散,并且可以独立地改变它们之间的交互。
STL 与中介者模式的结合
你可能会问:“STL 里又没有现成的中介者类,怎么结合?”
嗯,这里要注意。STL 虽然不直接提供中介者模式,但它提供了很多基础设施,让我们可以优雅地实现中介者。我个人习惯用 std::function 和 std::vector 来构建事件回调机制,用 std::shared_ptr 管理对象生命周期,用 std::mutex 保证线程安全。
说白了,STL 的容器、智能指针、函数对象,就是实现中介者模式的“砖瓦”。
一个实际例子:UI 控件协调
我在项目中遇到过这样一个场景:一个对话框里有多个控件——输入框、滑块、复选框、进度条。它们之间需要联动:
- 滑块拖动时,输入框的数字要同步更新
- 输入框输入数字时,滑块位置要跟着变
- 复选框勾选时,滑块和输入框要变成禁用状态
如果让每个控件直接互相引用,代码会变成什么样?我都不敢想。用中介者模式就清爽多了。
下面是我用 C++ 和 STL 实现的一个简化版中介者模式:
#include <iostream>
#include <vector>
#include <functional>
#include <memory>
#include <string>
// 中介者接口
class Mediator {
public:
virtual ~Mediator() = default;
virtual void notify(const std::string& sender,
const std::string& event) = 0;
};
// 控件基类
class Widget {
protected:
std::shared_ptr<Mediator> mediator_;
std::string name_;
public:
Widget(const std::string& name,
std::shared_ptr<Mediator> med)
: name_(name), mediator_(med) {}
virtual ~Widget() = default;
virtual void update(const std::string& event) = 0;
std::string getName() const { return name_; }
};
// 具体控件:滑块
class Slider : public Widget {
private:
int value_ = 50;
public:
Slider(const std::string& name,
std::shared_ptr<Mediator> med)
: Widget(name, med) {}
void setValue(int v) {
value_ = v;
std::cout << "滑块[" << name_
<< "] 值变为: " << value_ << "\n";
mediator_->notify(name_, "value_changed");
}
void update(const std::string& event) override {
if (event == "disable") {
std::cout << "滑块[" << name_
<< "] 被禁用\n";
} else if (event == "enable") {
std::cout << "滑块[" << name_
<< "] 被启用\n";
}
}
};
// 具体控件:输入框
class InputBox : public Widget {
private:
int value_ = 50;
public:
InputBox(const std::string& name,
std::shared_ptr<Mediator> med)
: Widget(name, med) {}
void setValue(int v) {
value_ = v;
std::cout << "输入框[" << name_
<< "] 值变为: " << value_ << "\n";
mediator_->notify(name_, "value_changed");
}
void update(const std::string& event) override {
if (event == "disable") {
std::cout << "输入框[" << name_
<< "] 被禁用\n";
} else if (event == "enable") {
std::cout << "输入框[" << name_
<< "] 被启用\n";
}
}
};
// 具体控件:复选框
class CheckBox : public Widget {
private:
bool checked_ = false;
public:
CheckBox(const std::string& name,
std::shared_ptr<Mediator> med)
: Widget(name, med) {}
void toggle() {
checked_ = !checked_;
std::cout << "复选框[" << name_
<< "] 状态: " << (checked_ ? "选中" : "未选中")
<< "\n";
if (checked_) {
mediator_->notify(name_, "checked");
} else {
mediator_->notify(name_, "unchecked");
}
}
void update(const std::string& event) override {
// 复选框不需要响应其他控件的事件
}
};
// 具体中介者:对话框
class DialogMediator : public Mediator {
private:
std::vector<std::shared_ptr<Widget>> widgets_;
public:
void addWidget(std::shared_ptr<Widget> w) {
widgets_.push_back(w);
}
void notify(const std::string& sender,
const std::string& event) override {
// 根据发送者和事件类型,协调其他控件
if (event == "value_changed") {
// 滑块或输入框的值变了,同步另一个
for (auto& w : widgets_) {
if (w->getName() != sender) {
// 这里简化处理,实际应该同步值
std::cout << "中介者通知 ["
<< w->getName()
<< "] 同步值\n";
}
}
} else if (event == "checked") {
// 复选框选中,禁用滑块和输入框
for (auto& w : widgets_) {
if (w->getName() != sender) {
w->update("disable");
}
}
} else if (event == "unchecked") {
// 复选框取消选中,启用滑块和输入框
for (auto& w : widgets_) {
if (w->getName() != sender) {
w->update("enable");
}
}
}
}
};
// 使用示例
int main() {
auto mediator = std::make_shared<DialogMediator>();
auto slider = std::make_shared<Slider>("音量滑块", mediator);
auto input = std::make_shared<InputBox>("音量输入", mediator);
auto checkbox = std::make_shared<CheckBox>("静音开关", mediator);
mediator->addWidget(slider);
mediator->addWidget(input);
mediator->addWidget(checkbox);
std::cout << "=== 用户拖动滑块 ===\n";
slider->setValue(75);
std::cout << "\n=== 用户勾选静音 ===\n";
checkbox->toggle();
std::cout << "\n=== 用户取消静音 ===\n";
checkbox->toggle();
return 0;
}
中介者模式的核心结构
上面的代码展示了几个关键角色:
| 角色 | 说明 | STL 工具 |
|---|---|---|
| Mediator(中介者接口) | 定义通信接口,所有控件通过它交互 | 纯虚类 |
| ConcreteMediator(具体中介者) | 实现协调逻辑,持有所有控件的引用 | std::vector<std::shared_ptr<Widget>> |
| Colleague(同事类接口) | 定义控件的基本行为 | 纯虚类 |
| ConcreteColleague(具体同事类) | 实现具体控件,只与中介者通信 | std::shared_ptr<Mediator> |
我的经验:用 std::shared_ptr 管理中介者和控件的生命周期时,要小心循环引用。如果控件持有中介者的 shared_ptr,中介者也持有控件的 shared_ptr,就会形成循环。我一般让控件持有中介者的 shared_ptr,中介者持有控件的 weak_ptr,或者让中介者用普通指针。
中介者模式的 SVG 结构图
中介者模式的优缺点
任何模式都不是银弹。中介者模式有它的适用场景,也有它的代价。
优点:
- 减少了对象之间的耦合,每个对象只依赖中介者
- 将多对多的交互简化为一对多的交互
- 交互逻辑集中管理,便于维护和修改
- 对象可以独立变化,只要不改变与中介者的接口
缺点:
- 中介者可能会变得非常复杂,成为“上帝对象”
- 如果交互逻辑频繁变化,中介者需要不断修改
- 性能上多了一层间接调用,但通常可以忽略
我曾经在一个项目中,把所有交互逻辑都塞进了一个中介者,结果那个类膨胀到了几千行。后来我不得不把它拆分成多个小中介者,每个负责一组相关的交互。记住:中介者不是越大越好,而是越清晰越好。
什么时候用中介者模式?
我个人总结了几条经验:
- 对象间存在复杂的网状引用:如果 A 知道 B,B 知道 C,C 又知道 A……赶紧用中介者。
- 交互逻辑经常变化:把变化的部分集中到中介者里,其他对象不用动。
- 想复用某个对象:如果对象只依赖中介者接口,换个中介者就能用在不同的场景里。
- UI 控件联动:这是中介者模式最经典的应用场景。
但也要注意,如果对象之间的交互很简单,比如只有两三个对象互相调用,那直接用引用反而更简洁。别为了用模式而用模式。
结合 STL 的更多玩法
除了上面用 std::vector 和 std::shared_ptr 的实现,STL 还能帮我们做更多:
- 用
std::map管理事件回调:把事件名称映射到回调函数列表,实现灵活的事件分发。 - 用
std::function替代虚函数:有时候不需要继承,直接用std::function注册回调更灵活。 - 用
std::any传递参数:C++17 的std::any可以让我们传递任意类型的参数,增强中介者的通用性。 - 用
std::shared_mutex处理并发:如果中介者在多线程环境中使用,读写锁可以提升性能。
举个例子,用 std::map 和 std::function 实现一个更灵活的事件系统:
#include <iostream>
#include <map>
#include <vector>
#include <functional>
#include <string>
class EventMediator {
private:
// 事件名 -> 回调列表
std::map<std::string,
std::vector<std::function<void(const std::string&)>>> listeners_;
public:
// 订阅事件
void on(const std::string& event,
std::function<void(const std::string&)> callback) {
listeners_[event].push_back(callback);
}
// 触发事件
void emit(const std::string& event,
const std::string& data = "") {
auto it = listeners_.find(event);
if (it != listeners_.end()) {
for (auto& cb : it->second) {
cb(data);
}
}
}
};
// 使用
int main() {
EventMediator mediator;
// 滑块订阅"值改变"事件
mediator.on("value_changed", [](const std::string& data) {
std::cout << "滑块收到通知,新值: " << data << "\n";
});
// 输入框也订阅同一个事件
mediator.on("value_changed", [](const std::string& data) {
std::cout << "输入框收到通知,新值: " << data << "\n";
});
// 触发事件
mediator.emit("value_changed", "75");
return 0;
}
这种方式比继承更灵活,但缺点是类型安全不如继承。你需要在回调里自己处理类型转换。我个人在小型项目里喜欢用这种方式,大型项目还是倾向于用继承,因为 IDE 的代码补全和静态检查更友好。
好了,中介者模式就聊到这里。记住:模式是工具,不是目的。用对了地方,它能帮你写出更干净、更可维护的代码。