21. 观察者模式(Observer):发布-订阅模型,C++中的事件系统,信号与槽机制

观察者模式,说白了就是「我不主动找你,但你变了得通知我」。这种模式在C++里特别常见,尤其是做UI框架、游戏引擎或者中间件的时候。我个人习惯把它叫做「发布-订阅模型」,因为它本质上就是有人负责发消息,有人负责收消息,中间通过一个调度中心来解耦。

我记得有一次接手一个老项目,里面的事件处理全是硬编码的if-else链,每次加一个新功能都要改好几个文件。后来我重构的时候,第一件事就是引入观察者模式。嗯,效果立竿见影——代码量少了,扩展性也上来了。

核心思想:一对多的依赖关系

观察者模式的定义其实很简单:定义一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。当主题对象的状态发生变化时,它会通知所有观察者,让它们自动更新。

你想想看,这就像你订阅了一个公众号。公众号(主题)发文章,你(观察者)就能收到推送。你不用天天去刷公众号,公众号也不会单独给你发消息——中间有个平台在协调。

关键角色:
  • Subject(主题):维护观察者列表,提供注册、注销、通知接口
  • Observer(观察者):定义更新接口,收到通知后执行具体逻辑
  • ConcreteSubject(具体主题):状态变化时触发通知
  • ConcreteObserver(具体观察者):实现更新接口,处理业务

经典实现:C++代码示例

先看一个最基础的实现。我尽量写得简洁,方便你理解核心逻辑。

#include <iostream>
#include <vector>
#include <memory>
#include <algorithm>

// 观察者基类
class Observer {
public:
    virtual ~Observer() = default;
    virtual void update(const std::string& message) = 0;
};

// 主题基类
class Subject {
public:
    virtual ~Subject() = default;
    virtual void attach(std::shared_ptr<Observer> obs) = 0;
    virtual void detach(std::shared_ptr<Observer> obs) = 0;
    virtual void notify(const std::string& message) = 0;
};

// 具体主题:天气站
class WeatherStation : public Subject {
private:
    std::vector<std::shared_ptr<Observer>> observers_;
    float temperature_;
public:
    void attach(std::shared_ptr<Observer> obs) override {
        observers_.push_back(obs);
    }

    void detach(std::shared_ptr<Observer> obs) override {
        auto it = std::remove(observers_.begin(), observers_.end(), obs);
        observers_.erase(it, observers_.end());
    }

    void notify(const std::string& message) override {
        for (auto& obs : observers_) {
            obs->update(message);
        }
    }

    void setTemperature(float temp) {
        temperature_ = temp;
        std::string msg = "当前温度: " + std::to_string(temperature_) + "°C";
        notify(msg);
    }
};

// 具体观察者:手机App
class PhoneApp : public Observer {
private:
    std::string name_;
public:
    PhoneApp(const std::string& name) : name_(name) {}
    void update(const std::string& message) override {
        std::cout << "[" << name_ << "] 收到天气更新: " << message << std::endl;
    }
};

// 具体观察者:智能手表
class SmartWatch : public Observer {
public:
    void update(const std::string& message) override {
        std::cout << "[智能手表] 震动提醒: " << message << std::endl;
    }
};

int main() {
    auto station = std::make_shared<WeatherStation>();
    auto app1 = std::make_shared<PhoneApp>("iPhone");
    auto app2 = std::make_shared<PhoneApp>("Android");
    auto watch = std::make_shared<SmartWatch>();

    station->attach(app1);
    station->attach(app2);
    station->attach(watch);

    station->setTemperature(25.5f);
    // 输出:
    // [iPhone] 收到天气更新: 当前温度: 25.500000°C
    // [Android] 收到天气更新: 当前温度: 25.500000°C
    // [智能手表] 震动提醒: 当前温度: 25.500000°C

    station->detach(app2);
    station->setTemperature(18.0f);
    // app2不再收到通知

    return 0;
}

这段代码里,WeatherStation 就是发布者,PhoneAppSmartWatch 是订阅者。每次温度变化,所有订阅者都会收到通知。注意我用了 std::shared_ptr 管理观察者生命周期,避免悬空指针。

我的经验: 在实际项目中,观察者列表的线程安全问题经常被忽略。如果你在多线程环境下使用,记得加锁或者用无锁队列。我曾经在一个网络库中因为没加锁,导致通知时迭代器失效,排查了两天才找到原因。

信号与槽机制:Qt的观察者模式

说到C++的事件系统,就绕不开Qt的信号与槽(Signals & Slots)。这是Qt框架对观察者模式的一种优雅实现。它比传统观察者模式多了几个好处:

  • 类型安全:编译期检查信号和槽的参数类型是否匹配
  • 松耦合:信号发送者不需要知道接收者的任何信息
  • 自动断开:对象销毁时自动断开连接,避免野指针

