C++20之设计模式:观察者模式

观察者模式

      • 观察者
        • 属性观察者
        • Observer\<T>
        • Observable<T>
      • 连接观察者和被观察者
        • 依赖问题
        • 取消订阅和线程安全
      • 可重入
        • 通过 Boost.Signals2 来实现 Observer
        • 总结

观察者

观察者模式是一种流行且必需的模式,QT的信号槽机制本质上就是观察者模式。

属性观察者

一个人每长大一岁的时候都会庆祝生日,怎么实现呢?可以给出下面这样一个定义:

struct Person {int age;Person(int age) : age{age} {}
};

怎么知道一个人的年龄发生改变了呢?尝试轮询?这种方法太糟糕!更好的是通知机制,当年龄变化时发出通知。

设计一个setter,使其变化时发出通知。

struct Person {int get_age() const { return age; }void set_age(const int value) { // 仅在变化时通知if (age != value) {age = value; // notify}}private:int age;
};

怎么实现notify呢?

Observer<T>

一种方法是定义一个基类,任何关心Person变化的对象都需要继承它

struct PersonListener {virtual void person_changed(Person &p, const string &property_name) = 0;
};

然而,属性更改可以发生在Person以外的类型上,此时需要为这些类型生成额外的类。这里使用更通用的定义:

template <typename T>
struct Observer {virtual void field_changed(T &source, const string &field_name) = 0;
};

field_changed()中的两个参数,第一个是对属性发生更改的对象的引用,第二个是属性的名称。名称作为字符串传递将损害代码的可重构性(因为属性名可以变化)

这个实现将允许我们观察Person类的变化

struct PersonObserver : Observer<Person> {void field_changed(Person &source, const string &field_name) override {cout << "Person's " << field_name << " has changed to " << source.get_age() << ".\n";}
};

这个方案允许同时观察多个类的属性变化。例如,将Creature类加入。

struct ConsolePersonObserver : Observer<Person>, Observer<Creature> {void field_changed(Person &source, const string &field_name) {}void field_changed(Creature &source, const string &field_name) {}
};

另一种替代方法是使用std::any。

Observable

Person作为一个可观察类,它将承担新的责任,即:

  • 维护一个列表,其中保存所有订阅Person变化的观察者
  • 观察者可以通过 subscribe()/unsubscribe() 订阅或者取消订阅
  • 当Person发生改变的时候,通过notify通知所有的观察者。
template <typename T>
struct Observable {void notify(T& source, const string& name);void subscribe(Observer<T>* f) { observers.emplace_back(f); };void unsubscribe(Observer<T>* f);private:vector<Observer<T>*> observers;
};

subscribe()/unsubscribe(),将一个观察者从列表中加入/删除。

notify()遍历每个观察者,并且依次调用对应的observer.field_changed()函数。

void notify(T &source, const string &name) {for (auto &&obs : observers) observes->field_changed(source, name);
}

但是,仅继承Observable<T>是不够的,我们的类还需要在其属性发生改变的时候调用notify()函数。

例如,考虑set_age()函数,它现在有三个职责:

