在这里插入图片描述

这周啃完软件设计体系里的观察者模式,从对着概念懵圈,到用Qt手搓了个完整的股票价格同步Demo,踩了不少坑,好消息是也终于把理论和实战彻底串起来了。

这篇博客完全按我自己的理解思路来写,不搞晦涩的教科书术语,都是大白话我们一起弄观察者模式。看完你能搞懂:

  1. 观察者模式的核心本质,再也不会背完概念就忘
  2. 怎么把设计模式和真实业务需求对应上,精准拆解角色
  3. 基于Qt完整实现观察者模式的可运行代码
  4. 本人落地观察者模式必踩的坑,帮你提前避坑

一、我对观察者模式的核心认识

按老师讲解把理论串起来后,我觉得观察者模式本质就是:当我们有数据变更时,要给所有关联窗口/类做数据同步的需求,就用这个设计模式

它的核心角色只有两个,职责边界非常清晰:

  • 主题(被观察者)
    职责:记录和修改数据,管理所有观察者;数据更新时,同步给所有观察者。
    (注:组合数据类做持久化存储,看需求,这不一定是被观察者的核心职责)

  • 观察者
    职责:展示自己观察的数据/事物(看需求非必要);
    必须提供更新数据的接口,让被观察者调用。

观察者模式的分类

观察者模式的核心逻辑是「主题变→通知观察者」,但通知时「数据怎么给观察者」,就分成了两种模型:

  • 拉模型(Pull Model):你Demo里用的就是这个!主题只通知「我变了」,观察者主动从主题里「拉」自己需要的数据
  • 推模型(Push Model):主题通知时,直接把观察者需要的数据「推」过去,观察者不用自己找。

二者优缺点对比:

推模型(Push Model)优缺点

核心优点 核心缺点
耦合度极低:观察者完全不依赖主题(被观察者)的任何接口,只依赖推送的数据本身。哪怕主题的类名、接口全换了,只要推送的价格、趋势数据不变,观察者一行代码都不用改。 扩展性极差:业务需求一变(比如新增一个观察者需要历史价格数据),就必须修改Observer抽象类的upData接口参数、修改主题的Notify推送逻辑,所有已有的观察者都必须跟着改接口实现,哪怕它们根本用不到新增的数据。
调用效率高:主题一次通知,就把所有数据一次性推送给观察者,观察者无需多次调用主题的接口获取数据。 数据冗余严重:主题会给所有观察者推送全量数据,哪怕部分观察者根本用不到。比如你的价格窗口只需要价格数据,但推模型依然会给它推送趋势数据,造成无效数据传输。

拉模型优缺点

核心优点 核心缺点
扩展性拉满:新增数据需求完全不用改核心抽象层。比如新增窗口要历史价格,只需要在主题里加一个getHistoryData()接口,需要的观察者自己调用即可,其他已有的观察者、抽象层代码一行都不用动。 耦合度相对更高:观察者必须依赖主题的具体接口,比如你的代码里,观察者必须知道主题有getData()getTrend()方法,还要强转主题的具体类型,一旦主题的接口名改了,观察者也要跟着改。
无数据冗余:观察者按需获取数据,要什么就调用什么接口拿什么,不会收到任何自己用不到的无效数据。 调用效率稍低:如果一个观察者需要多个数据,就要多次调用主题的get接口。比如一个窗口同时要价格、趋势、历史数据,就要连续调用3次主题的接口,比推模型的一次推送开销更高。
先纠正下笔误哈,你说的“腿”是推模型,下面给你极简、精准的场景列举,一眼分清,直接就能放到博客里用。

✅ 需求固定、数据统一 → 用
✅ 需求多变、按需获取 → 用

而我们下面用的就是拉模型的方式进行实现的。

二、根据实际需求将模式和业务对应

实战需求背景

实现一个桌面图形界面程序,模拟股票价格变化,实现多个窗口自动同步更新的效果。
系统包含三个独立窗口:

  1. 价格输入窗口(修改股票价格的入口)
  2. 当前价格显示窗口(实时展示最新股价)
  3. 涨跌趋势显示窗口(实时展示股价涨跌状态)
    核心要求:当股票价格发生变化时,两个显示窗口必须立即、自动同步更新。

