当前位置: 首页 > news >正文

【C++实战㊻】解锁C++观察者模式:从理论到实战

目录

  • 一、观察者模式的概念
    • 1.1 观察者模式的定义
    • 1.2 观察者模式的适用场景
    • 1.3 观察者模式的结构
  • 二、观察者模式的实战应用
    • 2.1 观察者模式的实现代码
    • 2.2 推模型与拉模型的区别
    • 2.3 观察者模式的线程安全处理
  • 三、观察者模式的实战技巧
    • 3.1 观察者模式的解耦设计
    • 3.2 观察者模式的通知顺序控制
    • 3.3 观察者模式的内存管理
  • 四、实战项目:股票行情通知系统
    • 4.1 项目需求
    • 4.2 观察者模式实现行情主题与客户端观察者
    • 4.3 多客户端并发接收通知的测试


一、观察者模式的概念

1.1 观察者模式的定义

观察者模式是一种行为设计模式,它定义了对象间的一种一对多的依赖关系 ,使得当一个对象(被称为主题,Subject)的状态发生改变时,所有依赖于它的对象(被称为观察者,Observer)都会得到通知并自动更新。在这种模式中,主题并不需要知道具体有哪些观察者,它只负责维护一个观察者列表,并在状态改变时通知这些观察者。观察者则实现特定的接口,以便在接收到通知时能够执行相应的操作。

用一个生活中的例子来理解,比如你关注了某个博主,当博主发布新内容时,你会收到通知,这里博主就是主题,你和其他关注者就是观察者。博主不需要知道具体是谁关注了他,只需要发布内容,所有关注他的人都会收到通知并可以根据自己的需求去查看新内容。

1.2 观察者模式的适用场景

  • 事件监听:在图形用户界面(GUI)编程中,当用户进行鼠标点击、键盘输入等操作时,程序需要做出相应的响应。例如,在一个按钮点击事件中,可能会有多个不同的操作需要执行,如保存数据、更新界面显示等。可以将按钮作为主题,将各个操作作为观察者,当按钮被点击(主题状态改变)时,通知所有相关的观察者执行相应的操作。
  • 消息通知:在消息系统中,当有新消息到达时,需要通知所有订阅该消息的用户。比如邮件系统,当有新邮件到达时,会通知收件人查看邮件。这里邮件服务器就是主题,收件人就是观察者。
  • 状态同步:在分布式系统中,不同节点之间需要保持数据或状态的同步。例如,在一个多节点的缓存系统中,当某个节点的数据发生更新时,需要通知其他节点更新它们的缓存数据,以保证数据的一致性。更新数据的节点就是主题,其他节点就是观察者。

1.3 观察者模式的结构

  • 主题(Subject):也被称为被观察者或可观察对象。它维护了一个观察者列表,提供了注册(attach)、移除(detach)观察者的方法,以及在自身状态改变时通知(notify)所有观察者的方法。主题并不知道具体观察者的实现细节,只与观察者接口进行交互。
  • 观察者接口(Observer):定义了一个更新(update)方法,当主题状态发生变化并通知观察者时,观察者将调用这个方法来执行相应的操作。所有具体的观察者类都必须实现这个接口。
  • 具体观察者(ConcreteObserver):实现了观察者接口,具体定义了在接收到主题通知时的行为。每个具体观察者都可以有自己独特的处理逻辑,以响应主题状态的变化。例如,在股票行情通知系统中,不同的投资者(具体观察者)可能会根据自己的投资策略对股票价格的变化做出不同的反应。

二、观察者模式的实战应用

2.1 观察者模式的实现代码

下面是一个用 C++ 实现观察者模式的示例代码,展示了主题注册、移除观察者和通知机制的实现逻辑:

