定义对象间一对多的依赖关系:当一个对象状态改变时,所有依赖它的对象都会得到通知并自动更新 —— 事件驱动世界的基石
观察者模式(Observer Pattern):定义对象间一种一对多的依赖关系,使得当一个对象(主题 Subject / 被观察者)改变状态时,所有依赖于它的对象(观察者 Observer)都会被自动通知并更新。它属于 GoF 23 种设计模式中的行为型模式,是"发布-订阅"思想在进程内、直接对象引用形态下的经典实现。
一句话记住它:"我不主动找你,我变了就喊一嗓子,谁关心谁就来。" 主题只负责广播"我变了",至于谁在听、听到后干什么,主题一概不关心。
先看一段没有模式的"坏代码"。假设我们要做一个气象站系统:气象站采集到新数据后,需要同步刷新三块展示屏——大屏、手机 App、网页端。
// 坏味道:气象站"认识"每一个具体的展示屏
class WeatherStation {
public:
void setMeasurements(float t, float h, float p) {
m_temp = t; m_humidity = h; m_pressure = p;
// 直接调用每个具体展示屏的方法 —— 高耦合!
m_bigScreen->update(t, h, p);
m_phoneApp->refresh(t, h, p);
m_webPage->render(t, h, p);
}
private:
BigScreen* m_bigScreen; // 具体类
PhoneApp* m_phoneApp; // 具体类
WebPage* m_webPage; // 具体类
};
这段代码的问题非常典型:
WeatherStation 的成员变量和 setMeasurements——每加一个展示端,就要改一次核心业务类,核心类被展示端"绑架"。💡 问题的本质:变化的方向错了。"有哪些展示端"是易变的部分,"数据变化要通知"是稳定的部分。观察者模式把易变的部分从稳定部分中剥离出去,让稳定部分只依赖一个抽象的"观察者"接口。
观察者模式的核心思想可以概括为四个字:订阅-广播。
attach(订阅)和 detach(退订)两个入口;状态变化时调用 notify,逐个通知观察者。update 接口。具体观察者实现这个接口,定义"收到通知后干什么"。生活类比:报刊订阅。出版社(主题)并不知道每个读者的名字和住址,读者只需在邮局登记订阅(attach)。每期报纸印好(状态变化),邮局按订阅名单投递(notify)。读者想停订就退订(detach),出版社完全不需要改动。再比如微信公众号:你关注了某个号(attach),号主一发文(notify),所有粉丝自动收到推送——号主从来不直接调用任何一个粉丝的"阅读"方法。
另一个经典类比是电台广播:电台(主题)对着频率喊话,收音机(观察者)调到该频率就能收到;收音机没开,电台照常广播,互不影响。观察者模式里的"频率"就是那个抽象的观察者接口。
模式还演化出两种通知模型:
Qt 的信号槽本质上是一种"推模型"的观察者:信号把参数打包发给槽函数。
观察者模式由四个角色组成:
| 角色 | 名称 | 职责 |
|---|---|---|
| 抽象主题 | Subject | 维护观察者列表;提供 attach() / detach() / notify() |
| 具体主题 | ConcreteSubject | 持有真实状态;状态变化时调用 notify();可提供 getter 供拉模型使用 |
| 抽象观察者 | Observer | 定义统一的更新接口 update()(纯虚函数) |
| 具体观察者 | ConcreteObserver | 实现 update(),定义收到通知后的具体行为;可持有对具体主题的引用以便拉数据 |
┌─────────────────┐ ┌──────────────────┐
│ <<interface>> │ │ <<interface>> │
│ Subject │ │ Observer │
├─────────────────┤ ├──────────────────┤
│ +attach(o) │ │ +update(data) │
│ +detach(o) │ └──────────────────┘
│ +notify() │ ▲
└───────┬─────────┘ │ 实现
│ 1 ── * 维护观察者列表 │
│ │
┌───────┴─────────┐ ┌───────┴──────────┐
│ ConcreteSubject │ │ ConcreteObserver │
├─────────────────┤ ├──────────────────┤
│ -state │ │ +update(data) │
│ +setState(s) │────────│ { 各自刷新 } │
└─────────────────┘ 通知 └──────────────────┘
协作流程:具体观察者把自己 attach 到具体主题 → 具体主题状态改变 → 调用 notify() → 遍历观察者列表逐个调用 update() → 各观察者自行刷新。整个过程主题只依赖抽象的 Observer 接口。
用 C++17 实现一个完整可编译的气象站例子。注意规范:观察者列表用 std::shared_ptr 管理所有权,detach 用 erase-remove 惯用法,杜绝悬空指针与内存泄漏。
// observer_demo.cpp —— C++17 观察者模式最小示例
// 编译:g++ -std=c++17 -Wall -Wextra observer_demo.cpp -o observer_demo
#include <iostream>
#include <vector>
#include <memory>
#include <algorithm>
#include <string>
// ── 抽象观察者:所有展示端实现的统一接口 ──
class IObserver {
public:
virtual ~IObserver() = default; // 虚析构,保证多态删除安全
virtual void update(float temp, float humidity, float pressure) = 0;
};
// ── 具体观察者:手机 App 展示端 ──
class PhoneApp : public IObserver {
public:
explicit PhoneApp(std::string name) : m_name(std::move(name)) {}
void update(float temp, float humi, float pres) override {
std::cout << "[" << m_name << "] 温度 " << temp
<< "°C | 湿度 " << humi
<< "% | 气压 " << pres << " hPa\n";
}
private:
std::string m_name;
};
// ── 主题(被观察者):气象站 ──
class WeatherStation {
public:
// 订阅:加入观察者列表
void attach(std::shared_ptr<IObserver> obs) {
m_observers.push_back(std::move(obs));
}
// 退订:从列表移除(erase-remove 惯用法)
void detach(const std::shared_ptr<IObserver>& obs) {
m_observers.erase(
std::remove(m_observers.begin(), m_observers.end(), obs),
m_observers.end());
}
// 状态变化入口:更新数据并广播
void setMeasurements(float t, float h, float p) {
m_temp = t; m_humidity = h; m_pressure = p;
notify();
}
private:
void notify() {
for (auto& obs : m_observers) { // 逐个通知,主题不认识具体类型
obs->update(m_temp, m_humidity, m_pressure);
}
}
std::vector<std::shared_ptr<IObserver>> m_observers; // 观察者列表
float m_temp = 0.0f, m_humidity = 0.0f, m_pressure = 0.0f;
};
int main() {
WeatherStation station;
auto livingRoom = std::make_shared<PhoneApp>("客厅大屏");
auto phone = std::make_shared<PhoneApp>("手机通知");
station.attach(livingRoom); // 两块屏都订阅
station.attach(phone);
station.setMeasurements(26.5f, 60.0f, 1013.2f); // 第一次数据更新,双屏都收到
station.detach(livingRoom); // 客厅大屏退订
station.setMeasurements(27.1f, 58.0f, 1012.8f); // 第二次更新,只剩手机收到
return 0;
}
程序输出:
[客厅大屏] 温度 26.5°C | 湿度 60% | 气压 1013.2 hPa
[手机通知] 温度 26.5°C | 湿度 60% | 气压 1013.2 hPa
[手机通知] 温度 27.1°C | 湿度 58% | 气压 1012.8 hPa
注意体会:加一块"网页展示屏"只需要新建一个继承 IObserver 的类再 attach 进去,WeatherStation 一行都不用改——这就是 OCP 的胜利。
Qt 本身就是观察者模式的"重度用户":信号与槽(Signals & Slots)机制就是观察者模式在 C++ 框架层面最成功的工程化实现。信号相当于"主题的通知",槽相当于"观察者的 update",connect() 相当于 attach,disconnect() 相当于 detach。相比手写观察者,Qt 的信号槽还额外提供了:
下面把气象站例子用 Qt6 + C++17 重写。三个文件 + CMake:
weatherstation.h —— 主题:气象站(QObject + 信号)
#ifndef WEATHERSTATION_H
#define WEATHERSTATION_H
#include <QObject>
// 主题:气象站。状态变化时通过信号广播给所有连接的观察者
class WeatherStation : public QObject {
Q_OBJECT
public:
explicit WeatherStation(QObject* parent = nullptr) : QObject(parent) {}
// 状态变化入口:更新数据并发射信号(等价于 notify())
void setMeasurements(double temp, double humidity, double pressure) {
m_temp = temp; m_humidity = humidity; m_pressure = pressure;
emit measurementsChanged(m_temp, m_humidity, m_pressure);
}
signals:
// 通知信号:参数就是推送给观察者的数据(推模型)
void measurementsChanged(double temp, double humidity, double pressure);
private:
double m_temp = 0.0, m_humidity = 0.0, m_pressure = 0.0;
};
#endif // WEATHERSTATION_H
display.h —— 观察者:展示端(QObject + 槽)
#ifndef DISPLAY_H
#define DISPLAY_H
#include <QObject>
#include <QDebug>
#include <QString>
// 观察者:展示端。槽函数等价于 update(),收到通知后自行刷新
class PhoneDisplay : public QObject {
Q_OBJECT
public:
explicit PhoneDisplay(const QString& name, QObject* parent = nullptr)
: QObject(parent), m_name(name) {}
public slots:
// 观察者的更新逻辑:打印一条日志模拟刷新 UI
void onMeasurementsChanged(double temp, double humidity, double pressure) {
qInfo() << "[" << m_name << "] 温度:" << temp
<< "°C 湿度:" << humidity << "% 气压:" << pressure << "hPa";
}
private:
QString m_name;
};
#endif // DISPLAY_H
main.cpp —— 组装:订阅与退订
#include <QCoreApplication>
#include "weatherstation.h"
#include "display.h"
int main(int argc, char* argv[]) {
QCoreApplication app(argc, argv);
WeatherStation station; // 主题
PhoneDisplay livingRoom("客厅大屏"); // 观察者 1
PhoneDisplay phone("手机通知"); // 观察者 2
// attach:把观察者的槽连接到主题的信号
QObject::connect(&station, &WeatherStation::measurementsChanged,
&livingRoom, &PhoneDisplay::onMeasurementsChanged);
QObject::connect(&station, &WeatherStation::measurementsChanged,
&phone, &PhoneDisplay::onMeasurementsChanged);
station.setMeasurements(26.5, 60.0, 1013.2); // 广播 1:两块屏都刷新
// detach:断开客厅大屏
QObject::disconnect(&station, &WeatherStation::measurementsChanged,
&livingRoom, &PhoneDisplay::onMeasurementsChanged);
station.setMeasurements(27.1, 58.0, 1012.8); // 广播 2:只剩手机收到
return 0;
}
CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(observer_demo LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_AUTOMOC ON) # 自动处理 Q_OBJECT 的 moc
find_package(Qt6 REQUIRED COMPONENTS Core)
add_executable(observer_demo
main.cpp
weatherstation.h
display.h
)
target_link_libraries(observer_demo PRIVATE Qt6::Core)
构建与运行:
mkdir build && cd build
cmake .. -DCMAKE_PREFIX_PATH=/path/to/qt6
cmake --build .
./observer_demo
运行输出(qInfo 格式):
[ "客厅大屏" ] 温度: 26.5 °C 湿度: 60 % 气压: 1013.2 hPa
[ "手机通知" ] 温度: 26.5 °C 湿度: 60 % 气压: 1013.2 hPa
[ "手机通知" ] 温度: 27.1 °C 湿度: 58 % 气压: 1012.8 hPa
在真正的 Qt 项目里,上面的 PhoneDisplay 通常是一个 QWidget/QLabel:把 onMeasurementsChanged 里换成 ui->tempLabel->setText(...) 就是标准的"数据变化驱动界面刷新",这也是 MVC 中 View 监听 Model 变化的基础。
以最小 C++ 示例为主线,梳理完整执行流程:
WeatherStation 和两个 PhoneApp,分别调用 station.attach(...) 把两个观察者加入 m_observers 列表。此时列表长度为 2,主题与观察者之间只有抽象接口的依赖。station.setMeasurements(26.5, 60.0, 1013.2),函数内部先更新三个成员变量(m_temp/m_humidity/m_pressure),再调用私有方法 notify()。notify() 用范围 for 遍历 m_observers,对每个元素调用 obs->update(m_temp, m_humidity, m_pressure)——这是多态调用,实际执行的是 PhoneApp::update,打印一行数据。两个观察者依次收到通知,输出两行日志。station.detach(livingRoom) 用 erase-remove 把客厅大屏从列表中移除,列表长度变 1。再次 setMeasurements(27.1, ...) 时,只有手机收到通知。WeatherStation 析构时其 std::shared_ptr<IObserver> 列表自动释放引用;PhoneApp 对象在 main 栈上自动析构。全程无手动 delete,无悬空指针。Qt 版本的流程本质相同,只是把"调用 update()"换成了"emit 信号 → 由元对象系统查找并调用所有已连接的槽";唯一差异是 disconnect 之后,如果接收者对象被销毁,连接会自动清理,无需手动维护列表。
观察者模式的每一个设计决策都指向同一个目标:让"通知的发出方"与"通知的接收方"彻底解耦。拆开来看:
Observer 接口,不依赖任何具体观察者。观察者可以独立变化、独立测试、独立部署,"多一个观察者"和"少一个观察者"对主题完全透明。IObserver 的类并 attach,主题类零修改——对扩展开放,对修改关闭。IObserver),而不是相互依赖具体类。高层模块(业务核心)不再依赖低层模块(展示细节)。update(),无需真实主题。两边都能独立单测。setMeasurements 一处,避免"忘通知"或"重复通知"这类低级 bug。💡 设计精髓:主题说"我变了,谁想知道自己来听",而不是"我挨个告诉你"。把通知的控制权从主题手中交还给"关注者自己",这就是观察者模式与"硬编码轮询/硬编码直调"的本质区别。
回到第二节的坏代码,想象系统继续演化的下场:
setMeasurements 里堆出一串 if (needPhone) ...; if (needWeb) ...,业务逻辑和展示逻辑纠缠成意大利面条。一句话总结反例的代价:稳定的通知机制被易变的接收方绑架,每一次"新增接收方"都变成对核心代码的一次外科手术。
适合使用的场景(3-5 个):
不适合的场景(2-4 个):
过度设计提醒:一对一的"A 变了 B 要更新",用普通调用即可,别为了"以后可能加观察者"提前套模式(YAGNI)。另外注意通知风暴:A 通知 B,B 又通知 A,可能形成环状级联;性能:广播是同步的,观察者里别做重活,否则拖慢主题。
| 对比模式 | 相似点 | 关键区别 |
|---|---|---|
| 中介者模式(Mediator) | 都是"多对象协作",都降低对象间耦合 | 观察者是一对多广播:主题→观察者单向流动,观察者之间互不认识;中介者是多对多中枢:所有对象(同事)只和中介者通信,由中介者协调转发,形成星型结构。Qt 的 QAbstractItemModel 之于多个 View 是观察者;而对话框里多个控件通过 QDialog 协调联动则更接近中介者。 |
| 发布-订阅(Pub/Sub) | 思想同源:发布者不直接调用订阅者 | 观察者模式里主题直接持有观察者引用,是进程内同步的;Pub/Sub 通常通过消息代理/主题通道中转,发布者和订阅者彼此完全无引用,可跨进程、跨机器、异步。观察者是 Pub/Sub 的"进程内简化版"。 |
| 命令模式(Command) | 都把"行为"封装成对象/接口 | 命令模式封装的是"一个待执行的动作"(可撤销、可排队),由调用者显式触发;观察者封装的是"对变化的响应",由主题状态变化隐式触发。命令是"主动的执行",观察者是"被动的响应"。 |
另外常被混淆的是信号槽与回调(Callback):回调是单接收者的函数指针,信号槽是多接收者、类型安全、可自动断连的观察者实现——可以认为 Qt 信号槽是回调的"观察者化升级版"。
观察者模式在 Qt 里不是"某个类用到了",而是整个框架的地基。最直接的体现:
QMetaObject::activate() 就是主题的 notify()——遍历连接列表,逐个调用槽函数(观察者的 update)。connect()=attach,disconnect()=detach,且接收者析构时自动断连(QObject 析构里调用 disconnect(this)),从机制上杜绝悬空指针。dataChanged() / rowsInserted() 等信号,所有 View(QListView/QTableView/QTreeView)作为观察者自动刷新。这正是 MVC 里"View 监听 Model"的标准观察者实现,也是 QAbstractItemView 的 setModel 内部 connect 的由来。finished()、QProcess 的 readyRead() 同理。QCoreApplication::sendEvent/postEvent + 各对象的 event()/eventFilter() 是观察者思想的另一变体——事件是"通知",事件过滤器是"可插拔的观察者"。可以说:理解观察者模式,就理解了 Qt 事件驱动架构的一半。
Q1:观察者模式和发布-订阅模式有什么区别?
A:观察者模式中,主题直接持有观察者的引用(或指针),通知是进程内同步调用,属于强引用的一对多;发布-订阅在两者之间引入了消息代理/通道,发布者和订阅者互不感知,支持跨进程、跨线程、异步。观察者模式可视为发布-订阅的进程内简化形态。Qt 信号槽介于两者之间:默认同线程直接调用(观察者风格),跨线程时退化为队列投递(带上了 Pub/Sub 的异步色彩)。
Q2:手写观察者模式时,如何避免悬空指针和内存泄漏?
A:三条策略:① 所有权清晰——观察者由外部持有(栈对象或 shared_ptr),主题只保存非拥有的引用/弱引用(如 std::weak_ptr 或裸指针,裸指针需配合"析构时 detach"的约定);② 观察者析构时必须从主题列表中移除自己(QObject 就是这么做的——析构自动 disconnect);③ 主题析构时清空列表。Qt 方案最省心:用 QObject 父子关系 + 信号槽自动断连,从语言层面消灭悬空通知。
Q3:推模型和拉模型各自优缺点?
A:推模型:主题把数据打包传给观察者,简单直接,但观察者可能收到大量用不上的数据,且主题需知道观察者"可能关心什么"。拉模型:主题只发"我变了",观察者按需调用主题 getter 取数,更灵活省流量,但观察者必须持有主题的具体类型引用,耦合更高。实践中常混合:通知带轻量提示参数,重数据按需拉取。
Q4:Qt 信号槽相比手写观察者,多解决了哪些问题?
A:① 类型安全(编译期检查参数签名);② 接收者析构自动断连,无悬空通知;③ 跨线程自动排队(QueuedConnection),线程安全;④ 支持一个信号连多个槽、一个槽连多个信号、信号连信号(转发);⑤ 断连粒度细(可按发送者/信号/接收者任意组合)。代价是 moc 元对象编译和反射查找的开销,以及信号槽调用比直接虚函数调用略慢。
Q5:观察者模式会不会导致性能问题?怎么优化?
A:会。同步广播下,N 个观察者串行执行,最慢的观察者拖慢主题。优化手段:① 观察者内部做轻量工作,重活异步化(Qt 中可用 Qt::QueuedConnection 或移入工作线程);② 状态高频变化时合并通知(如 Qt 的 layoutAboutToBeChanged 批量信号模式);③ 节流/去抖;④ 必要时按需注册,控制观察者数量。
题目:股票价格监控系统
用观察者模式实现一个简单的股票行情监控:
Stock 类(主题):持有股票代码和当前价格,提供 setPrice(double) 更新价格并广播。PriceAlert——当价格涨跌超过设定的阈值时打印"买入/卖出提醒";② TradeLogger——把每次价格变动打印成一行交易日志。提示 1:观察者接口 update 的签名可以直接传"当前价格",也可以传"价格变化量"——想一想哪种更适合"阈值告警"的需求(提示:告警需要知道新旧价格差)。
提示 2:观察者列表用 std::vector<std::shared_ptr<IObserver>>,退订时用 erase-remove;试着在 Stock 析构时打印一条日志,观察析构顺序,体会所有权管理。
进阶(可选):改用 Qt 实现——Stock 继承 QObject 发射 priceChanged(double) 信号,观察者用槽函数,用 QTimer::singleShot 模拟价格随时间波动,观察信号槽自动断连的优势。