【学习笔记】C/C++ 设计模式 - 观察者模式
前言
估计 2020 年写应用程序的机会比较多,之前一直在做嵌入式驱动程序和Android系统定制方面的工作,在应用程序方面积累的不是很多,因此迫切需要多学学应用编程这方面的知识。
之前在写小的应用程序的时候,总感觉会有更好的实现方式解耦,当时只是觉得要解决我所面临的瓶颈,可能需要找几个比较优秀的开源代码,多学习学习。因为一个偶然的机会接触设计模式之后,我嘞个去,这不就是可以解决困扰我很长时间的解耦问题的最佳解决方案吗?哈哈,在此写下自己的学习心得便于日后复习使用。
引用百度百科:软件设计模式(Design pattern),又称设计模式,是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性、程序的重用性。
观察者模式
简单介绍
观察者模式非常像我之前接触的 MQTT 协议中的发布/订阅机制,以及和 Android 中的广播机制。它们都是对某种消息\事件感兴趣就注册监听该消息\事件变化的场景。
借助该设计模式可以实现:产生事件的专注于事件检测逻辑,处理事件的专注于事件处理逻辑,不同的事件检测由不同的逻辑实现,不同的事件处理也由不同的事件处理逻辑实现,他们之间借助观察者模式即可实现联动。
因此可以将产生事件和处理事件的代码进行解耦隔离,各自处理互不干扰,这样实现的代码更加清晰明了和易于维护与扩展。如果我们在工作当中遇到类似的场景即可应用该设计模式。
应用场景
例:多对多,互联网领域,一个微博里面的某个明星即可认为是被观察者,关注该微博的粉色即可认为是观察者,当明星发布一条动态,那么所有关注其的粉丝都会收到该动态。多对多的场景,粉丝也可以同时关注其它的明星动态。(当然实际情况会更加复杂些,因为粉色自己也可以作为被观察者被人关注动态,这里只是举例说明,便于理解)
例:一对多,智能家居领域,需要实现离家安防模式,短信告警逻辑,视频录音录像逻辑,现场声光告警逻辑均可作为观察者,如借助人体红外感应实现的非法入侵检测逻辑功能,作为被观察者。当被观察者触发事件后(检测到非法入侵),那么就会触发短信告警、启动视频录音录像、启动现场喇叭和警示灯等功能逻辑。
原理说明
观察者设计模式是通过 C++ 面向对象的机制实现,主要是借助了继承的特性(继承权限、子类转换父类、纯虚函数),以及辅助用的模板类 list 实现。设计思想是首先编写两个类作为框架:
一个是被观察者类:
- 主要是维护一个观察者列表,用于记录有哪些观察者需要通知,该列表是 private 的,仅限于被观察者类框架自己维护。
- 提供增加和移除观察者的接口,用于登记和删除观察者,该接口是 public 的,所有对象都可以调用。
- 提供通知事件接口,用于通过遍历观察者列表,依次逐个调用观察者的事件回调来实现通知各个观察者,该接口是protected的,仅限于并且只应由派生类(即某个具体的被观察者)在产生事件时调用(这里决定了观察者回调函数不可阻塞和执行太复杂的代码,否则会影响后续的观察者事件通知不及时,也会影响被观察者的通知事件接口的执行事件时间长短)。
- 这里的回调主要是通过将继承了观察者的对象转换为其父类即观察者之后,再调用观察者的事件回调函数实现。
一个是观察者类:
- 提供一个事件回调纯虚函数,由某个具体的观察者继承实现,将会由被观察者产生事件的时候调用,由此得到通知。
- 可以增加一些额外的数据结构,如事件类型等等。
被观察者的具体实现
- 某个对象继承被观察者类。
- 产生某些事件时,其父类的通知事件接口,传入相应的参数即可。
观察者的具体实现
- 启动一个线程,利用队列和条件变量来让线程等待事件 。
- 某个对象继承观察者者类,并实现父类的事件回调的纯虚函数。
- 当事件触发时,将事件写入队列,并通过条件变量唤醒线程做业务处理。
示例代码
被观察者类框架
/**
******************************************************************************
* @文件 Subject.h
* @版本 V1.0.0
* @日期
* @概要 被观察者框架实现
* @作者 lmx
* @邮箱 lovemengx@qq.com
* @博客 https://me.csdn.net/lovemengx
******************************************************************************
**/
#ifndef __SUBJECT_H
#define __SUBJECT_H
#include <iostream>
#include <list>
#include <mutex>
#include "ObServer.h"
using namespace std;
// 被观察者(谁需要发布主题, 则继承并且在合适的时候调用通知方法)
class Subject
{
private: // 私有的, 由框架自己维护的数据结构, 主要用于维护观察者列表
mutex list_mutex;
list <ObServer*> observer_list;
protected: // 保护的, 由派生类使用的接口, 借此发布通知(仅对派生类可见, 但不公开, 避免被其它地方通过派生类使用)
int notice(ObServer::OBSERVER_EVENT_TYPE_E type, int argc);
public: // 公有的, 用于将观察者加入或移除观察列表的接口
int attach(ObServer* observer);
int detach(ObServer* observer);
};
#endif
/**
******************************************************************************
* @文件 Subject.cpp
* @版本 V1.0.0
* @日期
* @概要 被观察者的框架实现
* @作者 lmx
* @邮箱 lovemengx@qq.com
* @博客 https://me.csdn.net/lovemengx
******************************************************************************
**/
#include "Subject.h"
// 通知观察者
int Subject::notice(ObServer::OBSERVER_EVENT_TYPE_E type, int argc)
{
// 便利所有观察者对象, 并调用其 onEvent 方法来进行通知
// 如果某个观察者对象中的 onEvent 实现的代码出现阻塞或执行时间过长, 将会影响此循环执行时间
// 一种解决方式是为每个观察者事件回调函数创建一个线程来执行
// 另一种解决方式是要求具体的观察者对象各自维护一个事件队列和一个处理事件的线程, 在 onEvent 中将事件存入队列并唤醒处理该事件的线程
list_mutex.lock();
for (list<ObServer*>::iterator iter = observer_list.begin(); iter != observer_list.end(); ++iter)
{
(*iter)->onEvent(type, argc);
}
list_mutex.unlock();
return 0;
}
// 将指定的观察者附加到列表中
int Subject::attach(ObServer* observer)
{
if (NULL == observer)
return -1;
list_mutex.lock();
observer_list.push_back(observer);
list_mutex.unlock();
return 0;
}
// 将指定的观察者从列表中分离
int Subject::detach(ObServer* observer)
{
if (NULL == observer)
return -1;
list_mutex.lock();
observer_list.remove(observer);
list_mutex.unlock();
return 0;
}
观察者类框架
/**
******************************************************************************
* @文件 ObServer.h
* @版本 V1.0.0
* @日期
* @概要 观察者框架实现
* @作者 lmx
* @邮箱 lovemengx@qq.com
* @博客 https://me.csdn.net/lovemengx
******************************************************************************
**/
#ifndef __OBSERVER_H
#define __OBSERVER_H
// 观察者-谁需要观察则继承该类
class ObServer
{
public:
// 观察的事件类型
typedef enum {
OBSERVER_EVENT_TYPE_A,
OBSERVER_EVENT_TYPE_B,
}OBSERVER_EVENT_TYPE_E;
public:
// 当有事件变动时, 会由被观察者调用, 从而让观察者得到事件通知
virtual void onEvent(OBSERVER_EVENT_TYPE_E type, int argc) = 0;
};
#endif // !__SUBSCRIBE_H
具体的被观察者
/**
******************************************************************************
* @文件 SubjectTheme.h
* @版本 V1.0.0
* @日期
* @概要 被观察者的具体实现
* @作者 lmx
* @邮箱 lovemengx@qq.com
* @博客 https://me.csdn.net/lovemengx
******************************************************************************
**/
#ifndef __SUBJECT_THEME_H
#define __SUBJECT_THEME_H
#include "../Subject.h"
using namespace std;
// 某具体的被观察者
class SubjectTheme : public Subject
{
private:
// 利用线程模拟事件产生
bool mThreadTimerExitFlags;
thread mThreadTimer;
static void ThreadTimer(SubjectTheme* pSubjectTheme);
public:
void start();
void stop();
};
#endif
/**
******************************************************************************
* @文件 SubjectTheme.cpp
* @版本 V1.0.0
* @日期
* @概要 被观察者的具体实现
* @作者 lmx
* @邮箱 lovemengx@qq.com
* @博客 https://me.csdn.net/lovemengx
******************************************************************************
**/
#include <windows.h>
#include "SubjectTheme.h"
// 利用线程实现简单的定时产生事件的机制
void SubjectTheme::ThreadTimer(SubjectTheme* pSubjectTheme)
{
unsigned int count = 0;
// 间隔一定的时间出发一次事件通知
while (!pSubjectTheme->mThreadTimerExitFlags)
{
count++;
printf("SubjectTheme: notice...\n");
pSubjectTheme->notice(ObServer::OBSERVER_EVENT_TYPE_A, count);
Sleep(1000);
}
return;
}
// 启动模拟
void SubjectTheme::start()
{
printf("SubjectTheme: start...\n");
mThreadTimerExitFlags = false;
mThreadTimer = thread(ThreadTimer, this);
return;
}
// 停止模拟
void SubjectTheme::stop()
{
printf("SubjectTheme: stop...\n");
mThreadTimerExitFlags = true;
mThreadTimer.join();
return;
}
具体的观察者1
/**
******************************************************************************
* @文件 ObServerSubscribeA.h
* @版本 V1.0.0
* @日期
* @概要 某具体对象作为观察者的示例
* @作者 lmx
* @邮箱 lovemengx@qq.com
* @博客 https://me.csdn.net/lovemengx
******************************************************************************
**/
#ifndef __OBSERVER_SUBSCRIBEA_H
#define __OBSERVER_SUBSCRIBEA_H
#include "../ObServer.h"
class ObServerSubscribeA: public ObServer
{
private:
// 通过 Observer 继承
virtual void onEvent(OBSERVER_EVENT_TYPE_E type, int argc) override;
};
#endif
/**
******************************************************************************
* @文件 ObServerSubscribeA.h
* @版本 V1.0.0
* @日期
* @概要 某具体对象作为观察者的示例
* @作者 lmx
* @邮箱 lovemengx@qq.com
* @博客 https://me.csdn.net/lovemengx
******************************************************************************
**/
#include <iostream>
#include "ObServerSubscribeA.h"
// 通过 Observer 继承
void ObServerSubscribeA::onEvent(OBSERVER_EVENT_TYPE_E type, int argc)
{
printf("ObServerSubscribeA: onEvent type:%d argc:%d\n", type, argc);
}
具体的观察者2
/**
******************************************************************************
* @文件 ObServerSubscribeB.h
* @版本 V1.0.0
* @日期
* @概要 某具体对象作为观察者的示例
* @作者 lmx
* @邮箱 lovemengx@qq.com
* @博客 https://me.csdn.net/lovemengx
******************************************************************************
**/
#ifndef __OBSERVER_SUBSCRIBEB_H
#define __OBSERVER_SUBSCRIBEB_H
#include "../ObServer.h"
class ObServerSubscribeB : public ObServer
{
private:
// 通过 Observer 继承
virtual void onEvent(OBSERVER_EVENT_TYPE_E type, int argc) override;
};
#endif
/**
******************************************************************************
* @文件 ObServerSubscribeB.h
* @版本 V1.0.0
* @日期
* @概要 某具体对象作为观察者的示例
* @作者 lmx
* @邮箱 lovemengx@qq.com
* @博客 https://me.csdn.net/lovemengx
******************************************************************************
**/
#include <iostream>
#include "ObServerSubscribeB.h"
// 通过 Observer 继承
void ObServerSubscribeB::onEvent(OBSERVER_EVENT_TYPE_E type, int argc)
{
printf("ObServerSubscribeB: onEvent type:%d argc:%d\n", type, argc);
}
单元测试代码
/**
******************************************************************************
* @文件 test_ObServer.h
* @版本 V1.0.0
* @日期
* @概要 观察者设计模式单元测试
* @作者 lmx
* @邮箱 lovemengx@qq.com
* @博客 https://me.csdn.net/lovemengx
******************************************************************************
**/
#include <iostream>
#include <windows.h>
#include "test_ObServer.h"
#include "Theme/SubjectTheme.h"
#include "Subscribe/ObServerSubscribeA.h"
#include "Subscribe/ObServerSubscribeB.h"
void TestObServer::test(void)
{
SubjectTheme mSubjectTheme;
ObServerSubscribeA mObServerSubscribeA;
ObServerSubscribeB mObServerSubscribeB;
mSubjectTheme.attach(&mObServerSubscribeA);
mSubjectTheme.attach(&mObServerSubscribeB);
mSubjectTheme.start();
Sleep(10 * 1000);
mSubjectTheme.stop();
return;
}
执行结果