#include <iostream>
#include <vector>// 观察者接口
class Observer {
public:virtual void update() = 0;virtual ~Observer() {}
};// 主题类
class Subject {
private:std::vector<Observer*> observers;
public:void attach(Observer* observer) {observers.push_back(observer);}void detach(Observer* observer) {auto it = std::find(observers.begin(), observers.end(), observer);if (it != observers.end()) {observers.erase(it);}}void notify() {for (Observer* observer : observers) {observer->update();}}
};// 具体观察者A
class ConcreteObserverA : public Observer {
public:void update() override {std::cout << "ConcreteObserverA received update." << std::endl;}
};// 具体观察者B
class ConcreteObserverB : public Observer {
public:void update() override {std::cout << "ConcreteObserverB received update." << std::endl;}
};

在上述代码中:

  • Observer 是观察者接口,定义了 update 方法,当主题状态改变时,观察者通过这个方法来更新自己的状态。
  • Subject 是主题类,维护了一个观察者的向量 observers。attach 方法用于将观察者添加到列表中,detach 方法用于从列表中移除观察者,notify 方法用于通知所有注册的观察者。
  • ConcreteObserverA 和 ConcreteObserverB 是具体的观察者类,它们实现了 Observer 接口的 update 方法,定义了具体的更新行为。

2.2 推模型与拉模型的区别

在观察者模式中,根据主题通知观察者时传递数据的方式不同,可以分为推模型和拉模型:

  • 推模型:主题在通知观察者时,会将所有相关的数据主动推送给观察者。例如,在股票行情系统中,当股票价格发生变化时,行情主题会将最新的股票价格、成交量等数据直接发送给所有注册的客户端观察者。这种模型的优点是观察者无需主动获取数据,数据获取及时,适用于对实时性要求较高的场景。但缺点是如果数据量较大,可能会造成网络带宽的浪费,并且主题需要知道观察者需要哪些数据。
  • 拉模型:主题在通知观察者时,只传递少量的元信息,如状态变化的标识等。观察者如果需要具体的数据,需要主动向主题发起请求来拉取。还是以股票行情系统为例,行情主题通知客户端观察者股票价格有变化,但具体的价格数据需要客户端观察者自己去查询。拉模型的优点是主题不需要关心观察者需要哪些数据,灵活性较高,减轻了主题的负担。缺点是观察者获取数据的实时性相对较差,并且增加了观察者与主题之间的交互次数。

2.3 观察者模式的线程安全处理

在多线程环境下,观察者模式可能会出现并发问题,比如当主题在通知观察者时,另一个线程同时对观察者列表进行修改(添加或移除观察者),可能会导致程序崩溃或出现未定义行为。以下是一些常见的解决方案:

  • 加锁机制:在主题的 attach、detach 和 notify 方法中使用互斥锁(如 std::mutex)来保证同一时间只有一个线程可以访问观察者列表。例如:
class Subject {
private:std::vector<Observer*> observers;std::mutex mtx;
public:void attach(Observer* observer) {std::lock_guard<std::mutex> lock(mtx);observers.push_back(observer);}void detach(Observer* observer) {std::lock_guard<std::mutex> lock(mtx);auto it = std::find(observers.begin(), observers.end(), observer);if (it != observers.end()) {observers.erase(it);}}void notify() {std::lock_guard<std::mutex> lock(mtx);for (Observer* observer : observers) {observer->update();}}
};
  • 使用线程安全容器:C++ 标准库中的一些容器(如 std::unordered_map、std::vector 等)本身不是线程安全的。可以考虑使用一些线程安全的容器库,如 boost::container::flat_map 等,这些容器内部已经实现了线程安全机制,使用起来更加方便。
  • Copy - on - Write(写时复制):在主题状态变化时,创建一个新的观察者列表副本,在副本上进行修改操作,修改完成后再将副本替换原来的列表。这样可以避免在通知过程中对原列表的修改,从而保证线程安全。但这种方法会增加一定的内存开销。

三、观察者模式的实战技巧

3.1 观察者模式的解耦设计