  • 检查属性值是否已实际更改。如果age是20岁,设置为20岁,通知时是没有意义的。
  • 给属性赋合理的值。
  • 用正确的参数调用notify()函数

因此,set_age()的新实现可能长成这样:

struct Person : Observable<Person> {void set_age(const int age) {// check_age(age);if (this->age != age) {this->age = age;notify(*this, "age");}}private:int age;
};

连接观察者和被观察者

现在,使用设计的观察者和被观察者,下面是观察者的示例:

// CRTP
struct PersonObserver : Observer<Person> {void field_changed(Person &source, const string &field_name) override {cout << "Person's " << field_name << " has changed to " << source.get_age()<< ".\n";}
};

用法:

Person p{ 20 };
PersonObserver ob;
p.subscribe(&ob);
p.set_age(21); // Person's age has changed to 21.
p.set_age(22); // Person's age has changed to 22.

如果不关心有关属性依赖关系和线程安全性/可重入性的问题,就可以在这此止步。如果想看到更复杂的讨论,请继续阅读

依赖问题

大于18岁的人具有恋爱权,当某个人具有恋爱权之后我们希望被通知到。首先,假设Person类有如下的getter函数:

bool get_can_love() const { return age >= 18};

注意,get_can_love()没有底层属性成员和setter(我们可以引入这样的字段,例如can_love(),但它显然是多余的),但是有必要添加notify()接口。怎么做呢?试着找出是导致can_love改变的原因,是set_age()做的!因此,想要得到恋爱状态变化的通知,这些需要在set_age()中完成。

void set_age(const int value) const {if (age != value) {auto old_can_love = can_love();  // store old valueage = value;notify(*this, "age");if (old_can_love != can_love())  // check value has changednotify(*this, "can_love");}
}

set_age()里不仅检查年龄是否改变,也检查can_love是否改变并通知发出通知! 想象一下can_love依赖于两个字段,比如age和parent——这意味着它们的两个setter都必须处理can_love通知。更糟糕的是,如果年龄也会以这种方式影响其他10种属性呢? 这是一个不可用的解决方案!

当然,属性依赖关系可以被形式化为某种类型的map<string, vector<string>>。这将保留一个受属性影响的属性列表(或者相反,影响属性的所有属性)。遗憾的是,这个map必须手工定义,而且要与实际代码保持同步是相当棘手的。

取消订阅和线程安全

观察者如何从可观察对象中取消订阅?从观察者列表中删除即可,这在单线程场景中非常简单:

void unsubscribe(Observer<T>* observer) {observers.erase(remove(observers.begin(), observers.end(), observer), observers.end())
}

erase-remove的用法只在单线程场景中是正确的。vector不是线程安全的,所以同时调用subscribe()和unsubscribe()可能会导致意想不到的结果,因为这两个函数都会修改vector。

这很容易解决:只需对所有可观察对象的操作都加一个锁。这看起来很简单:

template <typename T>
struct Observable {void notify(T& source, const string& name) {scoped_lock<mutex> lock{mtx};...}void subscribe(Observer<T>* f) {scoped_lock<mutex> lock{mtx};...}void unsubscribe(Observer<T>* o) {scoped_lock<mutex> lock{mtx};...}private:vector<Observer<T>*> observers;mutex mtx;
};

另一个非常可行的替代方案是使用类似TPL/PPLconcurrent_ vector。当然,您会失去排序保证(换句话说,一个接一个地添加两个对象并不能保证它们按照那个顺序得到通知),但它肯定会让您不必自己管理锁。

可重入

最后一个实现通过在3个关键接口中加锁来保证线程安全。例如,有一个交通管理组件一直监视一个人,直到他18岁。当他们18岁时,组件取消订阅:

struct TrafficAdministration : Observer<Person> {void TrafficAdministration::field_changed(Person& source, const string& field_name) override {if (field_name == "age") {if (source.get_age() < 17)cout << "Whoa there, you are not old enough to drive!\n";else {// oh, ok, they are old enough, let's not monitor them anymorecout << "We no longer care!\n";source.unsubscribe(this);}}}
};

这将会出现一个问题,因为当某人17岁时,整个调用链将会是:

notify() --> field_changed() --> unsubscribe()

这是存在一个问题,因为在unsubscribe()中试图获取一个已经被获取的锁。这就是可重入问题。

  • 一种方法是简单地禁止这种情况

  • 另一种方法是放弃从集合中删除元素的想法。相反,我们可以这样写:

    void unsubscribe(Observer<T>* o) {auto it = find(observers.begin(), observers.end(), o);if (it != observers.end()) *it = nullptr;  // cannot do this for a set
    }
    

    随后,当使用notify()时,只需要进行额外的检查:

    void notify(T& source, const string& name) {for (auto&& obs : observes)if (obs) obs->field_changed(source, name);
    }
    
通过 Boost.Signals2 来实现 Observer

观察者模式有很多预打包的实现,并且可能最著名的是 Boost.Signals2 库。本质上,该库提供了一种称为信号的类型,它表示 C++ 中的信号术语(在别处称为事件)。可以通过提供函数或 lambda 表达式 来订阅此信号。它也可以被取消订阅,当你想通知它时,它可以被解除。

template <typename T>
struct Observable {signal<void(T&, const string&)> property_changed;
};

它的调用如下所示:

struct Person : Observable<Person> {void set_age(const int age) {if (this->age == age) return;this->age = age;property_changed(*this, "age");}
};

API 的实际使用将直接使用信号,当然,除非你决定添加更多 API 陷阱以使其更容易:

Person p{123};
auto conn = p.property_changed.connect([](Person&, const string& prop_name) {cout << prop_name << " has been changed" << endl;
});
p.set_age(20);  // name has been changed
// later, optionally
conn.disconnect();

connect() 调用的结果是一个连接对象,它也可以用于在你不再需要信号通知时取消订阅。

总结

毫无疑问,本章中提供的代码是一个明显的例子,它过度思考和过度设计了一个超出大多数人想要实现的问题的方式。

让我们回顾一下实现 Observer 时的主要设计决策:

  • 决定你希望你的 observable 传达什么信息。例如,如果你正在处理字段/属性更改,则可以包含属性名称。你还可以指定旧/新值,但传递类型可能会出现问题。

  • 你想让你的观察者成为tire class,还是你只需要一个虚函数列表?

  • 你想如何处理取消订阅的观察者?

    • 如果你不打算支持取消订阅——恭喜你,你将节省大量的实现观察者的工作,因为在重入场景中没有删除问题。
    • 如果你计划支持显式的 unsubscribe() 函数,你可能不想直接在函数中擦除-删除,而是将元素标记为删除并稍后删除它们。
    • 如果你不喜欢在(可能为空)裸指针上调度的想法,请考虑使用 weak_ptr 代替。
  • Observer<T> 的函数是否有可能是 从几个不同的线程调用?如果他们是,你需要保护你的订阅列表:

    • 你可以在所有相关函数上放置 scoped_lock;或者
    • 你可以使用线程安全的集合,例如 TBB/PPLcurrenct_vector。你将失去顺序保证。
  • 来自同一来源的多个订阅允许吗?如果是,则不能使用 std::set

遗憾的是,没有理想的 Observer 实现能够满足所有条件。 无论你采用哪种实现方式,都需要做出一些妥协。

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

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

相关文章

android get请求多个参数拼接工具

Android 多个参数拼接到请求地址中的小工具&#xff1a; public static String buildUrlWithParameters(String baseUrl, List<AssetGenesisBean> assetBeans) {StringBuilder urlBuilder new StringBuilder(baseUrl);if (assetBeans ! null && !assetBeans.isE…

服务器数据恢复—raid信息丢失导致RAID无法被识别的数据恢复案例

服务器数据恢复环境&故障&#xff1a; 某单位机房搬迁&#xff0c;将所有服务器和存储搬迁到新机房并重新连接线路&#xff0c;启动所有机器发现其中有一台服务器无法识别RAID&#xff0c;提示未做初始化操作。 发生故障的这台服务器安装LINUX操作系统&#xff0c;配置了NF…

vue3创建vite项目

一、创建vue3 vite项目&#xff1a; 命令行创建&#xff1a;npm create vitelatest vue3-tdly-demo -- --template vue (1)先进入项目文件夹&#xff0c;cd vue3-tdly-demo (2)之后执行&#xff0c; npm install (3)最后运行&#xff0c;npm run dev 将main.js文件内容改成…

React hook 之 useState

在组件的顶部定义状态变量&#xff0c;并传入初始值&#xff0c;确保当这些状态变量的值发生变化时&#xff0c;页面会重新渲染。 const [something,setSomething] useState(initialState); useState 返回一个由两个值组成的数组&#xff1a;1、当前的 state&#xff0c;在首次…

【leetcode】两数相加【中等】(C++递归解法)

总体来说&#xff0c;链表类问题往往是蛮适合用递归的方式求解的 要写出有效的递归&#xff0c;关键是要考虑清楚&#xff1a; 0. return的条件 1. 每步递归的操作&#xff0c;以及何时调用下一步递归 2. 鲁棒性&#xff08;头&#xff0c;尾结点等特殊情况是否依旧成立&am…

Windows 中配置 Python 3.11 环境安装教程

Python 是一门强大且广泛应用的编程语言。最新的 Python 3.11 提供了更多的功能和优化&#xff0c;本文将详细介绍如何在 Windows 中配置 Python 3.11 环境&#xff0c;并通过具体案例帮助您快速上手。 一、下载并安装 Python 3.11 1. 下载 Python 3.11 安装包 前往 Python …

Golang学习笔记20240725,Go语言基础语法

第一个Go程序 package mainimport "fmt"func main() {fmt.Println("hello world") }运行方式1&#xff1a; go run main.go运行方式2&#xff1a; go build .\hello_go.exe运行方式3&#xff1a;goland右键运行 字符串拼接 使用加号可以对字符串进行…

Codeforces Round 874 (Div. 3)(A~D题)

A. Musical Puzzle 思路: 用最少的长度为2的字符串按一定规则拼出s。规则是&#xff1a;前一个字符串的尾与后一个字符串的首相同。统计s中长度为2的不同字符串数量。 代码: #include<bits/stdc.h> #include <unordered_map> using namespace std; #define N 20…

【python】PyQt5中QPushButton的用法详细解析与应用实战

✨✨ 欢迎大家来到景天科技苑✨✨ &#x1f388;&#x1f388; 养成好习惯&#xff0c;先赞后看哦~&#x1f388;&#x1f388; &#x1f3c6; 作者简介&#xff1a;景天科技苑 &#x1f3c6;《头衔》&#xff1a;大厂架构师&#xff0c;华为云开发者社区专家博主&#xff0c;…

混淆后的代码报错如何定位问题

混淆后的代码报错定位问题可以通过以下步骤进行&#xff0c;主要依赖于ProGuard&#xff08;或R8&#xff09;生成的映射文件&#xff08;mapping file&#xff09;来将混淆后的代码还原成原始代码&#xff0c;以便调试和解决问题。 1. 启用混淆映射文件生成 确保在ProGuard配…

【全面介绍Python多线程】

🎥博主:程序员不想YY啊 💫CSDN优质创作者,CSDN实力新星,CSDN博客专家 🤗点赞🎈收藏⭐再看💫养成习惯 ✨希望本文对您有所裨益,如有不足之处,欢迎在评论区提出指正,让我们共同学习、交流进步! 🦇目录 1. 🦇前言2. 🦇threading 模块的基本用法3. 🦇Thre…

Unity中有关Animation的一点笔记

也许更好的阅读体验 Animation Unity中Animation类并不是直接记载了和播放动画有关的信息&#xff0c;可以简单理解Animation为一个动画播放器&#xff0c;播放的具体内容就像卡带一样&#xff0c;当我们有了卡带后我们可以播放动画。 对应的则是编辑器中的组件 所以Anima…

【学术会议征稿】第十一届电气工程与自动化国际会议 (IFEEA 2024)

第十一届电气工程与自动化国际会议 &#xff08;IFEEA 2024&#xff09; 2024 11th International Forum on Electrical Engineering and Automation IFEEA论坛属一年一度的国际学术盛会。因其影响力及重要性&#xff0c;IFEEA论坛自创建筹办以来&#xff0c;便受到国内外高等…

《昇思 25 天学习打卡营第 23 天 | 基于MindSpore的GPT-2文本摘要 》

《昇思 25 天学习打卡营第 23 天 | 基于MindSpore的GPT-2文本摘要 》 活动地址&#xff1a;https://xihe.mindspore.cn/events/mindspore-training-camp 签名&#xff1a;Sam9029 概述 文本摘要任务旨在从给定的文本中生成简短的摘要&#xff0c;同时保留关键信息。本案例使用…

网站打包封装成app,提高用户体验和商业价值

网站打包封装成app的优势 随着移动互联网的普及&#xff0c;用户对移动应用的需求越来越高。网站打包封装成app可以满足用户的需求&#xff0c;提高用户体验和商业价值。 我的朋友是一名电商平台的运营负责人&#xff0c;他曾经告诉我&#xff0c;他们的网站流量主要来自移动…

由bext安装“异常”引出的话题:windows上转义字符的工作原理

由bext安装“异常”引出的话题&#xff1a;Windows上转义字符的工作原理&#xff0c;与ai“闲扯”不经意学习知识点。 (笔记模板由python脚本于2024年07月25日 19:21:13创建&#xff0c;本篇笔记适合喜欢用ai学习的coder翻阅) 【学习的细节是欢悦的历程】 Python 官网&#xff…

GitLab添加TortoiseGIT生成SSH Key

文章目录 前言一、PuTTYgen二、GitLab 前言 GitLab是一个用于托管代码仓库和项目管理的Web平台&#xff0c;公司搭建自己的gitlab来管理代码&#xff0c;我们在clone代码的时候可以选择http协议&#xff0c;也可以选择ssh协议来拉取代码。 SSH (Secure Shell)是一种通过网络进…

【脚本】清空指定文件夹内容

main执行一次&#xff0c;1.txt就会写入一些东西。 原来的想法是覆盖重写&#xff0c;结果却是接着往后面写&#xff0c;检查源代码有点费事&#xff0c;不如在每次程序执行前&#xff0c;先直接清空文件夹&#xff01; 部分代码&#xff1a; 修改路径就能用。 import os im…

微信小程序-自定义tabBar

通过官网给出的示例自己实现了自定义的tabBar&#xff0c;但结果发现 无法监听页面生命周期函数 结语&#xff1a;原想的是实现不一样的效果&#xff08;如下&#xff09; 故尝试了自定义tabBar&#xff0c;虽然做出来了&#xff0c;但也发现这个做法存在不足&#xff1a; 在…

记一次Mycat分库分表实践

接了个活,又搞分库分表。 一、分库分表 在系统的研发过程中,随着数据量的不断增长,单库单表已无法满足数据的存储需求,此时就需要对数据库进行分库分表操作。 分库分表是随着业务的不断发展,单库单表无法承载整体的数据存储时,采取的一种将整体数据分散存储到不同服务…