18、观察者模式:模式动机与定义、角色与结构、C++代码实现、发布-订阅模型
模式动机:为什么需要观察者?
我先问大家一个问题:你写代码的时候,有没有遇到过这种场景?
一个对象状态变了,然后你得手动去通知一堆其他对象。比如一个UI窗口,用户点了“保存”,你得更新状态栏、刷新列表、还要弹个提示。一开始你直接在按钮回调里挨个调用。后来需求多了,每次加一个新模块,你都得回去改那个回调函数。改着改着,你就发现这个函数越来越长,耦合越来越紧。
我几年前维护过一个老项目,里面有个核心数据类,每次数据一变,它要调用十几个不同的更新函数。新同事加功能,第一件事就是去那个类里加一行调用。有一次有人不小心删了一行,结果整个报表模块三天没更新数据,排查了一整天才找到原因。
说白了,这种“一对多”的依赖关系,如果硬编码在调用方,维护成本会指数级上升。观察者模式就是为了解决这个问题——让被观察者(Subject)和观察者(Observer)之间解耦。被观察者只管发通知,不关心谁在听。观察者只管收通知,不关心谁发的。
核心动机:定义一种一对多的依赖关系,当一个对象状态发生变化时,所有依赖于它的对象都得到通知并自动更新。
模式定义与角色结构
观察者模式的定义其实很直白:
观察者模式(Observer Pattern):定义对象间的一种一对多的依赖关系,使得每当一个对象状态发生改变时,其相关依赖对象皆得到通知并被自动更新。
这个模式里,主要有这么几个角色:
| 角色 | 名称 | 职责 |
|---|---|---|
| Subject(目标) | 被观察者 | 维护观察者列表,提供注册、注销、通知接口 |
| Observer(观察者) | 观察者接口 | 定义更新接口,收到通知后执行具体逻辑 |
| ConcreteSubject(具体目标) | 具体被观察者 | 存储状态,状态变化时通知所有观察者 |
| ConcreteObserver(具体观察者) | 具体观察者 | 实现更新接口,保持与目标状态一致 |
嗯,这里要注意:观察者模式的核心是“订阅-通知”机制。目标对象不知道观察者的具体类型,只知道它们实现了同一个接口。这就是依赖倒置原则的体现——抽象不应该依赖细节。
SVG 结构图:观察者模式核心逻辑
C++ 代码实现:从零搭建观察者框架
理论说完了,咱们直接上代码。我个人习惯先写抽象接口,再写具体实现。
先看观察者接口:
// observer.h
#pragma once
#include <string>
class Subject; // 前向声明
class Observer {
public:
virtual ~Observer() = default;
virtual void update(Subject* subject) = 0;
virtual std::string name() const = 0;
};
然后是目标基类:
// subject.h
#pragma once
#include <vector>
#include <algorithm>
#include <memory>
#include "observer.h"
class Subject {
public:
virtual ~Subject() = default;
void attach(Observer* obs) {
if (obs) {
observers_.push_back(obs);
}
}
void detach(Observer* obs) {
auto it = std::remove(observers_.begin(), observers_.end(), obs);
observers_.erase(it, observers_.end());
}
void notify() {
for (auto* obs : observers_) {
obs->update(this);
}
}
private:
std::vector<Observer*> observers_;
};
这里有个细节:我用了原始指针。为什么不用 shared_ptr?因为观察者的生命周期通常由外部管理,目标对象不负责销毁观察者。用原始指针更轻量,也符合“非拥有”的语义。
避坑指南:我曾经在一个项目里用 shared_ptr 管理观察者,结果不小心造成了循环引用——目标持有观察者的 shared_ptr,观察者又持有目标的 shared_ptr。最后内存泄漏了。后来我改成原始指针 + 确保观察者在析构前调用 detach,问题就解决了。
接下来是具体目标——一个天气数据类:
// weather_data.h
#pragma once
#include "subject.h"
class WeatherData : public Subject {
public:
void setMeasurements(float temp, float humidity, float pressure) {
temperature_ = temp;
humidity_ = humidity;
pressure_ = pressure;
measurementsChanged();
}
float getTemperature() const { return temperature_; }
float getHumidity() const { return humidity_; }
float getPressure() const { return pressure_; }
private:
void measurementsChanged() {
notify(); // 通知所有观察者
}
float temperature_ = 0.0f;
float humidity_ = 0.0f;
float pressure_ = 0.0f;
};
具体观察者——显示面板:
// display_panel.h
#pragma once
#include "observer.h"
#include "weather_data.h"
#include <iostream>
class CurrentConditionsDisplay : public Observer {
public:
explicit CurrentConditionsDisplay(const std::string& name)
: name_(name) {}
void update(Subject* subject) override {
// 向下转型获取具体数据
auto* wd = dynamic_cast<WeatherData*>(subject);
if (wd) {
temperature_ = wd->getTemperature();
humidity_ = wd->getHumidity();
display();
}
}
std::string name() const override { return name_; }
void display() const {
std::cout << "[" << name_ << "] "
<< "温度: " << temperature_
<< "°C, 湿度: " << humidity_
<< "%" << std::endl;
}
private:
std::string name_;
float temperature_ = 0.0f;
float humidity_ = 0.0f;
};
客户端使用示例:
// main.cpp
#include "weather_data.h"
#include "display_panel.h"
int main() {
WeatherData weatherStation;
CurrentConditionsDisplay display1("客厅面板");
CurrentConditionsDisplay display2("卧室面板");
weatherStation.attach(&display1);
weatherStation.attach(&display2);
weatherStation.setMeasurements(25.3f, 65.0f, 1013.0f);
// 输出:
// [客厅面板] 温度: 25.3°C, 湿度: 65%
// [卧室面板] 温度: 25.3°C, 湿度: 65%
weatherStation.detach(&display1);
weatherStation.setMeasurements(26.1f, 70.0f, 1012.0f);
// 只有卧室面板收到通知
return 0;
}
发布-订阅模型:观察者模式的进阶形态
你想想看,观察者模式有一个局限:目标对象和观察者之间还是存在直接依赖——观察者需要知道目标的具体类型才能转型获取数据。在大型系统里,这种依赖还是不够灵活。
发布-订阅模型(Pub-Sub)就是观察者模式的升级版。它引入了一个“事件通道”或“消息代理”作为中间层。发布者只管往通道里扔消息,订阅者只管从通道里收消息,双方完全不知道对方的存在。
我参与过一个实时交易系统,就是用发布-订阅模型来解耦各个模块。订单模块发布“订单成交”事件,风控模块、清算模块、通知模块各自订阅自己关心的事件。加一个新模块,只需要注册一个新订阅者,完全不用改现有代码。
下面是一个简单的 C++ 事件总线实现:
// event_bus.h
#pragma once
#include <functional>
#include <map>
#include <vector>
#include <string>
#include <any>
class EventBus {
public:
using Handler = std::function<void(const std::any&)>;
void subscribe(const std::string& eventType, Handler handler) {
handlers_[eventType].push_back(std::move(handler));
}
void publish(const std::string& eventType, const std::any& data) {
auto it = handlers_.find(eventType);
if (it != handlers_.end()) {
for (const auto& handler : it->second) {
handler(data);
}
}
}
private:
std::map<std::string, std::vector<Handler>> handlers_;
};
使用方式:
EventBus bus;
// 订阅者A
bus.subscribe("temperature_change", [](const std::any& data) {
float temp = std::any_cast<float>(data);
std::cout << "订阅者A: 温度变为 " << temp << std::endl;
});
// 订阅者B
bus.subscribe("temperature_change", [](const std::any& data) {
float temp = std::any_cast<float>(data);
std::cout << "订阅者B: 记录温度 " << temp << std::endl;
});
// 发布者
bus.publish("temperature_change", 26.5f);
注意:发布-订阅模型虽然解耦更彻底,但调试起来也更困难。因为事件流是隐式的,你很难一眼看出某个事件被哪些订阅者处理了。我建议在关键事件上加上日志,或者用唯一ID追踪事件链路。
总结一下
观察者模式的核心就是“通知-更新”。它让目标对象和观察者之间从硬编码变成抽象依赖。发布-订阅模型则更进一步,通过中间层实现了完全解耦。
在实际项目中,我通常这样选择:
- 如果观察者和目标在同一个进程内,且数量不多,直接用观察者模式,代码更简单直接。
- 如果系统模块较多,或者需要跨模块、跨线程通信,我会选择发布-订阅模型,配合事件总线。
- 如果涉及分布式系统,那就得上消息队列(如 RabbitMQ、Kafka)了,那是另一个话题。
嗯,观察者模式就讲到这里。记住一点:解耦是手段,不是目的。别为了用模式而用模式,合适才是最好的。