22. 观察者模式:模式动机与定义、角色与结构、C++代码实现、事件驱动编程
观察者模式,说白了就是「发布-订阅」机制。一个对象状态变了,其他一堆对象能自动收到通知。我早年做的一个监控系统,服务器挂了,客户端得立刻知道——那时候用的就是观察者模式。今天咱们把它掰开揉碎讲清楚。
22.1 模式动机与定义
先想想一个场景:你写了个股票行情软件,股价一变,所有展示面板(K线图、数字报价、新闻推送)都得刷新。如果每个面板都去轮询股价,CPU 得炸。更好的做法是:股价中心一变化,主动通知所有面板。
这就是观察者模式的核心动机——解耦。被观察者(Subject)不需要知道观察者(Observer)是谁,只需要知道它们实现了同一个接口。观察者可以随时加入或退出,不影响系统其他部分。
定义:观察者模式定义了对象之间的一对多依赖关系,当一个对象状态发生变化时,所有依赖它的对象都会得到通知并自动更新。
我个人习惯把观察者模式理解为「报社与订户」的关系。报社只管印报纸,订户只管收报纸。订户多了少了,报社不用管。你想想看,这种松耦合的设计,在大型系统中有多重要。
22.2 角色与结构
观察者模式涉及四个角色,我画了张图帮你理清关系:
角色说明:
- Subject(抽象主题):提供 attach、detach、notify 三个核心方法。它维护一个观察者列表。
- ConcreteSubject(具体主题):持有真正的状态数据。状态变了就调 notify()。
- Observer(抽象观察者):定义 update 接口,所有观察者必须实现它。
- ConcreteObserver(具体观察者):实现 update,收到通知后拉取数据或做自己的事。
我的经验:实际项目中,Subject 的 notify() 调用时机很关键。我见过有人把 notify 放在 setter 里,结果构造函数里调 setter 时,观察者还没注册完,直接崩了。建议:状态批量修改完,手动调一次 notify,别在 setter 里自动触发。
22.3 C++代码实现
直接上代码。我写了一个股票报价的例子,你一看就懂。
#include <iostream>
#include <vector>
#include <algorithm>
#include <memory>
// 前向声明
class Subject;
// 抽象观察者
class Observer {
public:
virtual ~Observer() = default;
virtual void update(Subject* subject) = 0;
};
// 抽象主题
class Subject {
public:
virtual ~Subject() = default;
void attach(Observer* obs) {
observers_.push_back(obs);
}
void detach(Observer* obs) {
auto it = std::find(observers_.begin(), observers_.end(), obs);
if (it != observers_.end()) {
observers_.erase(it);
}
}
void notify() {
for (auto* obs : observers_) {
obs->update(this);
}
}
private:
std::vector<Observer*> observers_;
};
// 具体主题:股票价格
class StockPrice : public Subject {
public:
void setPrice(double price) {
price_ = price;
notify(); // 价格变了,通知所有人
}
double getPrice() const { return price_; }
private:
double price_ = 0.0;
};
// 具体观察者:散户
class RetailInvestor : public Observer {
public:
explicit RetailInvestor(const std::string& name) : name_(name) {}
void update(Subject* subject) override {
auto* stock = dynamic_cast<StockPrice*>(subject);
if (stock) {
std::cout << "[" << name_ << "] 收到通知,当前股价: "
<< stock->getPrice() << std::endl;
}
}
private:
std::string name_;
};
// 具体观察者:量化交易系统
class QuantSystem : public Observer {
public:
void update(Subject* subject) override {
auto* stock = dynamic_cast<StockPrice*>(subject);
if (stock) {
double price = stock->getPrice();
if (price < 100.0) {
std::cout << "[量化系统] 价格低于100,触发买入策略!" << std::endl;
}
}
}
};
int main() {
StockPrice apple;
RetailInvestor alice("Alice");
RetailInvestor bob("Bob");
QuantSystem quant;
apple.attach(&alice);
apple.attach(&bob);
apple.attach(&quant);
apple.setPrice(150.0);
apple.setPrice(95.0); // 量化系统会触发买入
apple.detach(&bob);
apple.setPrice(120.0); // Bob 不再收到通知
return 0;
}
运行结果:
[Alice] 收到通知,当前股价: 150
[Bob] 收到通知,当前股价: 150
[量化系统] 价格低于100,触发买入策略!
[Alice] 收到通知,当前股价: 120
[量化系统] 收到通知,当前股价: 120
注意:上面的代码用了原始指针管理观察者。我曾经在一个项目中因为观察者被提前销毁但没 detach,导致野指针崩溃。建议用 std::shared_ptr 或确保析构时自动 detach。另外,notify 里遍历观察者时,如果某个观察者的 update 又调了 attach/detach,会迭代器失效。可以用「先拷贝列表再遍历」的方式规避。
22.4 事件驱动编程
观察者模式是事件驱动编程的基石。你想想看,GUI 里的按钮点击、网络库里的连接建立、游戏里的碰撞检测——本质上都是观察者模式。
事件驱动编程的核心思想:不主动轮询,而是被动响应。系统维护一个事件循环,事件来了,分发给对应的处理器。观察者模式正好提供了这种「注册-通知」的机制。
我参与过一个实时交易系统,每秒处理上万笔订单。如果用轮询,CPU 全浪费在空转上。改用事件驱动后,每个订单处理完就触发「订单状态变更事件」,风控模块、清算模块、报表模块各自注册监听,效率提升了一个数量级。
事件驱动编程的几个关键点:
- 事件源:谁产生事件?比如用户点击、网络数据到达。
- 事件对象:携带什么数据?比如鼠标坐标、键盘按键码。
- 事件处理器:谁处理事件?就是观察者。
- 事件循环:谁分发事件?通常是框架或底层库。
实战建议:在 C++ 中实现事件驱动,可以用 std::function 替代纯虚接口。这样更灵活,观察者不需要继承某个基类。比如:
class EventEmitter {
public:
using Handler = std::function<void(const Event&)>;
void on(const std::string& eventName, Handler handler) {
handlers_[eventName].push_back(std::move(handler));
}
void emit(const std::string& eventName, const Event& event) {
auto it = handlers_.find(eventName);
if (it != handlers_.end()) {
for (auto& handler : it->second) {
handler(event);
}
}
}
private:
std::unordered_map<std::string, std::vector<Handler>> handlers_;
};
这种写法去掉了 Observer 基类,用 std::function 做回调。我更喜欢这种方式,因为它更轻量,而且 lambda 表达式用起来很顺手。
避坑指南:事件驱动编程容易踩的坑是「回调地狱」——事件里又触发事件,形成循环。我曾经调试过一个死循环,找了半天发现是 A 事件处理里发了 B 事件,B 事件处理里又发了 A 事件。解决方案:加递归深度限制,或者用异步队列延迟处理。
总结一下:观察者模式是解耦的利器,事件驱动编程是它的高级形态。从 GUI 到后端服务,从游戏到金融系统,这套思想无处不在。你写代码时多想想「谁依赖谁」,自然就能用好它。
公众号:蓝海资料掘金营,微信deep3321