【学习笔记】C/C++ 设计模式 - 观察者模式的更多相关文章
- 再起航,我的学习笔记之JavaScript设计模式02
我的学习笔记是根据我的学习情况来定期更新的,预计2-3天更新一章,主要是给大家分享一下,我所学到的知识,如果有什么错误请在评论中指点出来,我一定虚心接受,那么废话不多说开始我们今天的学习分享吧! 我们 ...
- 再起航,我的学习笔记之JavaScript设计模式01
我的学习笔记是根据我的学习情况来定期更新的,预计2-3天更新一章,主要是给大家分享一下,我所学到的知识,如果有什么错误请在评论中指点出来,我一定虚心接受,那么废话不多说开始我们今天的学习分享吧! 在通 ...
- 再起航,我的学习笔记之JavaScript设计模式03
我的学习笔记是根据我的学习情况来定期更新的,预计2-3天更新一章,主要是给大家分享一下,我所学到的知识,如果有什么错误请在评论中指点出来,我一定虚心接受,那么废话不多说开始我们今天的学习分享吧! 上一 ...
- 再起航,我的学习笔记之JavaScript设计模式04
我的学习笔记是根据我的学习情况来定期更新的,预计2-3天更新一章,主要是给大家分享一下,我所学到的知识,如果有什么错误请在评论中指点出来,我一定虚心接受,那么废话不多说开始我们今天的学习分享吧! 上回 ...
- 再起航,我的学习笔记之JavaScript设计模式05(简单工程模式)
我的学习笔记是根据我的学习情况来定期更新的,预计2-3天更新一章,主要是给大家分享一下,我所学到的知识,如果有什么错误请在评论中指点出来,我一定虚心接受,那么废话不多说开始我们今天的学习分享吧! 前几 ...
- 再起航,我的学习笔记之JavaScript设计模式06(工厂方法模式)
上一次已经给大家介绍了简单工厂模式,相信大家对创建型设计模式有了初步的了解,本次我将给大家介绍的是工厂方法模式. 工厂方法模式 工厂方法模式(Factory Method):通过对产品类的抽象使其创建 ...
- 再起航,我的学习笔记之JavaScript设计模式06(抽象工厂模式)
我的学习笔记是根据我的学习情况来定期更新的,预计2-3天更新一章,主要是给大家分享一下,我所学到的知识,如果有什么错误请在评论中指点出来,我一定虚心接受,那么废话不多说开始我们今天的学习分享吧! 前两 ...
- 再起航,我的学习笔记之JavaScript设计模式08(建造者模式)
我的学习笔记是根据我的学习情况来定期更新的,预计2-3天更新一章,主要是给大家分享一下,我所学到的知识,如果有什么错误请在评论中指点出来,我一定虚心接受,那么废话不多说开始我们今天的学习分享吧! 前几 ...
- 再起航,我的学习笔记之JavaScript设计模式09(原型模式)
我的学习笔记是根据我的学习情况来定期更新的,预计2-3天更新一章,主要是给大家分享一下,我所学到的知识,如果有什么错误请在评论中指点出来,我一定虚心接受,那么废话不多说开始我们今天的学习分享吧! 我们 ...
- 再起航,我的学习笔记之JavaScript设计模式07(抽象工厂模式)
我的学习笔记是根据我的学习情况来定期更新的,预计2-3天更新一章,主要是给大家分享一下,我所学到的知识,如果有什么错误请在评论中指点出来,我一定虚心接受,那么废话不多说开始我们今天的学习分享吧! 前两 ...
随机推荐
- VBA工程设置密码
VBA 工程设置密码 Alt + F11,进入程序界面: 工具---> VBAProject属性---> 保护---> 查看时锁定工程前打勾,并在下面的密码区输入密码.
- 我的Vue之旅 10 Gin重写后端、实现页面详情页 Mysql + Golang + Gin
第三期 · 使用 Vue 3.1 + Axios + Golang + Mysql + Gin 实现页面详情页 使用 Gin 框架重写后端 Gin Web Framework (gin-gonic.c ...
- springboot集成支付宝的支付(easy版)
SpringBoot对接支付宝 需要先注册账号 到支付宝开发者平台创建网页支付应用 启用公钥模式 需要使用到appId和下面的两个秘钥 写配置信息的代码 1.引入依赖 <dependency&g ...
- 【Virt.Contest】CF1215(div.2)
第二次打虚拟赛. CF 传送门 T1:Yellow Cards 黄色卡片 中规中矩的 \(T1\). 首先可以算出一个人也不罚下时发出的最多黄牌数: \(sum=a1*(k1-1)+a2*(k2-1) ...
- vue3的学习笔记:MVC、Vue3概要、模板、数据绑定、用Vue3 + element ui、react框架实现购物车案例
一.前端MVC概要 1.1.库与框架的区别 框架是一个软件的半成品,在全局范围内给了大的约束.库是工具,在单点上给我们提供功能.框架是依赖库的.Vue是框架而jQuery则是库. 1.2.MVC(Mo ...
- Android开发之应用更新或软件下载
Android开发之应用更新或软件下载 本文章学习前提:okHttp3或以上,EventBus或其它事件总线工具,四大组件的Activity和Service,安卓通知基础知识 新建项目文件 目录结构如 ...
- 云原生之旅 - 12)使用 Kaniko 在 Kubernetes上构建 Docker 容器镜像
前言 前一篇文章[云原生之旅 - 11)基于 Kubernetes 动态伸缩 Jenkins Build Agents]有讲到在 Kubernetes Pod (Jenkins build agent ...
- 使用vite + vue3 + ant-design-vue + vue-router + vuex 创建一个后台管理应用
使用vite + vue3 + ant-design-vue + vue-router + vuex 创建一个管理应用的记录 使用vite 创建项目 我创建的node 版本是 v16.17.1 使用N ...
- Pinely Round 1 (Div. 1 + Div. 2)
比赛链接 A 题意 构造两个长为 \(n\) 排列,使得两排列有长为 \(a\) 公共前缀和长为 \(b\) 的公共后缀. 题解 知识点:构造. 注意到,当 \(a+b\leq n-2\) 时,中间段 ...
- X活手环的表盘自定义修改
文章用到的所有工具及软件成品 前言 前几天我在某宝买了一个智能手环,无奈软件中的表盘太少,所有我想着修改一下app中的资源文件. 反编译APK 这里反编译APK用apktool工具就可以. apkto ...