在观察者模式中,解耦主题与观察者是非常重要的,这样可以提高代码的可维护性和扩展性。一种常见的解耦方法是通过接口或抽象类来实现。主题只依赖于观察者接口,而不依赖于具体的观察者类。例如:

// 观察者接口
class Observer {
public:virtual void update() = 0;virtual ~Observer() {}
};// 主题类
class Subject {
private:std::vector<Observer*> observers;
public:void attach(Observer* observer) {observers.push_back(observer);}void detach(Observer* observer) {auto it = std::find(observers.begin(), observers.end(), observer);if (it != observers.end()) {observers.erase(it);}}void notify() {for (Observer* observer : observers) {observer->update();}}
};

在这个例子中,Subject 类只与 Observer 接口交互,而不关心具体是哪些类实现了这个接口。这样,当需要添加新的观察者时,只需要创建一个实现 Observer 接口的新类,而不需要修改 Subject 类的代码。如果主题直接依赖于具体的观察者类,那么当有新的观察者类型出现时,就可能需要修改主题的代码,这会增加代码的维护难度,也不符合开闭原则。

3.2 观察者模式的通知顺序控制

在默认情况下,观察者模式中观察者的通知顺序是不确定的,这在大多数情况下是可以接受的。但在某些业务场景下,可能需要控制观察者的通知顺序。比如在一个游戏开发场景中,有一些观察者负责处理玩家的输入事件,有些观察者负责更新游戏画面,而更新游戏画面的观察者需要在处理输入事件的观察者之后执行,以确保画面显示的是最新的游戏状态。

一种控制通知顺序的方法是使用优先级队列。可以为每个观察者分配一个优先级,在通知时,按照优先级从高到低或从低到高的顺序依次通知观察者。例如,可以定义一个结构体来存储观察者及其优先级:

struct ObserverInfo {Observer* observer;int priority;ObserverInfo(Observer* obs, int pri) : observer(obs), priority(pri) {}
};class Subject {
private:std::vector<ObserverInfo> observers;
public:void attach(Observer* observer, int priority) {observers.emplace_back(observer, priority);}void detach(Observer* observer) {auto it = std::find_if(observers.begin(), observers.end(),[observer](const ObserverInfo& info) {return info.observer == observer;});if (it != observers.end()) {observers.erase(it);}}void notify() {auto compare = [](const ObserverInfo& a, const ObserverInfo& b) {return a.priority < b.priority;};std::sort(observers.begin(), observers.end(), compare);for (const auto& info : observers) {info.observer->update();}}
};

在上述代码中,attach 方法在添加观察者时同时记录其优先级。notify 方法在通知观察者之前,先对观察者列表按照优先级进行排序,然后依次通知观察者。

另一种方法是自定义排序。可以在主题类中维护一个自定义的排序规则,比如按照观察者注册的先后顺序进行通知。可以通过在观察者类中添加一个表示注册顺序的成员变量,然后在主题类的 notify 方法中根据这个变量进行排序。

3.3 观察者模式的内存管理

在观察者模式中,如果不妥善处理内存管理,可能会出现悬空指针的问题。当一个观察者对象被销毁,但主题的观察者列表中仍然保存着指向该对象的指针时,就会产生悬空指针。例如:

Subject subject;
ConcreteObserverA* observerA = new ConcreteObserverA();
subject.attach(observerA);
delete observerA;
// 此时 subject 的观察者列表中仍然保存着指向已删除对象的指针,即悬空指针
subject.notify(); // 这会导致未定义行为

为了避免悬空指针问题,可以使用智能指针来管理观察者对象的内存。例如,使用 std::shared_ptr 来代替原始指针:

#include <memory>class Observer {
public:virtual void update() = 0;virtual ~Observer() {}
};class Subject {
private:std::vector<std::shared_ptr<Observer>> observers;
public:void attach(std::shared_ptr<Observer> observer) {observers.push_back(observer);}void detach(const std::shared_ptr<Observer>& observer) {auto it = std::find(observers.begin(), observers.end(), observer);if (it != observers.end()) {observers.erase(it);}}void notify() {for (const auto& observer : observers) {observer->update();}}
};class ConcreteObserverA : public Observer {
public:void update() override {std::cout << "ConcreteObserverA received update." << std::endl;}
};

