C++设计模式系列文章目录

【C++设计模式】第一篇 C++单例模式–懒汉与饿汉以及线程安全

【C++设计模式】第二篇:策略模式(Strategy)–从基本介绍,内部原理、应用场景、使用方法,常见问题和解决方案进行深度解析


【C++设计模式】第三篇:观察者模式(别名:发布-订阅模式、模型-视图模式、源-监听器模式)

  • C++设计模式系列文章目录
  • 一、观察者模式简介
  • 二、解决的问题类型
  • 三、使用场景
  • 四、核心概念
  • 五、实现方式
    • 5.1 基础方式
    • 5.2 改进观察者模式
    • 5.3 总结
  • 八、最终小结
    • 一句话总结(发布-订阅模式):


原文链接:https://blog.csdn.net/weixin_44131922/article/details/149515925

一、观察者模式简介

观察者模式(Observer Pattern) 是一种 行为型设计模式(对象行为型模式),它定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。当主题对象发生变化时,它的所有依赖者(观察者)都会收到通知并自动更新。

你可以把它想象成“订阅-发布”机制:当你订阅了一份杂志后,每当有新一期出版时,你就会收到通知。这里的“你”就是观察者,“杂志”则是被观察的主题。

在这里插入图片描述

“红灯停,绿灯行”。在这个过程中,交通信号灯是汽车的观察目标,而汽车则是观察者。随着交通信号灯的变化,汽车的行为也随之变化,一盏交通信号灯可以指挥多辆汽车。

在软件系统中有些对象之间也存在类似交通信号灯和汽车之间的关系,一个对象的状态或行为的变化将导致其他对象的状态或行为也发生改变。观察者模式(Observer Pattern)则是用于建立一种对象与对象之间的依赖关系,使得每当一个对象状态发生改变时其相关依赖对象皆得到通知并被自动更新。发生改变的对象称为观察目标,被通知的对象称为观察者,一个观察目标可以对应多个观察者。它有如下别名:

  • 发布-订阅(Publish/Subscribe)模式
  • 模型-视图(Model/View)模式
  • 源-监听器(Source/Listener)模式
  • 从属者(Dependents)模式

二、解决的问题类型

观察者模式主要用于解决以下问题:

  • 状态变化需要通知相关联的对象:例如,用户界面中的模型数据发生变化时,视图需要自动更新。需要在系统中创建一个触发链。
  • 避免紧耦合:允许对象之间保持松散的联系,无需直接相互引用。一个抽象模型有两个方面,其中一个方面依赖于另一个方面,将这两个方面封装在独立的对象中使它们可以各自独立地改变和复用。
  • 实现广播通信:可以向所有注册的观察者发送消息,而不需要知道它们的具体身份。

三、使用场景

在这里插入图片描述

四、核心概念

Subject(主题/被观察者):维护了一个观察者列表,并提供方法供观察者注册或移除自身;在状态改变时通知所有观察者。
Observer(观察者):接收来自主题的通知并作出响应。
ConcreteSubject & ConcreteObserver:具体实现上述两个接口的实际类。

五、实现方式

原文链接:https://blog.csdn.net/hzdxyh/article/details/141126923

  • Observer(观察者):它是一个抽象类或接口,为所有的具体观察者定义一个更新接口,使得在得到主题的通知时更新自己。
  • Subject(主题):它维护了一系列依赖于它的Observer对象,并提供一个接口来允许Observer对象注册自己、注销自己以及通知它们。
  • ConcreteObserver(具体观察者):它实现了Observer接口,存储与Subject的状态自洽的状态。具体观察者根据需要实现Subject的更新接口,以使得自身状态与主题的状态保持一致。
  • ConcreteSubject(具体主题):它实现了Subject接口,将有关状态存入具体观察者对象,并在状态发生改变时向Observer发出通知。
    在这里插入图片描述

5.1 基础方式

按定义实现最基础的观察者模式功能:

/*** @brief 定义一个Observer抽象基类*/
class IObserver {public:IObserver() {}virtual ~IObserver() {}public:virtual void Update(int data) = 0;
};/*** @brief 定义一个Subject抽象基类*/
class ISubject {public:ISubject() {}virtual ~ISubject() {}public:virtual void Subscribe(std::shared_ptr<IObserver> observer) = 0;  // 观察者订阅事件virtual void Unsubscribe(std::shared_ptr<IObserver> observer) = 0;  // 观察者取消事件的订阅virtual void Notify(int data) = 0;  // 通知已订阅指定事件的观察者public:std::list<std::weak_ptr<IObserver>> observers_;  // 存放所有已订阅的observer
};/*** @brief 实现一个具体的观察者*/
class ConcreteObserver : public IObserver {public:ConcreteObserver(const std::string& name) : name_(name) {}virtual ~ConcreteObserver() override {}public:void Update(int data) override {std::cout << "observer [" << name_ << "] updated -> " << data << std::endl;}private:std::string name_;
};/*** @brief 实现一个具体的Subject*/
class ConcreteSubject : public ISubject {public:ConcreteSubject() {}virtual ~ConcreteSubject() override {}public:void Subscribe(std::shared_ptr<IObserver> observer) override {observers_.push_back(observer);};void Unsubscribe(std::shared_ptr<IObserver> observer) override {observers_.erase(std::remove_if(observers_.begin(), observers_.end(),[&observer](std::weak_ptr<IObserver> obj) {std::shared_ptr<IObserver> tmp =obj.lock();if (tmp != nullptr) {return tmp == observer;} else {return false;}}),observers_.end());}void Notify(int data) override {for (auto it = observers_.begin(); it != observers_.end(); ++it) {std::shared_ptr<IObserver> ps = it->lock();// weak_ptr提升为shared_ptr// 判断对象是否还存活if (ps != nullptr) {ps->Update(data);} else {it = observers_.erase(it);}}}
};// 测试
int main() {// 构造3个观察者对象std::shared_ptr<IObserver> observer1(new ConcreteObserver("observer1"));std::shared_ptr<IObserver> observer2(new ConcreteObserver("observer2"));std::shared_ptr<IObserver> observer3(new ConcreteObserver("observer3"));// 构造1个主题对象ConcreteSubject subject;// 为观察者订阅事件subject.Subscribe(observer1);subject.Subscribe(observer2);subject.Subscribe(observer3);// 通知订阅事件的观察者subject.Notify(10);// 模拟取消订阅subject.Unsubscribe(observer1);// 通知订阅事件的观察者subject.Notify(20);return 0;
}

控制台输出:

observer [observer1] updated -> 10
observer [observer2] updated -> 10
observer [observer3] updated -> 10
observer [observer2] updated -> 20
observer [observer3] updated -> 20

注意:在ISubject 中维护了一个已订阅的observer的list,在list中存放了observer对象的指针,在很多例子中list中直接存放observer的裸指针,在notify通知所有observer时,需要遍历list,调用每个observer的update接口,在多线程环境中,肯定不明确此时observer对象是否还存活,或是已经在其它线程中被析构了。本例这里使用了weak_ptr和shared_ptr替代observer裸指针,解决上述问题,同时还能避免循环引用

从上面的例子中可以看出:Observer是不依赖于Subject的,想要增加一个新的Observer只需要继承IObserver即可,无需修改Subject,这符合开闭原则,也实现了Observer与Subject的解耦。

5.2 改进观察者模式

上面例子中的观察者模式是经典模式,但是存在缺陷:

  • 需要继承,继承是强对象关系,只能对特定的观察者才有效,即必须是Observer抽象类的派生类才行;

  • 观察者被通知的接口参数不支持变化,导致观察者不能应付接口的变化
    为了解决上例观察者模式的缺陷,可使用C++11 做出改进

  • 通过被通知接口参数化和std::function 来代替继承;

  • 通过可变参数模板和完美转发来消除接口变化产生的影响。

下面以一个具体的场景举例:

  1. 有一个数据中心,数据中心负责从其他地方获取数据,并对数据进行加工处理
  2. 有一个图表组件,需要从数据中心拿数据进行可视化展示
  3. 有一个文本组件,需要从数据中心拿数据进行可视化展示
  4. 后期可能还有更多的组件,需要从数据中心拿数据…
/*** @brief 实现一个具体的Subject,模拟一个数据中心*/
template <typename Func>
class Subject {public:static Subject& GetInstance() {static Subject instance;return instance;}public:// 注册观察者,右值引用int Subscribe(Func&& f) { return Assign(f); }// 注册观察者,左值int Subscribe(const Func& f) { return Assign(f); }// 移除观察者void Unsubscribe(int id) { observers_map_.erase(id); }// 通知所有观察者template <typename... Args>void Notify(Args&&... args) {for (auto& it : observers_map_) {auto& func = it.second;func(std::forward<Args>(args)...);}}private:template <typename F>int Assign(F&& f) {int id = observer_id_++;observers_map_.emplace(id, std::forward<F>(f));return id;}private:Subject() = default;~Subject() = default;Subject(const Subject&) = delete;Subject& operator=(const Subject&) = delete;Subject(Subject&&) = delete;Subject& operator=(Subject&&) = delete;int observer_id_ = 0;                //观察者对应编号std::map<int, Func> observers_map_;  // 观察者列表
};/*** @brief 实现一个具体的观察者,模拟图表展示组件*/
class ChartView {public:ChartView() {}~ChartView() {}public:void Update(int data) {std::cout << "the chart data has been updated to [" << data << "]" << std::endl;}
};/*** @brief 实现一个具体的观察者,模拟文本展示组件*/
class TextView {public:TextView() {}~TextView() {}public:void Update(std::string key, std::string value) {std::cout << "the text data has been updated to [" << key << ": " << value << "]" << std::endl;}
};// 测试程序
int main(void) {ChartView cv;  // 图表展示组件// 从数据中心订阅Subject<std::function<void(int)>>::GetInstance().Subscribe(std::bind(&ChartView::Update, cv, std::placeholders::_1));TextView tv;  // 文本展示组件// 从数据中心订阅Subject<std::function<void(std::string, std::string)>>::GetInstance().Subscribe(std::bind(&TextView::Update, tv, std::placeholders::_1, std::placeholders::_2));// 这里模拟一个数据处理中心线程std::thread([]() {int cnt = 0;while (1) {// 模拟数据获取数据处理过程std::this_thread::sleep_for(std::chrono::seconds(1));auto time = std::chrono::duration_cast<std::chrono::seconds>(std::chrono::system_clock::now().time_since_epoch()).count();// 数据处理完成后,通知组件Subject<std::function<void(int)>>::GetInstance().Notify(cnt++);Subject<std::function<void(std::string, std::string)>>::GetInstance().Notify("time", std::to_string(time));}}).detach();// 主线程while (1) {std::cout << "main run ..." << std::endl;std::this_thread::sleep_for(std::chrono::seconds(5));}return 0;
}
main run ...
the chart data has been updated to [0]
the text data has been updated to [time: 1723446799]
the chart data has been updated to [1]
the text data has been updated to [time: 1723446800]
the chart data has been updated to [2]
the text data has been updated to [time: 1723446801]
the chart data has been updated to [3]
the text data has been updated to [time: 1723446802]
main run ...
the chart data has been updated to [4]
the text data has been updated to [time: 1723446803]

在本例中,将Subject改为单例模式,这样在组件中调用注册接口,在数据中心调用通知接口,完全解耦分离;图表组件和文本组件所需的数据个数和数据类型是不同的,这在基础的观察者模式中是无法实现的,改进后的观察者模式脱离了需要继承的约束,可以实现更加通用的功能,后期扩展更多组件时,不需要修改Subject代码,只需要新增Observer即可。

5.3 总结

观察者模式的优点主要包括:
解耦:观察者和被观察的对象是抽象耦合的,即它们之间不直接调用,而是通过消息传递来通知。
灵活性:可以在运行时动态地添加或删除观察者。
复用性:观察者模式可以单独地重用主题和观察者。
缺点
开销:如果观察者非常多,那么更新的效率就会比较低,因为需要遍历所有的观察者,并调用它们的更新方法。
在这里插入图片描述

八、最终小结

观察者模式是一种非常实用的设计模式,特别适合那些需要在不同组件之间维持松散耦合并且能够响应状态变化的应用场景。通过合理地运用观察者模式,我们可以在不破坏原有模块独立性的前提下,有效地实现组件间的协作与通信。

在开发图形用户界面(GUI)、实时数据监控系统、消息中间件等项目时,观察者模式能够帮助你更好地组织代码结构,提高系统的灵活性和可维护性。

一句话总结(发布-订阅模式):

观察者模式就像一个新闻通讯社,一旦有新的新闻发布,所有订阅了该新闻的人都会立即收到通知。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/news/921715.shtml
繁体地址,请注明出处:http://hk.pswp.cn/news/921715.shtml
英文地址,请注明出处:http://en.pswp.cn/news/921715.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

运作管理学习笔记5-生产和服务设施的选址

运作管理-北京交通大学5.1.设施选址概述 设施选址是一个战略性的决策&#xff0c;做这个决策的时候会投入比较多的资源&#xff0c;而且未来去改变选址的成本和代价也比较大。 5.1.1.设施选址的重要性 设施选址影响企业经营情况 设施选址对设施布局以及投产后的生产经营费用、产…

JUnit 详解

一、JUnit 简介&#xff1a;什么是 JUnit&#xff1f;为什么要用它&#xff1f;1.1 核心定义JUnit 是一个开源的、基于 Java 语言的单元测试框架&#xff0c;最初由 Erich Gamma (GoF 设计模式作者之一) 和 Kent Beck (极限编程创始人) 在 1997 年共同开发。作为 xUnit 测试框架…

数据结构造神计划第三天---数据类型

&#x1f525;个人主页&#xff1a;寻星探路 &#x1f3ac;作者简介&#xff1a;Java研发方向学习者 &#x1f4d6;个人专栏&#xff1a;《从青铜到王者&#xff0c;就差这讲数据结构&#xff01;&#xff01;&#xff01;》、 《JAVA&#xff08;SE&#xff09;----如此简单&a…

AI API Tester体验:API测试工具如何高效生成接口测试用例、覆盖异常场景?

前阵子帮后端测试支付接口时&#xff0c;我算是彻底明白 “API 测试能磨掉半条命”—— 明明接口文档里写了十几种参数组合&#xff0c;手动写测试用例时要么漏了 “签名过期” 的场景&#xff0c;要么忘了校验 “金额超过限额” 的返回值&#xff0c;测到半夜还被开发吐槽 “你…

音频驱动数字人人脸模型

1.LatentSync: Taming Audio-Conditioned Latent Diffusion Models for Lip Sync with SyncNet Supervision 字节 2024 文章地址&#xff1a;https://arxiv.org/pdf/2412.09262 代码地址&#xff1a;https://github.com/bytedance/LatentSync 训练推理都有 2.wan2.2-s2v …

CentOS部署ELK Stack完整指南

文章目录&#x1f680; ELK Stack 部署详解&#xff08;CentOS 7/8&#xff09;&#x1f4e6; 一、环境准备1. 关闭防火墙&#xff08;或开放端口&#xff09;2. 关闭 SELinux3. 安装基础依赖4. 验证 Java&#x1f53d; 二、下载并安装 ELK 组件1. 导入 Elastic GPG 密钥2. 创建…

Spring Boot 拦截器(Interceptor)与过滤器(Filter)有什么区别?

在 Spring Boot 项目中&#xff0c;我们经常会遇到需要在请求处理前后执行一些通用逻辑的场景&#xff0c;比如记录日志、权限校验、全局异常处理等。此时&#xff0c;我们通常会面临两种选择&#xff1a;过滤器&#xff08;Filter&#xff09; 和 拦截器&#xff08;Intercept…

【技术教程】如何将文档编辑器集成至基于Java的Web应用程序

在如今的企业协作场景中&#xff0c;“文档” 早已不是简单的文字载体&#xff01;从项目需求文档的多人实时修改&#xff0c;到财务报表的在线批注&#xff0c;再到合同草案的版本追溯&#xff0c;用户越来越需要在 Web 应用内直接完成 “编辑 - 协作 - 存储” 全流程。 但很…

多模态大模型Keye-VL-1.5发布!视频理解能力更强!

近日&#xff0c;快手正式发布了多模态大语言模型Keye-VL-1.5-8B。 与之前的版本相比&#xff0c;Keye-VL-1.5的综合性能实现显著提升&#xff0c;尤其在基础视觉理解能力方面&#xff0c;包括视觉元素识别、推理能力以及对时序信息的理—表现尤为突出。Keye-VL-1.5在同等规模…

洗完头后根据个人需求选择合适的自然风干 | 电吹风 (在保护发质的同时,也能兼顾到生活的便利和舒适。)

文章目录 引言 I 选合适的方式让头发变干 时间充裕,不需要做造型,选择自然风干 使用电吹风,比较推荐的做法 II 自然风干 天冷可能刺激头皮 III 电吹风吹干 容易造型 影响头皮健康 损伤发质 科普 头皮的微观结构 头发丝 引言 吹风吹干:容易造型,但损伤发质、影响头皮健康 …

GPS汽车限速器有哪些功能?主要运用在哪里?

GPS 汽车限速器是一种结合全球卫星定位&#xff08;GPS&#xff09;技术、车速采集技术与车辆控制 / 预警逻辑的设备&#xff0c;核心目标是通过技术手段限制车辆行驶速度&#xff0c;减少超速引发的交通事故&#xff0c;并辅助车辆管理。其功能与应用场景高度匹配不同用户的 “…

Python从入门到精通_01_python基础

1 源代码格式在python文件的第一行&#xff0c;输入以下语句&#xff0c;可以将python文件的编码格式设置为utf-8#-*- coding:utf-8 -*-2 输入输出input():输入&#xff0c;无论输入的是什么类型数据&#xff0c;最后都是字符串类型print(*args, sep , end\n, fileNone, flushF…

使用CI/CD部署项目(前端Nextjs)

写在前面&#xff1a;在github上使用CI/CD部署Nextjs项目&#xff0c;具体配置可以按照自己的实际的修改 这是我的项目配置&#xff0c;仅供参考 后端项目可以参考&#xff1a;使用CI/CD部署后端项目 正文开始 项目名&#xff08;PROJECT_NAME&#xff09;- CI/CD 部署指南…

Java全栈工程师面试实录:从基础到实战的全面解析

Java全栈工程师面试实录&#xff1a;从基础到实战的全面解析 面试官&#xff1a;李明&#xff08;资深技术负责人&#xff09; 应聘者&#xff1a;张宇&#xff08;28岁&#xff0c;硕士学历&#xff0c;5年开发经验&#xff09; 第一轮&#xff1a;Java语言与JVM基础 李明&…

C#中解析XML时遇到注释节点报错

在C#中解析XML时遇到注释节点报错的问题&#xff0c;这是因为XML注释节点&#xff08;<!-- -->&#xff09;是特殊的节点类型。当遍历XML节点时&#xff0c;注释节点也会被包含在内&#xff0c;但它们不能像普通元素节点那样处理。 解决方案 方法1&#xff1a;跳过注释节…

9.3深度循环神经网络

目前为止&#xff0c;只讨论了具有一个单向隐藏层的循环神经网络&#xff0c;其中隐变量和观测值域具体的函数形式的交互方式是相当随意的。只要交互类型建模具有足够的灵活性&#xff0c;不是一个单问题。然而&#xff0c;对一个单层来说&#xff0c;可能具有相当的挑战性。之…

CSS in JS 的演进:Styled Components, Emotion 等的深度对比与技术选型指引

CSS in JS 的演进&#xff1a;Styled Components, Emotion 等的深度对比与技术选型指引在现代前端开发中&#xff0c;组件化思维已成为主流&#xff0c;而如何科学、高效地管理组件的样式&#xff0c;也随之成为了一个重要议题。CSS in JS&#xff08;JS中的CSS&#xff09;应运…

【正则表达式】 正则表达式的分组和引用

🌈 个人主页:(时光煮雨) 🔥 高质量专栏:vulnhub靶机渗透测试 👈 希望得到您的订阅和支持~ 💡 创作高质量博文(平均质量分95+),分享更多关于网络安全、Python领域的优质内容!(希望得到您的关注~) 🌵目录🌵 前言 🍱一、基本语法 🍘二、分组类型 🍙2.1.…

Grafana 导入仪表盘失败:从日志排查到解决 max\_allowed\_packet 问题

问题背景 近期在为项目搭建一套基于 Prometheus 和 Grafana 的可观测性体系。在完成基础部署后&#xff0c;我准备导入一个功能相对复杂的官方仪表盘模板&#xff0c;以便快速监控各项指标。然而&#xff0c;当上传仪表盘的 JSON 文件并点击保存时&#xff0c;Grafana 界面却反…

java对接物联网设备(一)——使用okhttp网络工具框架对接标准API接口

当前无论是在互联网领域&#xff0c;还是物联网项目下&#xff0c;亦或者各类应用类软件&#xff0c;基于http标准接口的对接是目前市面上最常见也是最简单的数据交互方式之一&#xff0c;甚至可以说是最流行的&#xff0c;因为它不依赖的各种插件或者服务。 开发者或者提供服…