【设计模式】如何用C++实现观察者模式【发布订阅机制】
【设计模式】如何用C++实现观察者模式【发布订阅机制】
一、问题背景
代码质量影响生活质量。最近工作中频繁接触各种设计模式,深刻体会到优秀的设计模式不仅能显著降低后续维护的压力,还能提升开发效率。观察者模式作为一种降低耦合度、提高扩展性的利器,其设计模式思想让人受益匪浅。
过去曾读过一本《练习的心态》,书中提出了这样的问题:当我们专注于过程时,往往会偏离目标,越专注离目标也就越远;当我们关注目标时,要么无法专注于过程,要么因目标与现实的差距而产生放弃的念头。
如何把握过程与目标的关系,也许需要我们用一种观察者的智慧和心态,即同时使用两个视角来思考:
- 第一种视角作为被观察者,允许自身专注,沉浸于过程,最好进入心流状态。
- 第二种视角作为观察者,观察者观察沉浸于过程的执行者,时常检查执行者是否偏离目标,然后提醒执行者调整策略。
当掌握这两种视角后,做到极致的情况下,自身始终存在一种理性的观察者视角:当自身愤怒时,观察者能提供一种理性的视角,以避免行为造成无法接受的后果。如果能做到这一点,仅仅是愤怒的话又有何不可呢。
观察者模式中这两种视角是低耦合的,而作为普通人,难免经常会将两种视角混淆在一起。
”破山中贼易,破心中贼难“,要让心中始终存在一种理性的智慧,也许还需要在事上学、心上练。把握人性的度量,从而实现“从心所欲而不逾矩”的理想人格。
二、什么是观察者模式?
观察者模式(Observer Pattern)是一种常见的设计模式,属于行为型模式。它定义了一种一对多的依赖关系,使得当一个对象的状态发生改变时,其所有依赖对象(观察者)都会自动收到通知并更新。这种模式常用于事件驱动的系统。
观察者模式的核心概念:
- 被观察者(Subject):也称为发布者,它维护着一个观察者列表,当它的状态发生变化时,会通知所有观察者。
- 观察者(Observer):也称为订阅者,它订阅了某个被观察者,当被观察者状态发生变化时,它会收到通知并执行相应的操作。
三、观察者模式工作原理
-
注册观察者: 观察者向被观察者注册,把自己添加到被观察者的观察者列表中。
-
状态改变: 被观察者的状态发生变化。
-
通知观察者: 被观察者遍历观察者列表,依次通知所有注册的观察者。
-
更新: 观察者收到通知后,根据自己的逻辑进行更新。
四、为什么使用观察者模式?
-
低耦合: 被观察者和观察者之间是低耦合的,它们之间不需要知道对方的具体实现细节。
-
可扩展性: 可以动态地增加或删除观察者,而不需要修改被观察者或其他观察者的代码。
-
复用性: 观察者模式可以被应用于各种场景,提高代码的复用性。
五、实现步骤
以发布订阅为例,需要实现以下三个组件:
- Publisher:用于发布订阅事件,承担了观察者模式中的Subject的状态改变功能。
- TopicServer:用于中心化管理Topic订阅事件和Subscriber观察者列表,承担了中的Subject的通知功能。
- Subscriber:用于订阅Topic,当发布者Publisher发布消息时,订阅服务器TopicServer会通知所有订阅该Topic的订阅者Subscriber。
1. 订阅者类Subscriber
./subscribe/Subscribe.h
#ifndef SUBSCRIBE_H
#define SUBSCRIBE_H
#include <string>
namespace Observer
{class Subscriber{public:virtual ~Subscriber() = default;virtual void Update(const std::string &topic, const std::string &message) = 0;};
}
#endif
./subscribe/SubscribeImpl.h
#ifndef SUBSCRIBEIMPL_H
#define SUBSCRIBEIMPL_H
#include <Subscriber.h>
namespace Observer
{class SubscriberImpl : public Subscriber{public:explicit SubscriberImpl(const std::string& subscriberName);~SubscriberImpl() override;void Update(const std::string &topic, const std::string &message) override;private:std::string m_name;};
}
#endif
./subscribe/SubscribeImpl.cpp
#include <SubscriberImpl.h>
#include <iostream>
using namespace Observer;SubscriberImpl::SubscriberImpl(const std::string& subscriberName) : m_name(subscriberName) {}SubscriberImpl::~SubscriberImpl() {}void SubscriberImpl::Update(const std::string &topic, const std::string &message)
{std::cout << "Subscriber [" << m_name << "] received message on topic [" << topic << "]: " << message << std::endl;
}
2. 发布订阅中心类TopicServer
./topicServer/TopicServer.h
#ifndef TOPICSERVER_H
#define TOPICSERVER_H
#include <string>
#include <memory>
#include <Subscriber.h>
namespace Observer
{class TopicServer{public:virtual ~TopicServer() = default;virtual void Subscribe(const std::string& topic, std::shared_ptr<Subscriber> subscriber) = 0;virtual void Unsubscribe(const std::string& topic, std::shared_ptr<Subscriber> subscriber) = 0;virtual void Notify(const std::string& topic, const std::string& message) = 0;};
}
#endif
./topicServer/TopicServerImpl.h
#ifndef TOPICSERVERIMPL_H
#define TOPICSERVERIMPL_H
#include <TopicServer.h>
#include <unordered_map>
#include <vector>
namespace Observer
{class TopicServerImpl : public TopicServer{public:explicit TopicServerImpl();~TopicServerImpl() override;void Subscribe(const std::string& topic, std::shared_ptr<Subscriber> subscriber) override;void Unsubscribe(const std::string& topic, std::shared_ptr<Subscriber> subscriber) override;void Notify(const std::string& topic, const std::string& message) override;private:std::unordered_map<std::string, std::vector<std::shared_ptr<Subscriber>>> m_topicSubscriber;};
}
#endif
./topicServer/TopicServerImpl.cpp
#include <TopicServerImpl.h>
#include <algorithm>
using namespace Observer;TopicServerImpl::TopicServerImpl() {}TopicServerImpl::~TopicServerImpl() {}void TopicServerImpl::Subscribe(const std::string& topic, std::shared_ptr<Subscriber> subscriber)
{m_topicSubscriber[topic].push_back(subscriber);
}void TopicServerImpl::Unsubscribe(const std::string& topic, std::shared_ptr<Subscriber> subscriber)
{auto& subscribersList = m_topicSubscriber[topic];auto itBegin = std::remove(subscribersList.begin(), subscribersList.end(), subscriber);subscribersList.erase(itBegin, subscribersList.end());if (subscribersList.size() == 0) {m_topicSubscriber.erase(topic);}
}void TopicServerImpl::Notify(const std::string& topic, const std::string& message)
{if (m_topicSubscriber.contains(topic)) {for (auto subscriber : m_topicSubscriber[topic]) {subscriber->Update(topic, message);}}
}
3. 发布者类Publisher
./publisher/Publisher.h
#ifndef PUBLISHER_H
#define PUBLISHER_H
#include <string>
namespace Observer
{class Publisher{public:virtual ~Publisher() = default;virtual void PublishMessage(const std::string& topic, const std::string& message) = 0;};
}
#endif
/publisher/PublisherImpl.h
#ifndef PUBLISHERIMPL_H
#define PUBLISHERIMPL_H
#include <Publisher.h>
#include <TopicServer.h>
#include <memory>
namespace Observer
{class PublisherImpl : public Publisher{public:explicit PublisherImpl(std::shared_ptr<TopicServer> topicServer);~PublisherImpl() override;void PublishMessage(const std::string& topic, const std::string& message) override;private:std::shared_ptr<TopicServer> m_server;};
}
#endif
/publisher/PublisherImpl.cpp
#include <PublisherImpl.h>
using namespace Observer;PublisherImpl::PublisherImpl(std::shared_ptr<TopicServer> topicServer) : m_server(topicServer) {}PublisherImpl::~PublisherImpl() {}void PublisherImpl::PublishMessage(const std::string& topic, const std::string& message) {m_server->Notify(topic, message);
}
4. main函数调用
./main.cpp
#include <PublisherImpl.h>
#include <TopicServerImpl.h>
#include <SubscriberImpl.h>
namespace {constexpr std::string TOPICA {"TopicA"};constexpr std::string TOPICB {"TopicB"};
}
using namespace Observer;
int main()
{std::shared_ptr<TopicServer> server = std::make_shared<TopicServerImpl>();// 创建观察者std::shared_ptr<Subscriber> subscriber1 = std::make_shared<SubscriberImpl>("subscriber1");std::shared_ptr<Subscriber> subscriber2 = std::make_shared<SubscriberImpl>("subscriber2");std::shared_ptr<Subscriber> subscriber3 = std::make_shared<SubscriberImpl>("subscriber3");std::shared_ptr<Subscriber> subscriber4 = std::make_shared<SubscriberImpl>("subscriber4");// 订阅主题server->Subscribe(TOPICA, subscriber1);server->Subscribe(TOPICA, subscriber2);server->Subscribe(TOPICA, subscriber4);server->Subscribe(TOPICB, subscriber1);server->Subscribe(TOPICB, subscriber3);server->Subscribe(TOPICB, subscriber4);std::shared_ptr<Publisher> publisher = std::make_shared<PublisherImpl>(server);// 发布消息publisher->PublishMessage(TOPICA, "Hello from TopicA!");publisher->PublishMessage(TOPICB, "Greetings from TopicB!");// 移除订阅者server->Unsubscribe(TOPICA, subscriber4);server->Unsubscribe(TOPICB, subscriber4);// 发布消息publisher->PublishMessage(TOPICA, "Update from TopicA after unsubscribe!");publisher->PublishMessage(TOPICB, "Update from TopicB after unsubscribe!");return 0;
}
5. 编写CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
set(ProjectName Observer)
project(${ProjectName})set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED True)include_directories(./publisher./subscriber./topicServer
)file(GLOB LIB_FILEpublisher/*subscriber/*topicServer/*)message(${LIB_FILE})
add_executable(${ProjectName}main.cpp${LIB_FILE})
此时文件树结构如下:
.
├── CMakeLists.txt
├── main.cpp
├── publisher
│ ├── Publisher.h
│ ├── PublisherImpl.cpp
│ └── PublisherImpl.h
├── subscriber
│ ├── Subscriber.h
│ ├── SubscriberImpl.cpp
│ └── SubscriberImpl.h
└── topicServer├── TopicServer.h├── TopicServerImpl.cpp└── TopicServerImpl.h
6. 编译运行
mkdir build
cd build
cmake ..
make -j12
./Observer
运行结果如下
Subscriber [subscriber1] received message on topic [TopicA]: Hello from TopicA!
Subscriber [subscriber2] received message on topic [TopicA]: Hello from TopicA!
Subscriber [subscriber4] received message on topic [TopicA]: Hello from TopicA!
Subscriber [subscriber1] received message on topic [TopicB]: Greetings from TopicB!
Subscriber [subscriber3] received message on topic [TopicB]: Greetings from TopicB!
Subscriber [subscriber4] received message on topic [TopicB]: Greetings from TopicB!
Subscriber [subscriber1] received message on topic [TopicA]: Update from TopicA after unsubscribe!
Subscriber [subscriber2] received message on topic [TopicA]: Update from TopicA after unsubscribe!
Subscriber [subscriber1] received message on topic [TopicB]: Update from TopicB after unsubscribe!
Subscriber [subscriber3] received message on topic [TopicB]: Update from TopicB after unsubscribe!
运行结果说明:
订阅了TOPICA的subscriber1、subscriber2、subscriber4均收到了Publisher发布的Hello from TopicA!
订阅了TOPICB的subscriber1、subscriber3、subscriber4均收到了Publisher发布的Greetings from TopicB!
subscriber4对TOPICA和TOPICB去订阅后,再次发布消息则只有subscriber1、subscriber2、subscriber3能收到订阅信息
本文基于观察者模式,侧重于于阐述设计模式的核心思想,实现了一个简化的发布订阅系统。这种设计模式在实际生产环境中,往往需要更复杂的实现,比如涉及到不同进程之间的通信、负载均衡等,以满足高并发、高可用性的要求。
相关文章:
【设计模式】如何用C++实现观察者模式【发布订阅机制】
【设计模式】如何用C实现观察者模式【发布订阅机制】 一、问题背景 代码质量影响生活质量。最近工作中频繁接触各种设计模式,深刻体会到优秀的设计模式不仅能显著降低后续维护的压力,还能提升开发效率。观察者模式作为一种降低耦合度、提高扩展性的利器…...
【LC】2717. 半有序排列
题目描述: 给你一个下标从 0 开始、长度为 n 的整数排列 nums 。 如果排列的第一个数字等于 1 且最后一个数字等于 n ,则称其为 半有序排列 。你可以执行多次下述操作,直到将 nums 变成一个 半有序排列 : 选择 nums 中相邻的两…...
AI智算-k8s部署大语言模型管理工具Ollama
文章目录 简介k8s部署OllamaOpen WebUI访问Open-WebUI 简介 Github:https://github.com/ollama/ollama 官网:https://ollama.com/ API:https://github.com/ollama/ollama/blob/main/docs/api.md Ollama 是一个基于 Go 语言开发的可以本地运…...
CloudberryDB(二) 演化路线图
CloudberryDB 制定了演化路线图(https://github.com/orgs/cloudberrydb/discussions/369)并在逐步改进,这是 Cloudberry Database 发挥独特价值之处。 计划、正在进行或已完成的一些工作。 支持轻松升级 PostgreSQL 内核版本。 原有 Greenp…...
《拉依达的嵌入式\驱动面试宝典》—C/CPP基础篇(二)
《拉依达的嵌入式\驱动面试宝典》—C/CPP基础篇(二) 你好,我是拉依达。 感谢所有阅读关注我的同学支持,目前博客累计阅读 27w,关注1.5w人。其中博客《最全Linux驱动开发全流程详细解析(持续更新)-CSDN博客》已经是 Linux驱动 相关内容搜索的推荐首位,感谢大家支持。 《拉…...
实现canal监控binlog日志再通过消息队列异步处理
一、简单步骤 实现Canal监控Binlog日志,并通过消息队列进行异步处理,步骤如下: 配置Canal:首先,需要配置Canal进行Binlog日志监控。可以通过Canal的官方文档了解如何配置Canal。 连接到Canal:使用Canal客户…...
Linux DNS 协议概述
1. DNS 概述 互联网中,一台计算机与其他计算机通信时,通过 IP 地址唯一的标志自己。此时的 IP 地址就类似于我们日常生活中的电话号码。但是,这种纯数字的标识是比较难记忆的,而且数量也比较庞大。例如,每个 IPv4 地址…...
linux打包qt程序
参考这篇文章:Linux下Qt程序打包_linuxdeployqt下载-CSDN博客 本篇文章的系统环境是 : 虚拟机ubuntu18.04 用下面这个qmake路径 进行编译 在 ~/.bashrc 文件末尾,qmake目录配置到文件末尾 将上图中bin目录下的linuxdeployqt程序拷贝到/usr/bin下一份 &…...
软考中级-软件设计师通过心路经验分享
执念,第四次终于通过了 没买书,下班后每天2小时,四个2个月终于过了 学习经验: 1.下班后学习真的靠毅力,和上学的时候考证不是一个状态,大家要及时调整,否则过程很痛苦 2.失败三次的经验…...
safe area helper插件
概述 显示不同机型的必能显示的区域 实现步骤 引入safearea,引入其中的safearea的csharp 为cancas加入gameobject gameobject中加入safearea脚本 将UI作为这个gameobject的子物体,就可以完成显示...
李宏毅机器学习-批次 (batch)和动量(momentum)
一.batch(批次) 在计算微分时,不是对所有的数据算出来的Loss值做微分,而是将所有的数据分成一个一个的batch。一个batch是一个B,在更新参数时,拿B的资料计算Loss,计算gradient,再更新…...
C# 网络编程--关于UDP 通信(二)
UDP (User Datagram Protocol) 是一种无连接的传输层协议,主要用于支持数据报文的传输。它的主要特点包括简单、高效、不保证可靠性和顺序。 1.UDP协议基本概念 1.udp基于IP的简单的协议,不可靠的协议 2.优点:简单、 轻量化、 传输速度高、…...
【k8s集群应用】Kubernetes部署安装-二进制部署实例
文章目录 Kubernetes 部署方式常见的K8S安装部署方式Kubeadm与二进制部署的区别 Kubernetes部署安装环境配置Kubernetes集群初始化配置(实验环境)一、操作系统初始化配置二、部署Docker引擎 etcd 集群搭建配置 etcd 集群 Kubernetes Master 组件部署准备…...
js常见代码输出问题之promise,await,变量提升以及闭包(包括例子以及详细解析)
这里写目录标题 异步事件循环宏任务微任务1. 执行顺序2. 分类 Promise代码输出1. promise.then执行时机2. 宏任务微任务的多轮次3. .then .catch会返回新的promise4. 返回任意一个非 promise 的值都会被包裹成 promise 对象5. .then .catch 的值不能是promise本身6. 值透传7. .…...
遗传算法与深度学习实战(27)——进化卷积神经网络
遗传算法与深度学习实战(27)——进化卷积神经网络 0. 前言1. 自定义交叉算子2. 自定义突变操作符3. 进化卷积神经网络小结系列链接 0. 前言 DEAP toolbox 中提供的标准遗传操作符对于自定义的网络架构基因序列来说是不够的。这是因为任何标准的交叉算子…...
【Vue3】前端使用 FFmpeg.wasm 完成用户视频录制,并对视频进行压缩处理
强烈推荐这篇博客!非常全面的一篇文章,本文是对该博客的简要概括和补充,在不同技术栈中提供一种可行思路,可先阅读该篇文章再阅读本篇: FFmpeg——在Vue项目中使用FFmpeg(安装、配置、使用、SharedArrayBu…...
基础算法——前缀和
由于比赛基本都是采用Dev-C所以,算法篇基本都是采用Dev-C来解释(版本5.11,c11) 首先介绍一下前缀和算法 给定一个数组,有q次询问,每次询问: 两个整数l,r,求出数组 l 到 r的结果 遇…...
spring实例化对象的几种方式(使用XML配置文件)
前言 Spring框架作为一个轻量级的控制反转(IoC)容器,为开发者提供了多种对象实例化的策略。通过这些策略,开发者可以更加灵活地控制对象的生命周期和依赖关系。无论是通过XML配置、注解配置还是Java配置,Spring都能…...
【二叉树】力扣 129.求根节点到叶子节点数字之和
一、题目 二、思路 每找到一个非空节点,之前路径上的所有节点的数量级都要增加1个单位。例如,当前节点为3,之前的节点路径为1 -> 2,presum 1 * 10 2 12,现在路径变为了 1 -> 2 -> 3,sum pres…...
深度学习物体检测之YOLOV5源码解读
V5比前面版本偏工程化,项目化,更贴合实战 一.V5版本项目配置 (1)整体项目概述 首先github直接查找yolov5,下载下来即可。在训练时,数据是怎么处理的?网络模型架构是怎么设计的(如各层的设计)?yolov5要求是大于python3.8与大于等…...
10个Elog实用技巧:让你的博客管理效率翻倍
10个Elog实用技巧:让你的博客管理效率翻倍 【免费下载链接】elog Markdown 批量导出工具、开放式跨平台博客解决方案,随意组合写作平台(语雀/Notion/FlowUs/飞书/我来Wolai)和博客平台(Hexo/Vitepress/Halo/Confluence/WordPress等) 项目地址: https:/…...
Go 内存优化骚操作
1. 零内存占位符:struct{}{}原理:struct{} 是空结构体,Go 编译器对其做了特殊处理,它在内存中不占任何空间(大小为 0 字节)。场景 A:实现集合 (Set)map[string]struct{}。比起 map[string]bool&…...
算法实例分析:使数组相等的最小开销
使数组相等的最小开销通过题意分析可知要让所有值相等,必然不需要超出数据的最大最小值,因此左右边界可以预先缩小范围。然后根据我们上面的分析不断缩小搜索边界范围。关于函数的计算,只要统计所有数据与的差值再乘上权重即可。最后注意&…...
如何彻底解决游戏键盘冲突:Hitboxer SOCD Cleaner完整指南
如何彻底解决游戏键盘冲突:Hitboxer SOCD Cleaner完整指南 【免费下载链接】socd Key remapper for epic gamers 项目地址: https://gitcode.com/gh_mirrors/so/socd 你是否在激烈的游戏对战中遇到过这样的困扰?同时按下W和S键时角色突然卡顿&…...
树莓派5/4B新手开箱:用官方Raspberry Pi Imager工具10分钟完成系统部署
树莓派5/4B极速部署指南:官方Imager工具的全新工作流解析 第一次拿到树莓派5或4B时,很多用户会陷入传统部署方法的复杂流程中——下载镜像、格式化存储卡、烧录系统、手动配置网络……这些步骤不仅耗时,还容易因操作失误导致启动失败。而树莓…...
自媒体矩阵工具选型避坑!多个平台发布指南,新手也能选对工具
做自媒体这事吧,光靠一个账号,很多时候确实不太容易把流量做起来,所以大多数人,慢慢就会去做多平台、多账号这种矩阵。可问题也很现实,账号一多,平台一散,人就容易被各种切换后台、重复发内容、…...
人大金仓KingbaseES分区表‘挂载’与‘摘除’功能详解:像搭积木一样管理你的数据
人大金仓KingbaseES分区表‘挂载’与‘摘除’功能实战指南:数据管理的乐高式玩法 想象一下,你的数据库表像一堆积木,可以随时拆解、重组,而无需担心数据丢失或性能下降。这正是人大金仓KingbaseES分区表"挂载(ATTACH)"和…...
TCP 多客户端与服务器通信程序
一、项目简介 本项目是一个基于 Linux实现的 TCP 多客户端与服务器通信程序,主要包含: tcp_server:TCP 服务端程序,监听客户端连接,接收客户端数据,维护在线客户端列表。 tcp_client:TCP 客户…...
如何通过本地解析技术彻底解决九大网盘下载限速问题
如何通过本地解析技术彻底解决九大网盘下载限速问题 【免费下载链接】Online-disk-direct-link-download-assistant 一个基于 JavaScript 的网盘文件下载地址获取工具。基于【网盘直链下载助手】修改 ,支持 百度网盘 / 阿里云盘 / 中国移动云盘 / 天翼云盘 / 迅雷云…...
AI 不锈钢保温杯智能功率 MOSFET 完整选型方案
2026年随着 AI 技术在智能保温杯领域的深度渗透(如精准温控、语音交互、健康监测、无线充电管理),对功率 MOSFET 提出更高要求:高集成度、低功耗、小封装、高可靠性。微碧半导体(VBsemi)基于 SGT 及 Trench…...