在这个例子中,Subject 类的观察者列表中保存的是 std::shared_ptr<Observer>,当 ConcreteObserverA 对象的引用计数为 0 时,它会被自动销毁,从而避免了悬空指针的问题。同时,使用智能指针也能有效地防止内存泄漏,因为智能指针会在其生命周期结束时自动释放所指向的内存。

四、实战项目:股票行情通知系统

4.1 项目需求

在股票投资领域,投资者需要实时了解股票价格的变化,以便做出合理的投资决策。本项目旨在构建一个股票行情通知系统,满足以下主要需求:

  • 股票价格更新:系统能够实时获取股票的最新价格信息,并在价格发生变化时进行准确记录和更新。股票价格数据可以从金融数据提供商的接口获取,或者通过模拟生成。
  • 多个客户端实时接收通知:系统支持多个客户端同时连接,当股票价格发生变化时,所有连接的客户端都能够及时收到通知,获取最新的股票价格。客户端可以是不同的投资机构、个人投资者使用的交易软件或行情展示终端。

4.2 观察者模式实现行情主题与客户端观察者

下面使用 C++ 代码实现基于观察者模式的股票行情通知系统:

#include <iostream>
#include <vector>
#include <memory>// 观察者接口
class StockObserver {
public:virtual void update(double newPrice) = 0;virtual ~StockObserver() {}
};// 主题类
class StockSubject {
private:std::vector<std::shared_ptr<StockObserver>> observers;double stockPrice;
public:StockSubject(double initialPrice) : stockPrice(initialPrice) {}void attach(std::shared_ptr<StockObserver> observer) {observers.push_back(observer);}void detach(const std::shared_ptr<StockObserver>& observer) {auto it = std::find(observers.begin(), observers.end(), observer);if (it != observers.end()) {observers.erase(it);}}void setPrice(double newPrice) {stockPrice = newPrice;notify();}double getPrice() const {return stockPrice;}void notify() {for (const auto& observer : observers) {observer->update(stockPrice);}}
};// 具体观察者类
class Investor : public StockObserver {
private:std::string name;
public:Investor(const std::string& investorName) : name(investorName) {}void update(double newPrice) override {std::cout << name << " received stock price update: " << newPrice << std::endl;}
};

在上述代码中:

  • StockObserver 是观察者接口,定义了 update 方法,当股票价格发生变化时,观察者通过这个方法接收最新价格并进行相应处理。
  • StockSubject 是主题类,维护了一个观察者的智能指针向量 observers ,以及当前股票价格 stockPrice。attach 方法用于添加观察者,detach 方法用于移除观察者,setPrice 方法用于更新股票价格并通知所有观察者,getPrice 方法用于获取当前股票价格 ,notify 方法用于通知所有观察者。
  • Investor 是具体观察者类,实现了 StockObserver 接口的 update 方法,在接收到股票价格更新通知时,输出投资者的名字和最新价格。

4.3 多客户端并发接收通知的测试

为了测试多客户端并发接收通知的情况,可以创建多个 Investor 对象并注册到 StockSubject 中,然后模拟股票价格的变化,观察各个客户端的通知接收情况。同时,考虑到多线程环境下可能出现的问题,如线程安全等,需要进行相应的处理和验证。

