中介者模式:当对象们需要“中间人”

说实话,我第一次接触中介者模式,是在一个多人协作的聊天室项目里。那时候我还在用最原始的方式——每个用户对象直接持有其他所有用户的引用。结果呢?代码耦合得像一团乱麻,加一个新用户,得改十几个地方。后来我重构了代码,引入了一个“聊天室中介者”,世界瞬间清净了。

中介者模式,说白了就是给一堆互相调用的对象找个“中间人”。所有对象只跟中介者通信,不直接互相调用。这样,对象之间的多对多关系,就简化成了对象与中介者的一对多关系。

为什么需要中介者?

你想想看,在一个复杂的系统中,如果每个对象都直接知道其他对象的存在,会发生什么?

  • 耦合爆炸:A要调用B,B要调用C,C又要调用A……改一个地方,牵一发动全身。
  • 复用困难:你想把某个对象拿到另一个项目里用,结果发现它依赖了一堆其他对象,根本拆不出来。
  • 调试噩梦:一个操作触发了一连串的调用链,你根本不知道问题出在哪一环。

中介者模式就是来解决这些问题的。它把对象间的交互逻辑集中到一个地方管理。每个对象只需要知道中介者,不需要知道其他对象的存在。

核心思想:用一个中介对象来封装一组对象的交互。中介者使各对象不需要显式地相互引用,从而使其耦合松散,并且可以独立地改变它们之间的交互。

STL 与中介者模式的结合

你可能会问:“STL 里又没有现成的中介者类,怎么结合?”

嗯,这里要注意。STL 虽然不直接提供中介者模式,但它提供了很多基础设施,让我们可以优雅地实现中介者。我个人习惯用 std::functionstd::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 结构图

中介者模式结构图 Mediator(中介者接口) DialogMediator(具体中介者) Widget(同事类接口) Slider(滑块) InputBox(输入框) CheckBox(复选框) 图例 中介者 → 同事(通知) 同事 → 中介者(报告事件) 继承关系 中介者相关 同事类相关

中介者模式的优缺点

任何模式都不是银弹。中介者模式有它的适用场景,也有它的代价。

优点

  • 减少了对象之间的耦合,每个对象只依赖中介者
  • 将多对多的交互简化为一对多的交互
  • 交互逻辑集中管理,便于维护和修改
  • 对象可以独立变化,只要不改变与中介者的接口

缺点

  • 中介者可能会变得非常复杂,成为“上帝对象”
  • 如果交互逻辑频繁变化,中介者需要不断修改
  • 性能上多了一层间接调用,但通常可以忽略

我曾经在一个项目中,把所有交互逻辑都塞进了一个中介者,结果那个类膨胀到了几千行。后来我不得不把它拆分成多个小中介者,每个负责一组相关的交互。记住:中介者不是越大越好,而是越清晰越好。

什么时候用中介者模式?

我个人总结了几条经验:

  1. 对象间存在复杂的网状引用:如果 A 知道 B,B 知道 C,C 又知道 A……赶紧用中介者。
  2. 交互逻辑经常变化:把变化的部分集中到中介者里,其他对象不用动。
  3. 想复用某个对象:如果对象只依赖中介者接口,换个中介者就能用在不同的场景里。
  4. UI 控件联动:这是中介者模式最经典的应用场景。

但也要注意,如果对象之间的交互很简单,比如只有两三个对象互相调用,那直接用引用反而更简洁。别为了用模式而用模式。

结合 STL 的更多玩法

除了上面用 std::vectorstd::shared_ptr 的实现,STL 还能帮我们做更多:

  • std::map 管理事件回调:把事件名称映射到回调函数列表,实现灵活的事件分发。
  • std::function 替代虚函数:有时候不需要继承,直接用 std::function 注册回调更灵活。
  • std::any 传递参数:C++17 的 std::any 可以让我们传递任意类型的参数,增强中介者的通用性。
  • std::shared_mutex 处理并发:如果中介者在多线程环境中使用,读写锁可以提升性能。

举个例子,用 std::mapstd::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 的代码补全和静态检查更友好。

好了,中介者模式就聊到这里。记住:模式是工具,不是目的。用对了地方,它能帮你写出更干净、更可维护的代码。

公众号:蓝海资料掘金营,微信deep3321