引言

写这篇博客的原因就是在看了陈硕前辈的muduo后对Observer的一些思考.

首先简单说说Observer模式,先来看看它的定义:
定义对象间的一种一对多(变化)的依赖关系,以便当一个对象(Subject)的状态发生改变时,所有依赖于它的对象都得到通知并自动更新。
我觉得这其实说不上是一种模式,而是一种很自然的思考方式,一个subject,多个Observer,需求就是当subject的某个条件完成的时候对注册的observer进行通知,那么我们该如何实现呢,一般给出的解决方案是这样的,

(图片来源 侵删)

即给出一个subject(Observable)的基类,然后再给出一个observer的基类(接口),我们要做的只是用一个发布者的对象去继承subject,然后再用一个订阅者的类去继承(实现)Observer,这个时候一个Observer模式的基础就已经OK了,暂不考虑其中的线程安全问题,接下来我们要做的其实就是把Observer这个类加到subject中,然后在条件满足时进行执行即可,我们要做的仅仅是用实现几个函数而已,截止目前为止,面向对象表现良好.对象范式的基本概念之一就是对象之间互相发送消息,协作完成任务,消息就我们想成函数调用,那么我们需要知道这个从类实例化出来的对象是否有这么一个成员函数,这个时候我们得知道其地址,然后知道其类型,从而知道如何调用这个函数,这个时候面向对象不就成了面向类了吗?这个问题先放下,我们再回到Observer模式,从上面所说的解决方案反推,我需要实现一个Observer对象,即被通知的对象,我得继承Observer基类才可以,不然根本无法被存储在subject中,更何谈被通知呢,这导致了如果观察者如果想要观察多个消息,那就得去继承,多继承出现了,随着多继承的引入,自然得打上虚继承这个补丁,如果想要一个对象被通知不同的事件,举个简单的例子,subject条件为A的时候通知我,subject条件为B的时候也通知我,那么我们不得不写一系列条件判断,多么丑陋写法啊,不止丑陋,同样低效,当这个数字上升100倍时意味着每个update函数中有着大量的无效代码,且使类变得臃肿,但好像别无方法,着就是这么写的缺陷所在.程序的本质是程序员对于这个世界的抽象,各种模式当然也是对事物行为的抽象,那么Observer模式本身就当然没有问题,那么为什么会出现上面所说的情况呢?以此角度来看好像就是我们对于面向对象的理解出现了偏差.这个偏差就是把面向对象范式理解成了面向类,站在对象的角度来看我只是想和你通信啊,为什么要强加继承这层关系呢?这就出现了上面的问题.但是这并不意味着类这个概念有什么不对的,只是不能粗鲁的把两者混为一谈罢了.

解决这个问题就需要一个对象级别的通信机制,C++中的实现的机制就是function,bind,lambda与closure,利用这个机制我们可以轻松的解决上面提到的问题,具体的代码在陈硕前辈的muduo中有一个很好的实现,就拿其做一个样例:

template<typename Callback>
struct SlotImpl;template<typename Callback>
struct SignalImpl : boost::noncopyable{using Slotlist = std::vector<std::weak_ptr<SlotImpl<Callback>>>;SignalImpl() : slots_(new Slotlist){}void CopyOnWrite(){ //用于修改时不进行插入if(!slots_.unique()){slots_.reset(new Slotlist(*slots_));}}void clean(){std::lock_guard<std::mutex> guard(mutex_);CopyOnWrite();Slotlist& list(*slots_);auto current = list.begin();while(current != list.end()){if(current->expired()){current = list.erase(current);}else{++current;}}}std::shared_ptr<Slotlist> slots_;std::mutex mutex_;
};template<typename Callback>
struct SlotImpl : boost::noncopyable{using Data = SignalImpl<Callback>;std::weak_ptr<Data> data_;Callback cb_;std::weak_ptr<void> tie_;bool tied_;SlotImpl(const std::shared_ptr<Data>& data, Callback&& cb):data_(data), cb_(cb), tie_(), tied_(false){}SlotImpl(const std::shared_ptr<Data>& data, Callback&& cb, const std::shared_ptr<void>& tie):data_(data), cb_(cb), tie_(tie), tied_(true){}~SlotImpl(){auto data(data_.lock());if(data){data->clean();}}
};using Slot = std::shared_ptr<void>;template<typename Signature> //用一个参数搞出两个类型
class Signal;template<typename RET, typename... ARGS>
class Signal<RET(ARGS...)> : boost::noncopyable{ //特化版本
public:using Callback = std::function<void(ARGS...)>;using SignalComponent = SignalImpl<Callback>;using SlotComponent = SlotImpl<Callback>;Signal() : pool(new SignalComponent()){}Slot connect(Callback&& func){std::shared_ptr<SlotComponent> slot(new SlotComponent(pool, std::forward<Callback>(func)));add(slot);return slot;}Slot connect(Callback&& func, const std::shared_ptr<void>& tie){ //控制生命周期std::shared_ptr<SlotComponent> slot(new SlotComponent(pool, std::forward<Callback>(func), tie));add(slot);return slot;}void call(ARGS&&... args){ //显然也可以是voidSignalComponent& impl(*pool); //不增加引用计数的拷贝std::shared_ptr<typename SignalComponent::Slotlist> slots;{   std::lock_guard<std::mutex> guard(pool->mutex_);//std::lock_guard<std::mutex> guard(impl.mutex_);slots = impl.slots_; }typename SignalComponent::Slotlist& List(*slots); //直接指针使用也可for(auto item : List){std::shared_ptr<SlotComponent> slot = item.lock();if(slot){std::shared_ptr<void> flag;if(slot->tied_){flag = slot->tie_.lock();if(flag){slot->cb_(args...);}}else {slot->cb_(args...);}}}}private:void add(const std::shared_ptr<SlotComponent>& slot){SignalComponent& impl(*pool);{std::lock_guard<std::mutex> guard(impl.mutex_);impl.CopyOnWrite();impl.slots_->push_back(slot);}}std::shared_ptr<SignalComponent> pool;
};

简单的解释一下上面的代码,我们的发布者成了Signal,在初始化时需要指定一个接收的函数类型,值得一提的是这个类型是一个可被强制转换的类型,即这里把返回值指定为void,已接受更广泛的对象,你可能会发牢骚,那返回值怎么办,我们知道多线程接收返回值future是个不错选择,当然这种情况下promise也可以解决你的问题.我们继续回到这个代码.SlotImpl就是观察者,不过我们并不需要管它而已,相当于一个wrapper,我们只需要传入一个可调用对象而已,然后在SignalImpl中管理SlotImpl,其中用到了weak_ptr和Copy-on-write,这个链接中有提及这个copy_on_write的实现,weak_ptr的使用是为了保证多线程析构时的线程安全,这并不是本篇文章的重点.

其实这段代码本身是比较能说明问题,这是一个线程安全的Observer的实现,且避开了文章上面提到的一般Observer模式的写法会遇到的问题,其实这样看来我们完全可以对Observer模式下一个定义,即一对多回调.

补充一篇文章,写下这篇博客以后遇到的一篇文章,基本和我的这篇博客说了同一个问题,但是主题更加精炼,即接口与回调

参考:
https://blog.csdn.net/u011814346/article/details/71413142
https://blog.csdn.net/fly_wt/article/details/90896170
muduo

Observer之谬何在?相关推荐

  1. 当析构函数遇到多线程 ── C++ 中线程安全的对象回调

    陈硕 (giantchen_AT_gmail) 本文 PDF  下载: http://www.cppblog.com/Files/Solstice/dtor_meets_mt.pdf 摘要 编写线程安 ...

  2. c语言多线程造成的崩溃,C++多线程析构函数引起程序崩溃解析.pdf

    C多线程析构函数引起程序崩溃解析 当析构函数遇到多线程 ── C++ 中线程安全的对象回调 陈硕 (giantchen_AT_gmail) B/Solstice 摘要 编写线程安全的类不是难事,用同步 ...

  3. 我如何在GitHub Project上获得1,000个星星,以及在此过程中学到的教训

