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

【C++设计模式】观察者模式(1/2):从基础到优化实现

在这里插入图片描述

1. 引言

在 C++ 软件与设计系列课程中,观察者模式是一个重要的设计模式。本系列课程旨在深入探讨该模式的实现与优化。在之前的课程里,我们已对观察者模式有了初步认识,本次将在前两次课程的基础上,进一步深入研究,着重解决观察者生命周期问题,提升代码的安全性、灵活性、可维护性和扩展性。

2. 观察者模式基础回顾

2.1 基本概念

观察者模式包含主题(Subject)和观察者(Observer)两个核心概念。主题负责管理观察者列表,当主题发生有趣的事情时,会通知列表中的所有观察者。观察者则关注主题的状态变化,当收到通知时会做出相应的反应。

2.2 首次实现

首次实现中,我们创建了主题和观察者类。主题类可以添加、移除观察者,并在状态变化时通知所有观察者。使用 std::forward_list 存储观察者指针,通过遍历列表调用每个观察者的 notify 函数。示例代码创建了一个主题和三个观察者,展示了添加、通知和移除观察者的过程。

#include <iostream>
#include <forward_list>// 观察者类
class Observer {
public:virtual void notify() = 0;virtual ~Observer() = default;
};// 主题类
class Subject {
private:std::forward_list<Observer*> observers;
public:void addObserver(Observer* observer) {observers.push_front(observer);}void removeObserver(Observer* observer) {observers.remove(observer);}void notifyAll() {for (auto observer : observers) {observer->notify();}}
};// 具体观察者类
class ConcreteObserver : public Observer {
public:void notify() override {std::cout << "ConcreteObserver notified." << std::endl;}
};

2.3 首次实现的优缺点

优点是基本实现了观察者模式的功能,逻辑较为清晰。缺点是灵活性不足,若要创建更多的观察者和主题,需要创建不同的具体类,缺乏扩展性。

3. 改进实现:添加接口提升扩展性

3.1 改进思路

为了提高代码的灵活性和可扩展性,第二次实现为主题和观察者添加了接口。在 C++ 中,通过创建基类(类似抽象类)来实现接口的功能。

3.2 主题接口(ISubject)和观察者接口(IObserver)

// 观察者接口
class IObserver {
public:virtual void onNotify() = 0;virtual ~IObserver() = default;
};// 主题接口
class ISubject {
public:virtual void attach(IObserver* observer) = 0;virtual void detach(IObserver* observer) = 0;virtual void notifyAll() = 0;virtual ~ISubject() = default;
};

3.3 具体实现类

3.3.1 具体观察者类(Watcher)
#include <iostream>
#include <string>class Watcher : public IObserver {
private:std::string m_name;
public:explicit Watcher(const std::string& name) : m_name(name) {}void onNotify() override {std::cout << "Watcher - " << m_name << std::endl;}
};
3.3.2 具体主题类(SomeSubject)
#include <forward_list>class SomeSubject : public ISubject {
private:std::forward_list<IObserver*> m_observers;
public:void attach(IObserver* observer) override {m_observers.push_front(observer);}void detach(IObserver* observer) override {m_observers.remove(observer);}void notifyAll() override {for (auto observer : m_observers) {observer->onNotify();}}
};

3.4 测试代码

int main() {SomeSubject subject;Watcher watcher1("Watcher-1");Watcher watcher2("Watcher-2");Watcher watcher3("Watcher-3");subject.attach(&watcher1);subject.attach(&watcher2);subject.attach(&watcher3);subject.notifyAll();subject.detach(&watcher3);std::cout << std::endl;subject.notifyAll();return 0;
}

3.5 改进后的优点

通过使用接口,现在可以创建不同类型的主题和观察者类,只要它们继承自相应的接口并实现必要的函数。这使得代码更加灵活,可以轻松扩展以适应不同的需求。同时,接口的引入使得代码结构更加清晰,不同的功能被封装在不同的类中,提高了可维护性。

4. 解决观察者生命周期问题:利用 RAII 技术

4.1 问题提出

在现有代码中,若一个观察者超出作用域被销毁,但仍存在于主题的观察者列表中,当主题调用 notifyAll 时,会尝试访问已销毁的对象,从而导致运行时错误。

4.2 利用 RAII 解决问题

4.2.1 思路

RAII 是 C++ 的重要特性,通过对象的构造和析构自动管理资源。我们可以利用这一特性,在 Watcher 的构造函数中自动将其注册到主题,在析构函数中自动从主题移除,避免手动管理带来的遗漏和错误。