看一个简单的Qt例子:

#include <QObject>
#include <QDebug>

class Counter : public QObject {
    Q_OBJECT
public:
    Counter() : value_(0) {}

    void increment() {
        value_++;
        emit valueChanged(value_);
    }

signals:
    void valueChanged(int newValue);

private:
    int value_;
};

class Display : public QObject {
    Q_OBJECT
public slots:
    void showValue(int val) {
        qDebug() << "当前值:" << val;
    }
};

// 使用
Counter counter;
Display display;
QObject::connect(&counter, &Counter::valueChanged,
                 &display, &Display::showValue);

counter.increment(); // 输出: 当前值: 1
counter.increment(); // 输出: 当前值: 2

这里 valueChanged 是信号,showValue 是槽。通过 connect 连接后,每次 emit 信号,槽函数就会被自动调用。你想想看,这比手动遍历观察者列表要简洁得多吧?

注意: 信号与槽的性能开销比普通函数调用大一些。Qt内部通过元对象系统(MOC)实现,每次信号发射会进行参数拷贝和类型检查。如果你在性能敏感的热路径上频繁发射信号,建议考虑直接函数调用或者自定义事件分发。

现代C++的观察者模式:std::function + 回调

如果你不想依赖Qt,现代C++(C++11及以后)可以用 std::functionstd::bind 实现轻量级的观察者模式。我个人比较喜欢这种方式,因为它不需要继承任何基类,灵活性更高。

#include <iostream>
#include <functional>
#include <vector>
#include <memory>

class EventBus {
public:
    using Handler = std::function<void(const std::string&)>;

    void subscribe(Handler handler) {
        handlers_.push_back(std::move(handler));
    }

    void publish(const std::string& event) {
        for (auto& handler : handlers_) {
            handler(event);
        }
    }

private:
    std::vector<Handler> handlers_;
};

int main() {
    EventBus bus;

    bus.subscribe([](const std::string& msg) {
        std::cout << "模块A收到: " << msg << std::endl;
    });

    bus.subscribe([](const std::string& msg) {
        std::cout << "模块B收到: " << msg << std::endl;
    });

    bus.publish("系统启动完成");
    // 输出:
    // 模块A收到: 系统启动完成
    // 模块B收到: 系统启动完成

    return 0;
}

这种实现方式非常轻量,适合中小型项目。但要注意,std::function 的拷贝开销不小,如果订阅者很多,可以考虑用 std::shared_ptr 包装回调。

观察者模式的优缺点

优点 缺点
解耦发布者和订阅者,双方互不依赖 通知顺序不可控,可能导致意外行为
支持广播通信,一对多通知 如果观察者过多,通知性能会下降
符合开闭原则,新增观察者无需修改主题 观察者之间可能产生循环依赖,导致死循环
实现简单,易于理解 内存管理需要小心,避免悬空指针
避坑指南: 我曾经在一个游戏项目中,用观察者模式处理玩家死亡事件。结果玩家死亡后触发了多个观察者,其中一个观察者又去修改了玩家状态,导致再次触发死亡事件——死循环了。解决方案是在通知期间禁止注册/注销操作,或者用事件队列异步处理。

SVG架构图:观察者模式核心流程

观察者模式核心流程 Subject(主题) 维护观察者列表 Observer A update() Observer B update() Observer C update() notify() notify() notify() attach() / detach() 当主题状态变化时,遍历观察者列表,依次调用 update() 观察者无需知道彼此存在,实现完全解耦

什么时候用观察者模式?

我总结了几种典型场景:

  • UI事件处理:按钮点击、鼠标移动、窗口大小变化等
  • 数据同步:多个视图需要同时更新同一个数据模型
  • 消息总线:模块间通信,避免直接依赖
  • 游戏引擎:碰撞检测、玩家状态变化、成就系统触发
  • 日志系统:多个日志消费者(文件、网络、控制台)监听同一日志源
我的建议: 不要滥用观察者模式。如果只有一两个观察者,直接用回调函数或者接口调用更简单。观察者模式适合「不知道谁会来订阅,也不知道有多少人来订阅」的场景。过度设计反而会让代码变得难以追踪。

好了,关于观察者模式就聊到这里。记住它的核心:发布者不关心谁在听,订阅者不关心谁在说。这种解耦思想在很多架构设计中都能看到影子,比如事件驱动架构、响应式编程等。理解透了这个模式,你写出来的代码会更有弹性。


专注资料整理