#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
#include <chrono>
#include <random>std::mutex mtx;
std::condition_variable cv;
std::queue<double> priceQueue;// 模拟获取股票价格的线程函数
void fetchStockPrice(StockSubject& stock) {std::random_device rd;std::mt19937 gen(rd());std::uniform_real_distribution<> dis(100.0, 120.0);while (true) {std::this_thread::sleep_for(std::chrono::seconds(2)); // 模拟每2秒获取一次价格double newPrice = dis(gen);{std::unique_lock<std::mutex> lock(mtx);priceQueue.push(newPrice);}cv.notify_one();}
}

测试代码如下:

int main() {StockSubject stock(100.0);std::shared_ptr<Investor> investor1 = std::make_shared<Investor>("Investor A");std::shared_ptr<Investor> investor2 = std::make_shared<Investor>("Investor B");std::shared_ptr<Investor> investor3 = std::make_shared<Investor>("Investor C");stock.attach(investor1);stock.attach(investor2);stock.attach(investor3);std::thread priceThread(fetchStockPrice, std::ref(stock));while (true) {std::unique_lock<std::mutex> lock(mtx);cv.wait(lock, [] { return!priceQueue.empty(); });double newPrice = priceQueue.front();priceQueue.pop();lock.unlock();stock.setPrice(newPrice);}priceThread.join();return 0;
}

在上述测试代码中:

  • fetchStockPrice 函数模拟了从外部获取股票价格的过程,使用随机数生成器生成在一定范围内的股票价格,并将其放入 priceQueue 队列中,通过条件变量 cv 通知主线程有新的价格到来。
  • 在 main 函数中,创建了三个 Investor 对象(模拟三个客户端)并注册到 StockSubject 中。启动 fetchStockPrice 线程后,主线程通过条件变量等待新价格,一旦有新价格,就调用 stock.setPrice 方法更新股票价格,从而触发对所有观察者的通知。

通过这样的测试,可以验证系统在多客户端并发情况下,是否能够准确、及时地将股票价格变化通知到各个客户端,以及在多线程环境下系统的稳定性和性能表现。如果在测试过程中发现问题,如通知丢失、顺序混乱或程序崩溃等,可以进一步分析和优化代码,例如加强线程同步机制、优化通知逻辑等。

http://www.dtcms.com/a/409869.html

相关文章:

  • 类和对象(二)
  • 开源多场景问答社区论坛Apache Answer本地部署并发布至公网使用
  • vue3 通过 Vue3DraggableResizable实现拖拽弹窗,可修改大小
  • 广州网站制作信科建设白名单 网站
  • DirBuster工具的文本介绍以及使用方法
  • DeepSeek-V3.1-Terminus:蓝耘API+CherryStudio实测国产最新开源模型,推理能力竟让我后背发凉
  • 金仓数据库:破解电子证照国产化难题,开启政务效能新篇
  • 杭州小蜜蜂网站建设宝坻做网站哪家好
  • 解析前端框架 Axios 的设计理念与源码:从 Promise 美学到双适配架构
  • MQTT 关键特性详解
  • 数据仓库与数据挖掘课程设计
  • 半导体数据分析:GPR算法小白入门(三) 晶体管I-V特性仿真教程
  • 深入理解 Qt 元对象系统:QMetaEnum 的应用与实践
  • html video标签mp4格式视频显示不出来的问题
  • Unity 虚拟仿真实验中设计模式的使用 ——策略模式(Strategy Pattern)
  • 企业级网站欣赏新乡个人网站建设
  • 设计模式——单例模式
  • C++设计模式之结构型模式:外观模式(Facade)
  • PaddleX服务化部署精度低于命令行调用的原因及解决方案
  • 新型域名前置攻击利用Google Meet、YouTube、Chrome及GCP构建流量隧道
  • 使用 C# 设置 Excel 单元格数据验证
  • python 做 网站移动互联网终端设备的主要技术指标是什么
  • Claude Code 的“AI优先”
  • 海外网站推广的公司app开发者需要更新
  • Unity-状态机复用
  • 沈阳铁西做网站公司成都移动网站建设
  • AI提示词应用
  • 【汽车篇】AI深度学习在汽车零部件外观检测——机电轴承的应用
  • 智能网联汽车技术仿真教学软件-沉浸式学习,实战化训练
  • 深圳市网站备案百度seo哪家公司好