4.2.2 代码实现
#include <string>
#include "ISubject.h"class Watcher : public IObserver {
private:std::string m_name;ISubject& m_subject;
public:explicit Watcher(const std::string& name, ISubject& subject) : m_name(name), m_subject(subject) {m_subject.attach(this);}~Watcher() {m_subject.detach(this);}void onNotify() override {std::cout << "Watcher - " << m_name << std::endl;}
};
4.2.3 修改测试代码
#include "ISubject.h"
#include "IObserver.h"
#include "Watcher.h"
#include <iostream>int main() {SomeSubject subject;Watcher watcher1("Watcher-1", subject);Watcher watcher2("Watcher-2", subject);{Watcher watcher3("Watcher-3", subject);} // watcher3 自动从主题移除subject.notifyAll();return 0;
}

4.3 项目文件分离

在实现过程中,可能会遇到“不完整类型”的编译错误。为解决这个问题,我们将项目分离为不同的头文件和实现文件。将 IObserverWatcherISubjectSomeSubject 分别拆分为 .hpp 头文件和 .cpp 实现文件。在 main 函数中包含相应的头文件,确保编译器能够获取完整的类型信息。

4.4 测试改进后的代码

修改后的代码编译时不再报错,运行时也能正常工作。即使 Watcher 3 在新的作用域内创建和销毁,主题在通知时也不会出现运行时错误,因为 Watcher 3 已自动从主题的观察者列表中移除。

5. 总结与展望

5.1 总结

通过本次课程,我们从基础的观察者模式实现逐步优化,添加接口提升了代码的灵活性和可维护性,利用 RAII 技术解决了观察者生命周期问题,提高了代码的安全性。关键在于理解观察者模式的核心概念,掌握接口的使用和 RAII 技术的应用。

5.2 展望

当前代码使用了原始指针,可考虑使用智能指针(如 std::unique_ptr)进一步优化,避免内存泄漏。后续课程将继续为观察者模式添加更多功能,完善该设计模式的实现。希望大家能将这些知识应用到实际项目中,提升代码质量。

相关文章:

【C++设计模式】观察者模式(1/2):从基础到优化实现

1. 引言 在 C 软件与设计系列课程中&#xff0c;观察者模式是一个重要的设计模式。本系列课程旨在深入探讨该模式的实现与优化。在之前的课程里&#xff0c;我们已对观察者模式有了初步认识&#xff0c;本次将在前两次课程的基础上&#xff0c;进一步深入研究&#xff0c;着重…...

《机器学习数学基础》补充资料:欧几里得空间的推广

在《机器学习数学基础》第 1 章介绍了向量空间&#xff0c;并且说明了机器学习问题通常是在欧几里得空间。然而&#xff0c;随着机器学习技术的发展&#xff0c;特别是 AI 技术开始应用于科学研究中&#xff0c;必然会涉及到其他类型的空间。本文即在《机器学习数学基础》一书所…...

在配置PX4中出现的问题2

想要原教程的请看&#xff1a;第一次配置中出现的问题 前面一切正常&#xff08;gazebo导入models那一步在刚刚解压好的文件夹里就删不掉stereo_camera等文件&#xff0c;ls打开也看不到&#xff0c;应该时我下的包里面本来就没有&#xff09;&#xff0c;到 make px4_sitl_def…...

2025-2-24-4.9 单调栈与单调队列(基础题)

文章目录 4.9 单调栈与单调队列&#xff08;基础题&#xff09;单调栈739. 每日温度42. 接雨水单调队列239. 滑动窗口最大值 4.9 单调栈与单调队列&#xff08;基础题&#xff09; 很有趣的两个数据结构。 原视频讲解链接 单调栈 739. 每日温度 题目链接 给定一个整数数组 te…...

python绘图之swarmplot分布散点图

swarmplot 是 Seaborn 提供的一种用于展示分类数据分布的散点图。它的主要作用是将数据点按照分类变量&#xff08;通常是离散变量&#xff09;进行分组&#xff0c;并在每个分类中以一种非重叠的方式展示数据点的位置。这种可视化方式可以帮助我们直观地理解数据在不同分类下的…...

数据库之MySQL——事务(一)

1、MySQL之事务的四大特性(ACID)&#xff1f; 原子性(atomicity)&#xff1a;一个事务必须视为一个不可分割的最小工作单元&#xff0c;整个事务中的所有操作要么全部提交成功&#xff0c;要么全部失败回滚&#xff0c;对于一个事务来说&#xff0c;不可能只执行其中的一部分操…...

Linux学习笔记之文件

