观察者模式的定义
观察者模式(Observer)的 GoF 定义是:定义对象间的一种一对多的依赖关系,使得每当一个对象改变状态时,所有依赖于它的对象都会得到通知并被自动更新。
这里的核心前提是”通知方向是单向的”。被观察的对象(主题)不需要知道谁在观察自己、观察者具体要做什么。它只管在自己状态改变时喊一声,谁在乎谁就做出反应。
比如一个气象站,它持续采集温度、湿度、气压。数据显示屏、天气预测面板、预警系统都依赖同一份数据。气象站不可能把每个显示设备的更新逻辑都硬编码进去,它也不应该关心每个终端拿到数据后怎么用。
观察者模式的思路是把”数据源”和”数据消费者”拆开:主题只管维护一个观察者列表,状态变了就遍历通知;观察者只管在收到通知后读取数据、执行自己的处理逻辑。两者之间通过一个抽象接口解耦,新增观察者时不需要修改主题(被订阅者)的任何代码。
观察者模式用于解决什么问题
如果不用观察者,主题(被订阅者)会直接调用每个观察者的具体方法。
比如上文提到的气象站的系统,WeatherStation 在采集数据后需要更新三个设备:温度显示屏、预测面板、预警系统。
最朴素的写法是在 WeatherStation 里硬编码三个更新调用:
class WeatherStation {
public:
void measurementsChanged(float t, float h, float p) {
temperatureDisplay->update(t); // 温度屏
forecastPanel->update(t, h, p); // 预测面板
alertSystem->check(t, h, p); // 预警系统
}
};
每加一个设备就得往 WeatherStation 里追加一行调用,删一个设备也得改 WeatherStation。设备和数据源之间是紧耦合。
更严重的问题是,每个设备的接口可能不一样:update(t)、update(t,h,p)、check(t,h,p),主题必须知道每个观察者的具体接口,加一个新设备时如果接口不同,连调用方式都写不进去。
观察者模式则把通知方案标准化。所有观察者实现同一个 update() 接口,主题不再关心具体设备名和参数个数,只对着一组抽象 Observer 逐个调用 update()。新增的设备只要实现 Observer 接口并注册自己,而主题零改动。
两种实现方式的对比如下。
| 观察者数量 | 朴素写法 | 观察者模式 |
|---|---|---|
| 3 | 3 个硬编码调用 + 3 种不同接口 | Observer 接口 + attach/detach/notify |
| 10 | 10 个硬编码调用,接口混乱 | 循环 notify,接口统一 |
| N(动态增减) | 无法实现 | 运行时 attach/detach |
观察者数量越多、变化越频繁,观察者模式的价值越大。
观察者模式 UML 类图
观察者模式涉及四个角色:Subject(主题接口)、ConcreteSubject(具体主题,持有数据)、Observer(观察者接口)、ConcreteObserver(具体观察者)。
核心机制是 Subject 持有 Observer 列表,状态变化时遍历通知,观察者在收到通知后拉取数据。
flowchart BT
subgraph SubjectSide [主题侧]
Subject["Subject<br/>---<br/><<interface>> Subject<br/>---<br/>+attach(Observer) : void<br/>+detach(Observer) : void<br/>+notify() : void"]
WeatherData["WeatherData<br/>---<br/>ConcreteSubject<br/>-temperature : float<br/>-humidity : float<br/>-pressure : float<br/>---<br/>+getTemperature() : float<br/>+getHumidity() : float<br/>+getPressure() : float<br/>+measurementsChanged() : void"]
end
subgraph ObserverSide [观察者侧]
Observer["Observer<br/>---<br/><<interface>> Observer<br/>---<br/>+update() : void"]
CurrentDisplay["CurrentDisplay<br/>---<br/>ConcreteObserver<br/>-subject : Subject*<br/>---<br/>+update() : void<br/>+display() : void"]
ForecastPanel["ForecastPanel<br/>---<br/>ConcreteObserver<br/>-subject : Subject*<br/>---<br/>+update() : void<br/>+display() : void"]
AlertSystem["AlertSystem<br/>---<br/>ConcreteObserver<br/>-subject : Subject*<br/>---<br/>+update() : void<br/>+check() : void"]
end
WeatherData -- 继承 --> Subject
CurrentDisplay -- 继承 --> Observer
ForecastPanel -- 继承 --> Observer
AlertSystem -- 继承 --> Observer
Subject -. 持有Observer* .-> Observer
WeatherData -- 关联 --> CurrentDisplay
WeatherData -- 关联 --> ForecastPanel
WeatherData -- 关联 --> AlertSystem
style Subject fill:#fef3c7,stroke:#92400e,stroke-width:2px
style Observer fill:#dbeafe,stroke:#1e40af,stroke-width:2px
style WeatherData fill:#fef3c7,stroke:#92400e
style CurrentDisplay fill:#dbeafe,stroke:#1e40af
style ForecastPanel fill:#dbeafe,stroke:#1e40af
style AlertSystem fill:#dbeafe,stroke:#1e40af
UML 对象交互图:工作流程
以”气象站数据更新→三个设备收到通知”为例,看一次通知是怎么传播的。
sequenceDiagram
participant W as WeatherData(ConcreteSubject)
participant C as CurrentDisplay(Observer)
participant F as ForecastPanel(Observer)
participant A as AlertSystem(Observer)
W->>W: measurementsChanged()<br/>数据变化,触发通知
W->>W: notify()
Note over W: 遍历所有已注册的 Observer
W->>C: update()
C->>W: getTemperature()
Note over C: 拉取自己关心的数据<br/>更新显示
W->>F: update()
F->>W: getTemperature() / getPressure()
Note over F: 拉取自己关心的数据<br/>执行预测逻辑
W->>A: update()
A->>W: getTemperature() / getHumidity()
Note over A: 拉取自己关心的数据<br/>评估是否触发预警
关键动作是:主题数据变化后触发 notify,遍历所有观察者逐个调用 update();每个观察者在 update() 里按需拉取主题数据,各取所需、互不干扰。主题不知道观察者具体要做什么,只管通知。
伪代码:将 UML 类转变为实现
下面把 UML 里的四个角色逐个落成代码。
以下代码为说明原理的示意性伪代码,省略头文件与 include guard 等内容。
先声明前向类,再定义 Subject 和 Observer 两个接口:
class Observer;
class Subject {
public:
virtual ~Subject() = default;
virtual void attach(Observer* o) = 0;
virtual void detach(Observer* o) = 0;
virtual void notify() = 0;
};
class Observer {
public:
virtual ~Observer() = default;
virtual void update() = 0; // 主题通知入口
};
WeatherData 是 ConcreteSubject,持有数据并管理观察者列表:
class WeatherData : public Subject {
std::vector<Observer*> observers;
float temperature = 0;
float humidity = 0;
float pressure = 0;
public:
void attach(Observer* o) override {
observers.push_back(o);
}
void detach(Observer* o) override {
// 实际工程中建议 erase-remove idiom
auto it = std::find(observers.begin(), observers.end(), o);
if (it != observers.end()) observers.erase(it);
}
void notify() override {
for (auto* o : observers) {
o->update(); // 逐个通知
}
}
// 数据变化时触发通知链
void measurementsChanged() {
notify();
}
// 模拟传感器更新数据
void setMeasurements(float t, float h, float p) {
temperature = t; humidity = h; pressure = p;
measurementsChanged();
}
// 观察者通过这些 getter 拉取数据
float getTemperature() const { return temperature; }
float getHumidity() const { return humidity; }
float getPressure() const { return pressure; }
};
CurrentDisplay 是 ConcreteObserver,update() 里拉取温度数据:
class CurrentDisplay : public Observer {
Subject* subject; // 保存主题引用,用于拉取数据
float temperature = 0;
public:
explicit CurrentDisplay(Subject* s) : subject(s) {
subject->attach(this); // 构造时自动注册
}
void update() override {
// 从主题拉取自己关心的数据
auto* wd = dynamic_cast<WeatherData*>(subject);
if (wd) temperature = wd->getTemperature();
display();
}
void display() const {
// 输出当前温度
}
};
ForecastPanel 拉取温度和气压做预测:
class ForecastPanel : public Observer {
Subject* subject;
float temperature = 0;
float pressure = 0;
public:
explicit ForecastPanel(Subject* s) : subject(s) {
subject->attach(this);
}
void update() override {
auto* wd = dynamic_cast<WeatherData*>(subject);
if (wd) {
temperature = wd->getTemperature();
pressure = wd->getPressure();
}
display();
}
void display() const {
// 根据温度和气压做预测
}
};
AlertSystem 监测温度和湿度,超阈值告警:
class AlertSystem : public Observer {
Subject* subject;
float temperature = 0;
float humidity = 0;
public:
explicit AlertSystem(Subject* s) : subject(s) {
subject->attach(this);
}
void update() override {
auto* wd = dynamic_cast<WeatherData*>(subject);
if (wd) {
temperature = wd->getTemperature();
humidity = wd->getHumidity();
}
check();
}
void check() const {
if (temperature > 40.0f) {
// 高温预警
}
}
};
Client 端组装主题和观察者,数据更新自动推送给所有设备:
int main() {
WeatherData weather;
CurrentDisplay display(&weather);
ForecastPanel forecast(&weather);
AlertSystem alert(&weather);
weather.setMeasurements(25.0f, 65.0f, 1013.0f);
// 三个观察者的 update() 自动被调用
// display 显示温度 25.0
// forecast 用温度 25.0 + 气压 1013.0 做预测
// alert 检查温度 25.0 未超阈值
weather.setMeasurements(42.0f, 30.0f, 1008.0f);
// alert 触发高温预警
}
以上代码走通了完整的通知链:主题数据变化→notify→遍历所有观察者→每个观察者 update 里拉取数据做自己的事。新增一个观察者,只要实现 Observer 并注册到主题即可。
用 std::function 的轻量替代
如果观察者数量不多、逻辑简单,可以用 std::function 回调代替整套接口继承。
class WeatherStation {
std::vector<std::function<void(float, float, float)>> callbacks;
public:
void subscribe(std::function<void(float, float, float)> cb) {
callbacks.push_back(std::move(cb));
}
void notify(float t, float h, float p) {
for (auto& cb : callbacks) {
cb(t, h, p);
}
}
};
// 使用端
WeatherStation ws;
ws.subscribe([](float t, float h, float p) {
std::cout << "当前温度: " << t << "\n";
});
ws.subscribe([threshold = 40.0f](float t, float, float) {
if (t > threshold) std::cout << "高温预警!\n";
});
这种写法省掉了 Observer 接口、继承和 dynamic_cast,回调直接接收数据参数,代码量少很多。
代价是缺乏生命周期管理。observer 对象如果被销毁,std::function 回调向量里的 lambda 捕获的裸指针就成了悬垂引用。经典 Observer 模式可以靠 detach/unregister 机制保护这一点,而回调方案需要自己处理。两种写法没有绝对优劣,取决于你的场景:观察者数量多、生命周期复杂、需要运行时增删,用经典模式;逻辑简单、生命周期可控、追求代码紧凑,用 std::function。
落到工程里要注意的几点
Subject 的析构函数要小心。 如果主题持有观察者的裸指针,观察者被销毁时没有 detach,主题的列表里就留下悬垂指针,下次 notify 会访问已释放内存。反过来,观察者持有主题的裸指针,主题先于观察者被销毁时,观察者后续调用 getter 也会访问已释放内存。实际工程通常用 std::weak_ptr 解决循环引用,或强制观察者析构前 detach。
notify 过程中不要增删观察者。 如果某个观察者的 update() 里触发了 attach 或 detach,当前正在遍历的 vector 被修改,迭代器失效导致崩溃。解决办法是 notify 时拷贝一份观察者列表再遍历,或者使用 std::list + 标记删除。
推模型 vs 拉模型需要权衡。 push 模型是主题在 notify 时直接把数据传给观察者(update(t, h, p)),观察者不需要反向拉取,效率高但假设所有观察者都要同样的数据。pull 模型是本文示例的做法(update() 无参数,观察者主动 getter),灵活但多一次回调。选哪种取决于数据一致性要求和观察者类型多少。
dynamic_cast 是把双刃剑。 本文伪代码里 ConcreteObserver 用 dynamic_cast<WeatherData*> 获取主题引用,这在观察者和主题强绑定时不优雅。更好的做法是给 Observer 构造函数直接传 WeatherData*,省掉 cast。另一个方向是让 Subject 的 notify 接口把自身引用作为参数传进去(update(Subject*)),观察者自己决定要不要转成具体类型。
线程安全需要额外处理。 如果 notify 发生在某条线程,而另一条线程正在 attach/detach,不加锁就是 data race。最轻量的方案是 std::shared_mutex(C++17):attach/detach 加写锁,notify 加读锁。