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 就是发布者,PhoneApp 和 SmartWatch 是订阅者。每次温度变化,所有订阅者都会收到通知。注意我用了 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 信号,槽函数就会被自动调用。你想想看,这比手动遍历观察者列表要简洁得多吧?
现代C++的观察者模式:std::function + 回调
如果你不想依赖Qt,现代C++(C++11及以后)可以用 std::function 和 std::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架构图:观察者模式核心流程
什么时候用观察者模式?
我总结了几种典型场景:
- UI事件处理:按钮点击、鼠标移动、窗口大小变化等
- 数据同步:多个视图需要同时更新同一个数据模型
- 消息总线:模块间通信,避免直接依赖
- 游戏引擎:碰撞检测、玩家状态变化、成就系统触发
- 日志系统:多个日志消费者(文件、网络、控制台)监听同一日志源
好了,关于观察者模式就聊到这里。记住它的核心:发布者不关心谁在听,订阅者不关心谁在说。这种解耦思想在很多架构设计中都能看到影子,比如事件驱动架构、响应式编程等。理解透了这个模式,你写出来的代码会更有弹性。