    by Andrea Bizzotto 通过安德里亚·比佐托(Andrea Bizzotto) 我如何在GitHub Project上获得1,000个星星,以及在此过程中学到的教训 (How I got ...

  4. [区块链] 如何在Corda上写一个派发股息的Dapp

    如何在Corda上寫一個派发股息的Dapp 前言 架构 1. CreateStock 发行股票 Observer 观察员 2. MoveStock 转移股票 3. AnnounceDividend 派 ...

  5. Vue中关于[__ob__:Observer]数据问题

    [ob: Observer]=> 这些数据是vue这个框架对数据设置的监控器,一般都是不可枚举的. 操作数据的过程中不要删除这些属性: 因为你已经将数据绑定在了vue之中,vue就肯定要为数据添 ...

  6. unity editor android 黑屏_如何在Unity中利用nReal制作AR应用

    来源:新浪VR nReal眼镜是今年最有趣的增强现实小工具之一.它们已经在CES上展示过了,几个月前笔者在北京亲自试用过,在我的评测中,我强调了它们不仅非常轻.时尚,而且还能提供非常明亮的全息视图. ...

  7. 如何在CPU上优化GEMM矩阵乘法

    如何在CPU上优化GEMM矩阵乘法 How to optimize GEMM on CPU (TL;DR) TVM 提供抽象接口,允许用户分别描述算法和算法的实现组织(所谓的调度).通常,在高性能调度 ...

  8. 如何在 GPU 上优化卷积

    如何在 GPU 上优化卷积 将演示如何在 TVM 中编写高性能卷积实现.正方形大小的输入张量和过滤器为例,假设卷积的输入具有大batch批量.在这个例子中,使用不同的布局存储数据,实现更好的数据局部性 ...

  9. 如何在 CPU 上优化 GEMM

    如何在 CPU 上优化 GEMM (TL;DR) TVM 提供抽象接口,允许用户分别描述算法和算法的实施组织(所谓的调度).通常,在高性能调度中编写算法,会破坏算法的可读性和模块化.尝试各种看似有前途 ...

最新文章

  1. ARKIT/ARCore对比分析(一)
  2. 虚拟机软件之vmware workstation安装篇
  3. log--求自然对数
  4. 【杂谈】如何让你的2020年秋招CV项目经历更加硬核,可深入学习有三秋季划4大领域32个方向(2020.7.23号后涨价)
  5. C++中虚函数工作原理和(虚)继承类的内存占用大小计算
  6. aix升级openssh_AIX5.3如何安装openssh | 学步园
  7. do与mysql数据类型对照_dophon-db: dophon框架的数据库模块,支持mysql,sqlite数据库,带有orm持久化功能与链式操作实例,贴近逻辑习惯,支持mysql多数据源配置...
  8. php 编译 iconv错误,php编译错误:configure: error: Please reinstall the iconv library.
  9. JS调试的时候遇到无限debugger怎么办?
  10. PHP判断用户是否登录
  11. access性别字段_12、ACCESS数据表的筛选(ACCESS图解操作系列)
  12. 【C++】【GADL】读取栅格数据获取信息
  13. 2022手机号段大全、归属运营商整理—2022.01.04更新(包含三大运营商)
  14. Java 最常见的 100+ 面试题:金三银四必备
  15. SSL-ZYC 最小步数
  16. Xneomai 简介
  17. 树莓派(二) adb命令控制手机拨打/接听电话
  18. 详解C语言最快关键字——register
  19. 切比雪夫不等式例题讲解_「高中数学」柯西不等式,最全解析,高考必备,搞定最后十分...
  20. oracle的当前日期,Oracle 获取当前日期及日期格式

热门文章

  1. 数据库3 表记录的插入、修改和删除
  2. Java面试题-尚硅谷版
  3. LInux查看CPU GPU温度
  4. 第二章 2.3 一阶逻辑等值式与前束范式
  5. 用Python 爬虫爬取贴吧图片
  6. 你上一次忍住没揍产品经理是什么时候?
  7. C#下生成CSR证书签名请求
  8. 花10年时间学程序就能做好吗?
  9. 跨境卖家一定要了解亚马逊红人计划!
  10. Android:使用ZXing生成二维码(支持添加Logo图案)