1.文件 1.1文件属性 当我们创建文件时&#xff0c;文件就有了对应的属性&#xff0c;可以用mkdir创建目录&#xff0c;touch创建普通文件。用ls -al查看文件属性。 从上图可以看出目录或者文件的所有者&#xff0c;所属组&#xff0c;其他人权限&#xff0c;创建时间等信息。由…...

LLM学习

1、基础概念篇 大模型训练三部曲Pretraining SFT RLHF...

Classic Control Theory | 13 Complex Poles or Zeros (第13课笔记-中文版)

笔记链接&#xff1a;https://m.tb.cn/h.TtdexbP?tkeFAlejKBSzQhttps://m.tb.cn/h.TtdexbP?tkeFAlejKBSzQ...

给小米/红米手机root(工具基本为官方工具)——KernelSU篇

目录 前言准备工作下载刷机包xiaomirom下载刷机包【适用于MIUI和hyperOS】“hyper更新”微信小程序【只适用于hyperOS】 下载KernelSU刷机所需程序和驱动文件 开始刷机设置手机第一种刷机方式【KMI】推荐提取boot或init_boot分区 第二种刷机方式【GKI】不推荐 结语 前言 刷机需…...

【MySQL】表的增删查改(CRUD)(上)

个人主页&#xff1a;♡喜欢做梦 欢迎 &#x1f44d;点赞 ➕关注 ❤️收藏 &#x1f4ac;评论 CRUD&#xff1a;Create&#xff08;新增数据&#xff09;、Retrieve&#xff08;查询数据&#xff09;、Update&#xff08;修改数据&#xff09;、Delete&#xff08;修改数据…...

测试用例的Story是什么?

测试用例的 Story&#xff08;用户故事&#xff09;是指描述某个功能或场景的具体用户需求&#xff0c;它通常以简短的业务背景用户操作期望结果的方式呈现&#xff0c;使测试人员能够理解测试的目标和价值。用户故事能够帮助团队更好地设计测试用例&#xff0c;确保功能满足用…...

15.4 FAISS 向量数据库实战:构建毫秒级响应的智能销售问答系统

FAISS 向量数据库实战:构建毫秒级响应的智能销售问答系统 关键词:FAISS 向量数据库、销售知识库构建、相似度检索优化、大规模问答匹配、量化索引技术 1. 销售问答场景的向量化挑战与解决方案 1.1 传统检索方案痛点分析 #mermaid-svg-AeVgih79asJb7lb8 {font-family:"…...

Golang笔记——Interface类型

大家好&#xff0c;这里是&#xff0c;关注 公主号&#xff1a;Goodnote&#xff0c;专栏文章私信限时Free。本文详细介绍Golang的interface数据结构类型&#xff0c;包括基本实现和使用等。 文章目录 Go 语言中的 interface 详解接口定义实现接口空接口 interface{} 示例&…...

如何查看图片的原始格式

问题描述&#xff1a;请求接口的时候&#xff0c;图片base64接口报错&#xff0c;使用图片url请求正常 排查发现是图片格式的问题&#xff1a; 扩展名可能被篡改&#xff1a;如果文件损坏或扩展名被手动修改&#xff0c;实际格式可能与显示的不同&#xff0c;需用专业工具验证…...

FreiHAND (handposeX-json 格式)数据集-release >> DataBall

FreiHAND &#xff08;handposeX-json 格式&#xff09;数据集-release 注意&#xff1a; 1)为了方便使用&#xff0c;按照 handposeX json 自定义格式存储 2)使用常见依赖库进行调用,降低数据集使用难度。 3)部分数据集获取请加入&#xff1a;DataBall-X数据球(free) 4)完…...

【Rust中级教程】2.8. API设计原则之灵活性(flexible) Pt.4:显式析构函数的问题及3种解决方案

喜欢的话别忘了点赞、收藏加关注哦&#xff08;加关注即可阅读全文&#xff09;&#xff0c;对接下来的教程有兴趣的可以关注专栏。谢谢喵&#xff01;(&#xff65;ω&#xff65;) 说句题外话&#xff0c;这篇文章一共5721个字&#xff0c;是我截至目前写的最长的一篇文章&a…...

LabVIEW Browser.vi 库说明

browser.llb 库位于C:\Program Files (x86)\National Instruments\LabVIEW 2019\vi.lib\Platform目录&#xff0c;它是 LabVIEW 平台下用于与网络浏览器相关操作的重要库。该库为 LabVIEW 开发者提供了一系列工具&#xff0c;用于实现网页浏览控制、网页数据获取与交互等功能&a…...

promise的方法有哪些?【JavaScript】

