实用指南:精读 C++20 设计模式:行为型设计模式——观察者模式

news/2025/10/18 11:33:47/文章来源:https://www.cnblogs.com/yxysuanfa/p/19147185

精读 C++20 设计模式:行为型设计模式——观察者模式

前言

​ 观察者!这个是一个很有名的设计模式——简而言之,我们这个模式在关心对象的变化。当对象变化的时候,我们要触发点事情,这个怎么做呢?我们要放一个观察者,看着它:嘿对象变了处理点事情!这就是这个设计模式在做的事情。

Observer<T>

​ 现在我们很关心Person的Age变化,甚至要求它变化的时候咱们就做点事情:

class Person
{
int age;
public:
void setAge(const int _age);
};

​ 那根据前言,咱们就做点事情:

template<typename T>struct Observer{virtual void monitor_change(T& p, const std::string& what_changed) = 0;};

​ 之后咱们就可以:

struct PersonObserver : Observer<Person>{void monitor_change(Person& p, const std::string& what_changed) override{if(what_changed == "age"){// process the sessions, like, print}}};

​ 甚至如果我们想要监控更多的属性,就可以采用多继承了(虽然不太建议)

Observable<T>

​ 被监视对象也要支持被监控!这个事情很简单:

template<typename T>struct Observeable{void notify(T& src, const std::string& f_n){for(auto& o : obs)o->monitor_change(src, f_n);}// push/pop <-> subsrcibe / unsubsrcibe the obs vectorprivate:vector<Observer<T>*> obs;};

连续观察者 + 被观察者

​ 连续观察者是一个经典的设计场景:说白了就是:A 观察 B,B 观察 C;当 C 变化,B 收到通知并更新,从而触发 B 对 A 的通知 —— 这就是“连续观察者/被观察者”。听着没啥?但是问题没那么简单。

依赖问题(cycles / bounce)

​ 第一个问题——能成为链式的话,能不能成为环呢?显然这是有风险的。A 观察 B,B 又观察 A。如果 A 改变 => 通知 B,B 的回调修改 A => 再通知 B => 无限循环。实战中这类问题会导致栈溢出或持续 CPU 占用。咋办呢?

  1. 我们完全可以触严苛化触发条件变化——在 setX() 前比较新旧值,只有真正变化才通知(我们在 Person::setAge 中演示)。这是首选且最有效的方式。
  2. 我们根据自己的场景进行合并化:合并多个变化后一次性发出通知(coalesce),比如 begin_update()/end_update() 模式,只有 end_update() 时才通知。
  3. 在某些更新路径临时禁用通知(例如:ScopedNotificationDisable),完成后恢复并可选择是否发一次最终通知。
  4. 在 notify 路径中维护最大嵌套深度或使用版本号来防止同一事件反复传播(但往往是权宜之计,Overflow 检测机制)。
  5. 设计时避免互相观察;如果必需,明确哪端是“主要数据源”并在被动端做好防护

取消订阅 + 线程安全(并发场景)

// observable.hpp — 一个可复用的 Observable 实现
#pragma once
#include <functional>#include <mutex>#include <unordered_map>#include <vector>#include <unordered_set>#include <cstddef>#include <memory>template<typename T>class Observable {public:using Callback = std::function<void(T&, const std::string&)>;// Subscription:RAII 风格(析构时自动取消)class Subscription {public:Subscription() = default;Subscription(size_t id, Observable* owner) : id_(id), owner_(owner) {}Subscription(const Subscription&) = delete;Subscription& operator=(const Subscription&) = delete;Subscription(Subscription&& o) noexcept { id_ = o.id_; owner_ = o.owner_; o.owner_ = nullptr; o.id_ = 0; }Subscription& operator=(Subscription&& o) noexcept {if (this != &o) { unsubscribe(); id_ = o.id_; owner_ = o.owner_; o.owner_ = nullptr; o.id_ = 0; }return *this;}~Subscription() { unsubscribe(); }void unsubscribe() {if (owner_) { owner_->unsubscribe(id_); owner_ = nullptr; id_ = 0; }}bool valid() const { return owner_ != nullptr; }private:size_t id_ = 0;Observable* owner_ = nullptr;};Observable() = default;~Observable() = default;// 订阅,返回 Subscription,析构或手动调用 unsubscribe 取消Subscription subscribe(Callback cb) {std::lock_guard lock(mutex_);const size_t id = next_id_++;if (in_notify_ > 0) {// 在 notify 中订阅,延迟加入(避免修改当前观察者集合)pending_add_.emplace_back(id, std::move(cb));} else {observers_.emplace(id, std::move(cb));}return Subscription{id, this};}// 直接按 id 取消(Subscription 会调用它)void unsubscribe(size_t id) {std::lock_guard lock(mutex_);if (in_notify_ > 0) {pending_remove_.insert(id);} else {observers_.erase(id);}}// 通知所有观察者(线程安全,可重入)void notify(T& src, const std::string& what_changed) {std::vector<Callback> snapshot;{std::lock_guard lock(mutex_);++in_notify_;snapshot.reserve(observers_.size());for (auto &kv : observers_) snapshot.push_back(kv.second);}// 调用回调(在外部 unlocked)for (auto &cb : snapshot) {try {cb(src, what_changed);} catch (...) {// 任意异常策略:不要让单个 observer 崩掉整个流程// 这里简单吞掉,也可记录日志}}// 结束通知,若是最外层 notify,应用挂起的增删{std::lock_guard lock(mutex_);--in_notify_;if (in_notify_ == 0) apply_pending_locked();}}private:void apply_pending_locked() {// 必须在持锁状态下调用for (auto &id : pending_remove_) observers_.erase(id);pending_remove_.clear();for (auto &p : pending_add_) observers_.emplace(p.first, std::move(p.second));pending_add_.clear();}private:std::mutex mutex_;std::unordered_map<size_t, Callback> observers_;std::vector<std::pair<size_t, Callback>> pending_add_;std::unordered_set<size_t> pending_remove_;size_t next_id_ = 1;int in_notify_ = 0; // notify 嵌套计数};

上面的 Observable 实现已经做了线程安全的基本保障:subscribe / unsubscribe / notifymutex 保护共享状态。notify 在外面调用回调,避免回调期间持锁(防止回调里阻塞导致其他线程无法订阅)。在 notify 中退订/订阅的请求会被延迟处理(放到 pending 集合),避免在迭代 observers_ 时修改容器。

可重入性(Reentrancy)与嵌套通知

观察者在其回调中可能会再次修改 subject(例如 UI 在收到 age 更新后又调用 setAge() 进行校正)。这会导致嵌套 notify() 调用。我们的实现支持嵌套通知(in_notify_ 计数器),并把对订阅集合的修改延迟到最外层通知完成。这样避免了在迭代容器时的并发修改崩溃。

但嵌套通知仍需要注意:

  • 嵌套 notify 会再次发送 snapshot(包括可能仍存在的观察者),从而产生更深的调用栈与复杂的执行顺序。
  • 若没有做好变更检测或抑制,很容易进入无限循环(见依赖问题)。
  • 有时我们希望“递归通知即时看到新订阅”,有时又希望“通知期间新增的订阅不接收当前正在进行的事件”。上面实现选择后者(snapshot 在 notify 开始时产生),这是常见且可预期的行为。若你需要前者,设计会更复杂(需要在 notify 中读取到 pending add),但会导致回调里新增的观察者在本轮也收到通知,可能制造惊喜或风险。一般不推荐。

可选的“延迟操作队列”策略(示例思路):

  • 把所有 subscribe/unsubscribe/其他修改放到队列中,在 notify 完成后、或在安全点统一执行。
  • 这可以避免竞态并让通知视作原子操作,但也会增加延迟(订阅在本轮不会立即生效)。这是常见的 trade-off。

订阅并缓存状态的只读代理

在 GUI/渲染或大型系统中,一个常见模式是 View:订阅被观察对象并维护一份本地缓存,用于快速读取(避免每次访问都加锁或计算)。View 是观察者的一种具体用途。

#include 
#include 
struct PersonView {std::atomic cached_age{0};std::optional::Subscription> sub;void attach(Person& p) {// 订阅并更新缓存sub = p.changes.subscribe([this](Person& who, const std::string& f){if (f == "age") cached_age.store(who.age, std::memory_order_relaxed);});// 初始化缓存cached_age.store(p.age, std::memory_order_relaxed);}int age() const { return cached_age.load(std::memory_order_relaxed); }
};

优点

  • 快速读(无需每次从主对象加锁或计算)。
  • 视图可以把更新批量化、格式化或做额外的衍生计算(例如显示字符串形式)。

注意

  • 缓存有时会过期(滞后),设计时需保证接受可接受的最终一致性。
  • 若缓存需要严格一致性(强一致),就不能单纯用这种异步订阅方式,需要同步读取或在更新时做同步通知/等待。

总结

我们试图解决的问题
我们如何解决
  • 提供 Observable 抽象,允许注册回调/观察者,变更时 notify 所有注册的观察者。
  • 通过 RAII Subscription 实现自动退订;通过 weak_ptr 协助管理生命周期;通过 snapshot + pending queues 实现线程安全与 reentrancy-safe 的 notify。
  • 通过变更检测、事务/抑制或批量通知应对循环依赖与性能问题。视图(View)模式把观察者的职责扩展为“订阅并缓存”以便快速读取。
优点
缺点(以及缓解)
  1. 生命周期与悬指针问题:观察者或被观察者被销毁会造成回调访问已释放内存。
    • 缓解:使用 Subscription (RAII),回调内使用 weak_ptr 检查,或者在对象析构时先统一退订。
  2. 循环依赖/无限回调:观察链可能产生循环触发。
    • 缓解:做好变更检测、事务/批量更新、或明确禁止双向观察。
  3. 并发与性能问题:大量观察者和频繁通知可能导致拷贝开销或锁竞争。
    • 缓解:snapshot + 延迟 apply 是通用的安全折中;性能敏感场景考虑 RCU/lock-free 数据结构或降低通知频率(采样/限流)。
  4. 语义复杂性(何时生效):新增订阅在当前通知中是否能收到事件,订阅/退订是否即时生效——不同实现会有不同语义,需在设计中明确。
    • 缓解:在文档中明确语义(例如:本实现保证“订阅在本轮通知不会收到正在进行的事件”;退订被延迟到最外层通知完成时生效)

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

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

相关文章

大疆无人机RTMP推流至LiveNVR实现web页面实时播放与录像回放,并可以转GB28181协议级联推送给上级监控视频管理平台

@目录1、无人机推流转国标2、获取RTMP推流地址2.1、RTMP推流地址格式2.2、推流地址示例2、设备RTMP推流3、配置拉转RTMP3.1、直播流地址格式3.2、直播流地地址示例3.3、通道配置直播流地址4、配置级联到GB28181国标平台…

Character Animator 2025下载安装教程:2D角色动画软件零基础入门,附最新下载安装教程及激活方法

还在找Character Animator 2025怎么下载安装?这份保姆级教程帮你一步到位!不管是做虚拟主播、短视频动画,还是教育课件制作,掌握CH 2025的安装方法是第一步。本文包含详细下载渠道、安装步骤、快捷键及常见问题,看…

2025年彩钢瓦/镀锌板/折弯件/C型钢/Z型钢/压型瓦/楼承板/次檩条厂家推荐排行榜,专业钢结构安装与定制加工实力解析

2025年彩钢瓦/镀锌板/折弯件/C型钢/Z型钢/压型瓦/楼承板/次檩条厂家推荐排行榜,专业钢结构安装与定制加工实力解析随着我国建筑工业化的快速发展,钢结构建筑因其施工周期短、抗震性能好、可回收利用等优势,在工业厂…

2025 年最新金相厂家最新推荐排行榜:涵盖金相磨抛机 / 切割机 / 显微镜 / 抛光机 / 预磨机设备,助力企业精准选择优质品牌

当前材料检测行业持续发展,金相检测作为材料分析核心环节,对石油机械、铁路器材、航空航天等领域的产品质量与安全起着关键作用。随着市场需求升级,金相品牌数量激增,但品牌间技术实力、产品质量和服务水平差距明显…

武汉图核科技

武汉图核科技新的名字 以前的名字是英语单词音译过来的,没有什么具体含义,也不容易看出是做什么的。中国人还是取个中文名好一些,于是想换个名字,要言简意赅,简单好记。不擅长取名,找AI来帮忙:一眼相中了图核科…

maven的概述以及在mac安装部署

maven的概述以及在mac安装部署pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco"…

完整教程:display ospf peer 概念及题目

完整教程:display ospf peer 概念及题目pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "M…

Python 列表切片赋值教程:掌握 “移花接木” 式列表修改技巧

Python 列表切片赋值教程:掌握 “移花接木” 式列表修改技巧$(".postTitle2").removeClass("postTitle2").addClass("singleposttitle");列表_切片赋值_slice_assignment_嫁接 回忆上次…

2025中国开发者必看:主流代码托管平台本土化能力深度测评

2025中国开发者必看:主流代码托管平台本土化能力深度测评 在数字化转型加速推进的当下,代码托管平台已成为软件开发团队不可或缺的基础设施。随着国内开发者群体的快速扩张,对代码托管服务的本土化需求也日益凸显。…

开源数据采集工具 logstash(收集日志)/telegraf(收集指标)

Telegraf 是一个用 Go 编写的代理程序,是收集和报告指标和数据的代理。可收集系统和服务的统计数据,并写入到 InfluxDB 数据库。Telegraf 具有内存占用小的特点,通过插件系统开发人员可轻松添加支持其他服务的扩展。…

2025年粉末冶金制品厂家推荐排行榜,粉末冶金零件,金属注射成形,结构件,齿轮,轴承公司最新精选

2025年粉末冶金制品厂家推荐排行榜:粉末冶金零件、金属注射成形、结构件、齿轮、轴承公司最新精选行业背景与发展趋势粉末冶金技术作为现代制造业的核心工艺之一,在汽车、家电、机械装备等领域发挥着越来越重要的作用…

多模态大语言模型LISA - 详解

多模态大语言模型LISA - 详解pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco"…

2025 年升降平台车厂家最新推荐口碑排行榜:覆盖多类型产品,聚焦实力厂家,为企业选购提供权威参考剪叉式/手动液压/电动液压升降平台车厂家推荐

在工业生产、仓储物流等领域,升降平台车是不可或缺的关键设备,其质量与性能直接关系到企业运营效率与生产安全。当前市场上,升降平台车品牌繁杂,部分厂家技术落后、工艺不规范、售后不完善,导致企业选购时面临诸多…

供应商图纸协同是什么?主要有哪几个核心原则?

供应商图纸协同是确保制造业供应链高效运作的基础。它不仅涉及图纸和数据的安全传递,也需要关注信息的准确性和及时性。企业通过建立数字平台,可以统一管理图纸及相关文件,加快信息流转。这一过程强调了沟通的重要性…

「Java EE开发指南」用MyEclipse开发的EJB开发工具(二)

「Java EE开发指南」用MyEclipse开发的EJB开发工具(二)如果您需要支持Java EE 5中引入的简化基于注释的POJO编程模型,那么EJB开发工具就是您的正确选择。在此您将了解到:EJB开发工具和EJB项目 持久性支持和EJB项目…

2025 年堆高车厂家最新推荐排行榜:聚焦专利技术、华为等大牌合作案例及国内优质品牌解析手动液压/手动液压/卷筒/油桶堆高车厂家推荐

当前,仓储物流与生产制造行业对堆高车的需求持续攀升,但其市场供给呈现 “质量参差、选型复杂” 的态势。一方面,部分厂家缺乏核心技术,产品故障率高、维护成本高,难以适配高强度作业;另一方面,企业采购时易受低…

chromadb的使用

chromadb的使用from chromadb.config import Settings from chromadb.utils import embedding_functions import os import chromadb # 设置 Chroma 配置 persist_directory = "database" if not os.path.ex…

TResult Funcin T, out TResult的应用

TResult Func<in T, out TResult>的应用在 C# 中,Func<bool, string>是一个委托类型,表示一个接受 bool类型参数并返回 string类型的方法。 // 声明 Func<bool, string> 变量 Func<bool, strin…

2025 年最新推荐!编码器源头厂家排行榜:聚焦无磁 / 光学 / 脉冲等多类型产品,精选行业优质企业

随着工业自动化向高精度、高智能化方向快速迈进,编码器作为闭环控制系统的核心传感部件,市场需求持续攀升,但行业乱象也随之凸显。部分厂家缺乏核心技术,产品精度与可靠性不足,难以适配高端制造场景;售后体系不完…

Excelize 开源基础库发布 2.10.0 版本更新

2025年10月14日,开源电子表格文档基础库 Excelize 发布了 2.10.0 正式版本,该版本包含了 40 多项新增功能、错误修复和兼容性提升优化。Excelize 是 Go 语言编写的用于操作 Office Excel 文档基础库,基于 ECMA-376,…