设计模式之观察者模式:理论总结 Qt 实战 C++ 完整代码实现
文章目录
这周啃完软件设计体系里的观察者模式,从对着概念懵圈,到用Qt手搓了个完整的股票价格同步Demo,踩了不少坑,好消息是也终于把理论和实战彻底串起来了。
这篇博客完全按我自己的理解思路来写,不搞晦涩的教科书术语,都是大白话我们一起弄观察者模式。看完你能搞懂:
- 观察者模式的核心本质,再也不会背完概念就忘
- 怎么把设计模式和真实业务需求对应上,精准拆解角色
- 基于Qt完整实现观察者模式的可运行代码
- 本人落地观察者模式必踩的坑,帮你提前避坑
一、我对观察者模式的核心认识
按老师讲解把理论串起来后,我觉得观察者模式本质就是:当我们有数据变更时,要给所有关联窗口/类做数据同步的需求,就用这个设计模式。
它的核心角色只有两个,职责边界非常清晰:
-
主题(被观察者):
职责:记录和修改数据,管理所有观察者;数据更新时,同步给所有观察者。
(注:组合数据类做持久化存储,看需求,这不一定是被观察者的核心职责) -
观察者:
职责:展示自己观察的数据/事物(看需求非必要);
必须提供更新数据的接口,让被观察者调用。
观察者模式的分类
观察者模式的核心逻辑是「主题变→通知观察者」,但通知时「数据怎么给观察者」,就分成了两种模型:
- 拉模型(Pull Model):你Demo里用的就是这个!主题只通知「我变了」,观察者主动从主题里「拉」自己需要的数据。
- 推模型(Push Model):主题通知时,直接把观察者需要的数据「推」过去,观察者不用自己找。
二者优缺点对比:
推模型(Push Model)优缺点
| 核心优点 | 核心缺点 |
|---|---|
| 耦合度极低:观察者完全不依赖主题(被观察者)的任何接口,只依赖推送的数据本身。哪怕主题的类名、接口全换了,只要推送的价格、趋势数据不变,观察者一行代码都不用改。 | 扩展性极差:业务需求一变(比如新增一个观察者需要历史价格数据),就必须修改Observer抽象类的upData接口参数、修改主题的Notify推送逻辑,所有已有的观察者都必须跟着改接口实现,哪怕它们根本用不到新增的数据。 |
| 调用效率高:主题一次通知,就把所有数据一次性推送给观察者,观察者无需多次调用主题的接口获取数据。 | 数据冗余严重:主题会给所有观察者推送全量数据,哪怕部分观察者根本用不到。比如你的价格窗口只需要价格数据,但推模型依然会给它推送趋势数据,造成无效数据传输。 |
拉模型优缺点
| 核心优点 | 核心缺点 |
|---|---|
扩展性拉满:新增数据需求完全不用改核心抽象层。比如新增窗口要历史价格,只需要在主题里加一个getHistoryData()接口,需要的观察者自己调用即可,其他已有的观察者、抽象层代码一行都不用动。 |
耦合度相对更高:观察者必须依赖主题的具体接口,比如你的代码里,观察者必须知道主题有getData()、getTrend()方法,还要强转主题的具体类型,一旦主题的接口名改了,观察者也要跟着改。 |
| 无数据冗余:观察者按需获取数据,要什么就调用什么接口拿什么,不会收到任何自己用不到的无效数据。 | 调用效率稍低:如果一个观察者需要多个数据,就要多次调用主题的get接口。比如一个窗口同时要价格、趋势、历史数据,就要连续调用3次主题的接口,比推模型的一次推送开销更高。 |
| 先纠正下笔误哈,你说的“腿”是推模型,下面给你极简、精准的场景列举,一眼分清,直接就能放到博客里用。 |
✅ 需求固定、数据统一 → 用推
✅ 需求多变、按需获取 → 用拉
而我们下面用的就是拉模型的方式进行实现的。
二、根据实际需求将模式和业务对应
实战需求背景
实现一个桌面图形界面程序,模拟股票价格变化,实现多个窗口自动同步更新的效果。
系统包含三个独立窗口:
- 价格输入窗口(修改股票价格的入口)
- 当前价格显示窗口(实时展示最新股价)
- 涨跌趋势显示窗口(实时展示股价涨跌状态)
核心要求:当股票价格发生变化时,两个显示窗口必须立即、自动同步更新。
角色对应拆解
结合前面的理解,我们可以对应拆解:
- 价格输入窗口 → 被观察者(主题):它是数据变化的触发方,负责更新数据、同步数据,我们把它提取成类
PriceControlWindow。 - 两个显示窗口 → 观察者:它们只需要接收数据更新的通知,刷新自己的展示内容,我们提取成两个类
TrendWindow、CurrentPriceWindow。
疑问:为什么是被观察者管理所有观察者?
注:观察者模式的核心逻辑是「谁触发变化,谁负责通知」,从这个角度理解,这件事就完全合理了——只有数据的修改方,才知道什么时候该发通知,自然由它来管要通知给谁。
抽象层核心约定
把角色的共性抽出来,就有了我们的抽象类设计,这里有两个核心约定,也是观察者模式的灵魂:
-
Subject(主题/被观察者):必须包含三个核心行为:注册观察者
Attach、删除观察者Detach、通知所有观察者Notify。注:在写观察者模式的代码时函数返回值和参数不强制固定,比如插入删除可设计成
bool返回是否成功,参数是观察者指针,通知函数无参数,因为主题里已经存了所有观察者信息等,但具体模块的核心行为必须有:如注册观察者、删除观察者、通知所有观察者。 -
Observer(观察者):核心职责只有一个:必须定义一个统一的更新接口
upData,这是被观察者能通知到所有观察者的唯一保证,同上参数和返回值可以根据业务调整,但这个接口必须有。 -
Data(数据类):需要一个实体类和主题组合(被主题包含)。因为要存每日股票价格,用
vector<float>;股票状态用 -1(跌)、0(无变化)、1(涨)即可。
对应上面的设计,我画了完整的UML类图,每个类的职责和接口都一一对应:
三、实现实际需求
开发环境:Qt 5/6 + C++11及以上,代码可以直接复制运行,我用的是VS Stdio 2022 写的。
先搭抽象层——Observer和Subject抽象类
这个只有一个注意的点由于主题和观察者之间存在互相包含的情况,所以我们如果分开写两个头文件未来避免头文件相互引用问题,可以事先声明一个,其他的类图中描述的很清楚,直接实现即可:
// Observer.hpp 被观察者抽象类
#pragma once
class Subject;
class Observer
{
public:
virtual void upData(Subject* sub) = 0;
};
// Subject.hpp 观察者/主题抽象类
#pragma once
#include "Observer.hpp"
class Subject {
public:
virtual void Attach(Observer* obs) = 0; // 注册观察者
virtual void Detach(Observer* obs) = 0; // 移除观察者
virtual void Notify() = 0; // 通知所有观察者
};
数据类实现
数据类只做一件事:存数据、给外部提供安全的读写能力。
这里我踩了个小坑:一开始写了手动setTrend的接口,但实际需求里,涨跌趋势是由价格历史数据自动计算的,手动设置反而会导致数据不一致,建议优化掉了冗余的逻辑,但是我这里没有改嘿嘿。
// Data.hpp 声明
#pragma once
#include <vector>
class Data{
public:
float getData();
void setData(float data);
int getTrend();
void setTrend(int trend);
private:
std::vector<float> _data;
int _trend = 0;
};
// Data.cpp 实现
#include "Data.hpp"
float Data::getData()
{
return _data.back();
}
void Data::setData(float data)
{
_data.push_back(data);
}
int Data::getTrend()
{
int size = _data.size();
if (size <= 1)
return 0;
float before = _data[size - 2];
float now = _data[size - 1];
if (before == now)
return 0;
else if (before < now)
return 1;
else
return -1;
}
void Data::setTrend(int trend)
{
_trend = trend;
}
被观察者实例化——价格控制窗口
被观察者的核心逻辑就是三件事:做UI、维护观察者列表、数据更新时发通知。
我的思路是直接在构造函数里写Qt的UI创建代码,用vector维护观察者列表,通知就是遍历列表,挨个调用观察者的upData接口。
// PriceControlWindow.hpp
#pragma once
#include <QWidget>
#include <QLabel>
#include <QLineEdit>
#include <QPushButton>
#include <QVBoxLayout>
#include <vector>
#include <string>
#include "Subject.hpp"
#include "Data.hpp"
class PriceControlWindow : public QWidget, public Subject{
Q_OBJECT
public:
explicit PriceControlWindow(QWidget* parent = nullptr) : QWidget(parent)
{
// 1. 创建输入框
controlLabel = new QLineEdit(this);
controlLabel->setPlaceholderText("Today's stock prices: ");
// 2. 创建按钮
readBtn = new QPushButton("Update Price", this);
// 3. 布局(把两个控件放进去)
QVBoxLayout* layout = new QVBoxLayout(this);
layout->addWidget(controlLabel);
layout->addWidget(readBtn);
// 4. 按钮点击 → 读取输入框内容
connect(readBtn, &QPushButton::clicked, this, [this]() {
QString text = controlLabel->text();
float tmp = text.toFloat();
stock_data.setData(tmp);
Notify();
});
}
// 注册观察者,将观察者对象插入到观察者列表
void Attach(Observer* obs) override
{
if (obs && std::find(observers.begin(), observers.end(), obs) == observers.end())
observers.push_back(obs);
}
// 找到对应观察者,将观察者删除
void Detach(Observer* obs) override
{
auto it = std::find(observers.begin(), observers.end(), obs);
if (it != observers.end())
observers.erase(it);
}
// 调用所有观察者对象的upData来进行更新数据
void Notify() override
{
for (auto obs : observers)
{
if (obs)
obs->upData(this); // 通知所有观察者
}
}
float getData()
{
return stock_data.getData();
}
// 向Data存储数据
void setData(float val)
{
stock_data.setData(val);
}
int getTrend()
{
return stock_data.getTrend();
}
// 向Data存储数据
void setTrend(int val)
{
stock_data.setTrend(val);
Notify(); // 趋势更新,通知观察者
}
private:
Data stock_data;
std::vector<Observer*> observers; // 维护观察者列表
QLineEdit* controlLabel;
QPushButton* readBtn;
};
观察者实例化——两个显示窗口
观察者的逻辑非常简单:做UI、实现upData接口,收到通知后从被观察者那里拿最新数据,刷新自己的UI就行。
// CurrentPriceWindow.hpp
#include <QtWidgets/QApplication>
#include <QWidget>
#include <QLabel>
#include <QVBoxLayout>
#include "Observer.hpp"
#include "Subject.hpp"
#include "PriceControlWindow.hpp"
class CurrentPriceWindow : public QWidget, public Observer{
Q_OBJECT
public:
explicit CurrentPriceWindow(QWidget* parent = nullptr) : QWidget(parent) {
// 初始化UI控件
priceLabel = new QLabel("Current Price: 0.0", this);
QVBoxLayout* layout = new QVBoxLayout(this);
layout->addWidget(priceLabel);
setLayout(layout);
}
void upData(Subject* sub) override
{
PriceControlWindow* window = (PriceControlWindow*)sub;
if (window) {
float latestPrice = window->getData();
priceLabel->setText(QString("Current Price: %1").arg(latestPrice));
}
}
private:
QLabel* priceLabel;
};
// TrendWindow.hpp
#include <QtWidgets/QApplication>
#include <QWidget>
#include <QLabel>
#include <QVBoxLayout>
#include <QDebug>
#include "Observer.hpp"
#include "Subject.hpp"
#include "PriceControlWindow.hpp"
class TrendWindow :public QWidget, public Observer{
Q_OBJECT
public:
explicit TrendWindow(QWidget* parent = nullptr) : QWidget(parent) {
// 初始化UI控件
trendLabel = new QLabel("Trend: No Change", this);
QVBoxLayout* layout = new QVBoxLayout(this);
layout->addWidget(trendLabel);
setLayout(layout);
}
void upData(Subject* sub) override
{
PriceControlWindow* window = (PriceControlWindow*)sub;
int mark = window->getTrend();
QString trend;
if (mark == 0)
trend = "No Change";
else if (mark > 0)
trend = QString::fromUtf8("\u2191 Up");
else
trend = QString::fromUtf8("\u2193 Down");
trendLabel->setText(trend);
}
private:
QLabel* trendLabel;
};
四、效果演示
光说不练假把式,下面就是如何调用,让代码跑起来,效果我们可以看截图:
#include <QApplication>
#include "CurrentPriceWindow.hpp"
#include "PriceControlWindow.hpp"
#include "TrendWindow.hpp"
int main(int argc, char* argv[])
{
// 1. 初始化Qt应用程序(所有Qt程序必须写这行)
QApplication a(argc, argv);
// ===================== 2. 创建窗口对象 =====================
// 被观察者:价格控制主窗口(用来输入价格)
PriceControlWindow controlWin;
controlWin.resize(300, 150); // 设置窗口大小
// 观察者1:当前价格显示窗口
CurrentPriceWindow priceWin;
priceWin.resize(300, 150);
// 观察者2:价格趋势显示窗口
TrendWindow trendWin;
trendWin.resize(300, 150);
// ===================== 3. 绑定观察者 =====================
// 把两个观察窗口 注册到 控制窗口
controlWin.Attach(&priceWin);
controlWin.Attach(&trendWin);
// ===================== 4. 显示所有窗口 =====================
controlWin.show();
priceWin.show();
trendWin.show();
// 5. 启动Qt事件循环
return a.exec();
return 0;
}
最开始,没有数据:
当我们输入第一个数据,100时,股票价格更新为100,由于是第一天所以股票无波动:
第二天我们输入200,显示股票上涨:
第三天我们输入 20,显示股票下降:
OK,大功告成!
五、作者的踩坑记录
这些都是我自己写代码的时候实打实踩的坑,如果是和我一样刚接触设计模式的朋友大概率也会遇到,提前给大家避坑:
-
最开始想把 Qt UI 和观察者模式的核心逻辑完全解耦,思路是没问题的,解耦后代码可维护性更高,但刚接触 Qt 对信号槽机制还不熟练,暂时先把逻辑写在一起了,后续可以再优化。
-
千万别忘了在数据更新后调用主题的Notify()同步函数,但也要注意不要重复调用,会导致观察者冗余更新,甚至出现未知 bug。
-
数据类的get/set不要设计得太冗余,要贴合实际业务需求,用不上的接口就删掉,不然很容易出现数据不一致的问题,虽然我没有删。
-
我对 Qt 的头文件依赖不熟,不然完全可以用一个Common.hpp统一管理公共头文件,既美观又不容易出现头文件引用错误,大家可以试一试。
-
违背依赖倒置原则:
Subject抽象类未定义数据获取的虚接口,导致观察者必须强转依赖具体的PriceControlWindow实现类,而非依赖抽象接口。建议在Subject中声明getData()、getTrend()纯虚函数,让观察者只依赖抽象层,解耦更彻底,这里我是AI指出的,大家不要和我才一样的坑!
六、写在最后
其实写完整个 Demo 再回头看,观察者模式的核心根本不是那几个固定的接口,而是解耦。
它把数据的生产方(被观察者)和数据的消费方(观察者)完全分开了:
生产方不用管有多少个消费方,只需要在数据变化的时候发个通知就行。
消费方不用管数据什么时候变,只需要实现好更新接口,收到通知就做自己的事,两边互不干扰。
除了这个 Qt GUI 的场景,像事件总线、消息订阅发布、游戏里的状态同步、甚至前端的事件监听,全都是观察者模式的落地。只要我们遇到「一个对象变化,要通知其他 N 个对象同步更新」的场景,第一个就该想到它。
如果这篇博客对你有帮助的话能够我点一个赞吗,这是对我对我最大的鼓励,如果想日和再拿出来看看复习下收藏一下,如果对我写的设计模式感兴趣的话我日后会持续更新,点个关注,下一篇更精彩,这个专栏的下一篇文章大概率是装饰器模式哦!

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)