Promise对象在JavaScript中是一种处理异步操作的方式&#xff0c;它提供了一组方法来管理和控制异步操作的结果。以下是一些常用的Promise方法&#xff1a; 以下是对 constructor(executor)‌、then(onFulfilled, onRejected&#xff09;、catch(onRejected)‌、 finally(onFin…...

基于模仿学习(IL)的端到端自动驾驶发展路径

基于模仿学习&#xff08;IL&#xff09;的端到端自动驾驶发展路径 1. 核心论文解析 (1) UniAD&#xff1a;感知-规划一体化 核心思想&#xff1a;首次提出将感知任务&#xff08;如目标检测、车道线识别、轨迹预测&#xff09;与规划任务集成到统一的端到端框架中&#xff…...

前端倒计时误差!

提示:记录工作中遇到的需求及解决办法 文章目录 前言一、误差从何而来?二、五大解决方案1. 动态校准法(基础版)2. Web Worker 计时3. 服务器时间同步4. Performance API 高精度计时5. 页面可见性API优化三、生产环境最佳实践四、终极解决方案架构前言 前几天听说公司某个项…...

基于服务器使用 apt 安装、配置 Nginx

&#x1f9fe; 一、查看可安装的 Nginx 版本 首先&#xff0c;你可以运行以下命令查看可用版本&#xff1a; apt-cache madison nginx-core输出示例&#xff1a; nginx-core | 1.18.0-6ubuntu14.6 | http://archive.ubuntu.com/ubuntu focal-updates/main amd64 Packages ng…...

React---day11

14.4 react-redux第三方库 提供connect、thunk之类的函数 以获取一个banner数据为例子 store&#xff1a; 我们在使用异步的时候理应是要使用中间件的&#xff0c;但是configureStore 已经自动集成了 redux-thunk&#xff0c;注意action里面要返回函数 import { configureS…...

高效线程安全的单例模式:Python 中的懒加载与自定义初始化参数

高效线程安全的单例模式:Python 中的懒加载与自定义初始化参数 在软件开发中,单例模式(Singleton Pattern)是一种常见的设计模式,确保一个类仅有一个实例,并提供一个全局访问点。在多线程环境下,实现单例模式时需要注意线程安全问题,以防止多个线程同时创建实例,导致…...

腾讯云V3签名

想要接入腾讯云的Api&#xff0c;必然先按其文档计算出所要求的签名。 之前也调用过腾讯云的接口&#xff0c;但总是卡在签名这一步&#xff0c;最后放弃选择SDK&#xff0c;这次终于自己代码实现。 可能腾讯云翻新了接口文档&#xff0c;现在阅读起来&#xff0c;清晰了很多&…...

Caliper 负载(Workload)详细解析

Caliper 负载(Workload)详细解析 负载(Workload)是 Caliper 性能测试的核心部分,它定义了测试期间要执行的具体合约调用行为和交易模式。下面我将全面深入地讲解负载的各个方面。 一、负载模块基本结构 一个典型的负载模块(如 workload.js)包含以下基本结构: use strict;/…...

go 里面的指针

指针 在 Go 中&#xff0c;指针&#xff08;pointer&#xff09;是一个变量的内存地址&#xff0c;就像 C 语言那样&#xff1a; a : 10 p : &a // p 是一个指向 a 的指针 fmt.Println(*p) // 输出 10&#xff0c;通过指针解引用• &a 表示获取变量 a 的地址 p 表示…...

论文阅读:Matting by Generation

今天介绍一篇关于 matting 抠图的文章&#xff0c;抠图也算是计算机视觉里面非常经典的一个任务了。从早期的经典算法到如今的深度学习算法&#xff0c;已经有很多的工作和这个任务相关。这两年 diffusion 模型很火&#xff0c;大家又开始用 diffusion 模型做各种 CV 任务了&am…...

高抗扰度汽车光耦合器的特性

晶台光电推出的125℃光耦合器系列产品&#xff08;包括KL357NU、KL3H7U和KL817U&#xff09;&#xff0c;专为高温环境下的汽车应用设计&#xff0c;具备以下核心优势和技术特点&#xff1a; 一、技术特性分析 高温稳定性 采用先进的LED技术和优化的IC设计&#xff0c;确保在…...

RushDB开源程序 是现代应用程序和 AI 的即时数据库。建立在 Neo4j 之上

一、软件介绍 文末提供程序和源码下载 RushDB 改变了您处理图形数据的方式 — 不需要 Schema&#xff0c;不需要复杂的查询&#xff0c;只需推送数据即可。 二、Key Features ✨ 主要特点 Instant Setup: Be productive in seconds, not days 即时设置 &#xff1a;在几秒钟…...