角色对应拆解

结合前面的理解,我们可以对应拆解:

  1. 价格输入窗口 → 被观察者(主题):它是数据变化的触发方,负责更新数据、同步数据,我们把它提取成类 PriceControlWindow
  2. 两个显示窗口 → 观察者:它们只需要接收数据更新的通知,刷新自己的展示内容,我们提取成两个类 TrendWindowCurrentPriceWindow

疑问:为什么是被观察者管理所有观察者?
注:观察者模式的核心逻辑是「谁触发变化,谁负责通知」,从这个角度理解,这件事就完全合理了——只有数据的修改方,才知道什么时候该发通知,自然由它来管要通知给谁。

抽象层核心约定

把角色的共性抽出来,就有了我们的抽象类设计,这里有两个核心约定,也是观察者模式的灵魂:

  1. Subject(主题/被观察者):必须包含三个核心行为:注册观察者Attach、删除观察者Detach、通知所有观察者Notify

    注:在写观察者模式的代码时函数返回值和参数不强制固定,比如插入删除可设计成 bool 返回是否成功,参数是观察者指针,通知函数无参数,因为主题里已经存了所有观察者信息等,但具体模块的核心行为必须有:如注册观察者、删除观察者、通知所有观察者

  2. Observer(观察者):核心职责只有一个:必须定义一个统一的更新接口upData,这是被观察者能通知到所有观察者的唯一保证,同上参数和返回值可以根据业务调整,但这个接口必须有。

  3. 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,大功告成!
在这里插入图片描述

五、作者的踩坑记录

这些都是我自己写代码的时候实打实踩的坑,如果是和我一样刚接触设计模式的朋友大概率也会遇到,提前给大家避坑:

  1. 最开始想把 Qt UI 和观察者模式的核心逻辑完全解耦,思路是没问题的,解耦后代码可维护性更高,但刚接触 Qt 对信号槽机制还不熟练,暂时先把逻辑写在一起了,后续可以再优化。

  2. 千万别忘了在数据更新后调用主题的Notify()同步函数,但也要注意不要重复调用,会导致观察者冗余更新,甚至出现未知 bug。

  3. 数据类的get/set不要设计得太冗余,要贴合实际业务需求,用不上的接口就删掉,不然很容易出现数据不一致的问题,虽然我没有删。

  4. 我对 Qt 的头文件依赖不熟,不然完全可以用一个Common.hpp统一管理公共头文件,既美观又不容易出现头文件引用错误,大家可以试一试。

  5. 违背依赖倒置原则Subject抽象类未定义数据获取的虚接口,导致观察者必须强转依赖具体的PriceControlWindow实现类,而非依赖抽象接口。建议在Subject中声明getData()getTrend()纯虚函数,让观察者只依赖抽象层,解耦更彻底,这里我是AI指出的,大家不要和我才一样的坑!

六、写在最后

其实写完整个 Demo 再回头看,观察者模式的核心根本不是那几个固定的接口,而是解耦

它把数据的生产方(被观察者)和数据的消费方(观察者)完全分开了:
生产方不用管有多少个消费方,只需要在数据变化的时候发个通知就行。
消费方不用管数据什么时候变,只需要实现好更新接口,收到通知就做自己的事,两边互不干扰。

除了这个 Qt GUI 的场景,像事件总线、消息订阅发布、游戏里的状态同步、甚至前端的事件监听,全都是观察者模式的落地。只要我们遇到「一个对象变化,要通知其他 N 个对象同步更新」的场景,第一个就该想到它。

如果这篇博客对你有帮助的话能够我点一个赞吗,这是对我对我最大的鼓励,如果想日和再拿出来看看复习下收藏一下,如果对我写的设计模式感兴趣的话我日后会持续更新,点个关注,下一篇更精彩,这个专栏的下一篇文章大概率是装饰器模式哦!

在这里插入图片描述

Logo

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

更多推荐