摘要:
it人员无论是使用哪种高级语言开发东东,想要更高效有层次的开发程序的话都躲不开三件套:数据结构,算法和设计模式。设计模式是软件开发人员在软件开发过程中面临的一般问题的解决方案,使用设计模式是为了重用代码、让代码更容易被他人理解、保证代码可靠性。
设计模式坚持七大原则:开闭原则,单一职责原则,里氏替换原则,依赖倒转原则,接口隔离原则,迪米特原则,合成复用原则,而各项设计模式又区分为三大模式,创建型模式,结构型模式,行为模式。
此系列专注讲解C++开发过程中高需求好用的设计模式类型,能更好的简练项目架构和代码量,通过使用场景以及代码实现来更好的介绍。本文介绍的是观察者模式Publish-Subscribe。
(开发环境:VSCode,GCC13.2.0,cmake3.8)
关键词
: C++,设计模式,观察者模式,Design pattern,Publish-Subscribe
声明:
本文作者原创,转载请附上文章出处与本文链接。
正文:
介绍
观察者模式(Observer Pattern)是一种常见且广泛使用的行为型设计模式,它定义了一种一对多的依赖关系,使得当一个对象(被观察者)的状态发生变化时,所有依赖于它的对象(观察者)都会得到通知并自动更新,这种设计模式有时也被称为发布-订阅模式。
使用场景
我们可以将观察者模式理解为,我们订阅一份报纸。
首先决定订阅报纸的时候,需要去报刊告诉相关工作人员;填写自己订阅的信息,比如想要订阅什么类型的报纸(体育类、财经类等),之后留下自己的住址(这样就可以享受送报上门服务了)。
类似以上场景需求就可以用到观察者模式,例如图形用户界面(GUI)监听模型的状态变化,又或者事件处理系统在事件发生时通知所有注册的事件监听器。
通俗来讲就是一种消息传递机制,能在一定程度上降低I/O传输的负荷,对于时时数据变动的数据源不适用,但对于需要监控,而数据不会时时变动的数据源适用,在此之前做过的CEF相关项目中就使用到此技巧。
注意事项
- 性能问题:如果被观察者状态变化频繁或观察者数量众多,可能导致通知操作消耗大量时间和资源,影响系统性能。
- 循环依赖:观察者之间可能存在循环依赖,导致系统难以理解和维护。
- 通知顺序问题:在某些情况下,观察者的通知顺序可能很重要,但观察者模式本身并不保证通知的顺序。
- 缺乏状态变化细节:观察者模式只通知观察者状态发生了变化,但不提供状态变化的具体细节,可能需要观察者自行查询。
代码实现
最少需要有订阅者,发布者,事件处理中心(注册注销机制,消息轮询),存储数据的结构可以采用STL中的容器,这样可以更多的关注于业务代码的封装与实现。以下通过在CEF项目使用的C++代码来说明模式的搭建实现,代码较为成熟可应付大部分场景需求,在此例子中发布者与事件处理中心统归为一个类,不过功能区分明显耦合度不高。
定义事件与数据:
可订阅的数据结构为ISSUEDATA
,包含三个数据可任意类型的value,数值是否有变以及是否被订阅,也即是<key,value>
中的value
;初始化可供订阅者订阅的数据为isString,isNum,isBool
,也即是<key,value>
中的key
。设置在了CommunicateBase
发布者类内。
struct ISSUEDATA
{
ISSUEDATA() : isChange(true), isObserver(false) {}
ISSUEDATA operator () (std::any _value)
{
this->value = _value;
return *this;
}
std::any value; // 可任意类型的value
bool isChange; // 是否有变
bool isObserver; // 是否被订阅
};
// ....................................
// 初始化数据
void CommunicateBase::_InitData()
{
ISSUEDATA issue;
data.emplace(std::make_pair(std::string("isString"), issue(std::string("giegie"))));
data.emplace(std::make_pair(std::string("isNum"), issue(double(0.0))));
data.emplace(std::make_pair(std::string("isBool"), issue(bool(true))));
}
订阅者:
使用了单例类方便调用,MessageRouter
通过unordered_map
容器CallbackuMap
保存哪个订阅者订阅了哪些数据,然后当数据更新及对应数据传送到MessageRouter
时通过Dispatch
函数检索CallbackuMap
容器显示更新数据。
// MessageRouter.h
#pragma once
#include <iostream>
#include <string>
#include <unordered_map>
#include "include/cef_app.h"
#include "include/cef_v8.h"
#include <list>
typedef struct
{
std::string msgName;
CefRefPtr<CefV8Context> context;
CefRefPtr<CefV8Value> callback;
}MSGCALL; // 可自定义数据结构体,有context、callback即可
// 键:订阅对象(isString...);值:list结构体(CEF相关的一种消息传递,也即订阅者)
typedef std::unordered_map<std::string, std::list<MSGCALL>> CallbackuMap;
class MessageRouter
{
public:
MessageRouter() = default;
MessageRouter(const MessageRouter&) = delete;
MessageRouter& operator=(const MessageRouter&) = delete;
// 创建-使用数据库单例 lazy_load
static MessageRouter* GetInstance()
{
static std::once_flag s_flag;
std::call_once(s_flag, [&]()
{
messageRouter = new MessageRouter();
});
return messageRouter;
}
// 释放单例
static void Release()
{
if (messageRouter != NULL)
{
messageRouter->callback_umap.clear();
delete messageRouter;
messageRouter = NULL;
}
}
// 获取umap
CallbackuMap* GetUnorderMap() { return &callback_umap; }
// 发射-消息池
bool Dispatch(std::string, CefRefPtr<CefValue>);
private:
CallbackuMap callback_umap;
static MessageRouter* messageRouter;
};
// MessageRouter.cpp
#include "MessageRouter.h"
// 初始化静态成员
MessageRouter* MessageRouter::messageRouter = NULL;
bool MessageRouter::Dispatch(std::string _msgstr, CefRefPtr<CefValue> _value)
{
auto itor = callback_umap.find(_msgstr);
if (itor != callback_umap.end())
{
CefV8ValueList arguments;
CefRefPtr<CefV8Value> msgstr;
CefRefPtr<CefV8Value> value;
// 判断传输过来的数据的数据类型生成相应的V8类型
if (VTYPE_STRING == _value->GetType())
value = CefV8Value::CreateString(_value->GetString());
else if (VTYPE_DOUBLE == _value->GetType())
value = CefV8Value::CreateDouble(_value->GetDouble());
else if (VTYPE_BOOL == _value->GetType())
value = CefV8Value::CreateBool(_value->GetBool());
else
return false;
for (MSGCALL msgcall : itor->second)
{
msgstr = CefV8Value::CreateString(msgcall.msgName);
arguments.push_back(msgstr);
arguments.push_back(value);
msgcall.context->Enter();
CefRefPtr<CefV8Value> retval = msgcall.callback->ExecuteFunction(nullptr, arguments);
if (retval.get())
{
if (retval->IsBool())
bool handled = retval->GetBoolValue();
}
msgcall.context->Exit();
}
return true;
}
return false;
}
发布者:
也使用了单例模式方便调用,CommunicateBase
与MessageRouter
在CEF中处于不同进程,一个GUI,一个数据底层,但也维持一定程度的数据联通(IPC),MessageRouter
可以把哪些数据(isString,isNum,isBool)
被订阅传给CommunicateBase
,CommunicateBase
则可以把被订阅的数据更新及被订阅数据更新后的value
传递给MessageRouter
以供显示。
CommunicateBase
通过定时器timer
,以及PollingDataPub
函数进行轮询,根据存放数据容器data
内isChange,isObserver
两个标准位判断是否发消息给MessageRouter
;也能通过增、删、改、查、订阅5个成员函数修改数据容器data
。
// CommunicateBase.h 数据中台:集合数据,处理数据,发放数据,(绑定数据源)
#pragma once
#include <memory>
#include <mutex>
#include <iostream>
#include <any>
#include <unordered_map>
#include <include/cef_app.h>
#include "EventDefinition.h" // 通用函数方法集合
#include "CTimer.h" // 定时器类
#include "include/base/cef_callback.h"
#include "include/cef_task.h"
#include "include/wrapper/cef_closure_task.h"
struct ISSUEDATA
{
ISSUEDATA() : isChange(true), isObserver(false) {}
ISSUEDATA operator () (std::any _value)
{
this->value = _value;
return *this;
}
std::any value; // 可任意类型的value
bool isChange; // 是否有变
bool isObserver; // 是否被订阅
};
typedef std::unordered_map<std::string, ISSUEDATA> iter;
class CommunicateBase
{
public:
CommunicateBase() = default;
CommunicateBase(const CommunicateBase&) = default;
CommunicateBase& operator=(const CommunicateBase&) = default;
// 创建-使用数据库单例 lazy_load
static CommunicateBase* GetInstance()
{
static std::once_flag s_flag;
std::call_once(s_flag, [&]()
{
communicatebase = new CommunicateBase();
communicatebase->_InitData();
});
return communicatebase;
}
// 释放资源
static void Release()
{
if (communicatebase != NULL)
{
communicatebase->GetUnorderMap()->clear();
delete timer;
timer = NULL;
delete communicatebase;
communicatebase = NULL;
}
}
// 获取data/timer
std::unordered_map<std::string, ISSUEDATA>* GetUnorderMap() { return &data; }
static CTimer* GetTimer() { return timer; }
// 增、删、改、查、订阅
bool EmplaceData(std::string, std::any); // true成功,false失败
int EraseData(std::string); // 1成功,0失败
bool UpdateData(std::string, std::any, bool = false); // true成功,false失败
bool FindData(std::string, std::any&, bool = false); // true成功,false失败
bool UpdateObr(std::string, bool); // 订阅和取消订阅
void PollingDataPub(CefRefPtr<CefFrame>); // 轮询订阅数据是否发送改变
static CTimer* timer;
private:
// 初始化数据
void _InitData();
// 处理发生改变的订阅数据
bool _PollingDataSwitch(CefRefPtr<CefFrame>, std::string, std::any);
static CommunicateBase* communicatebase;
std::unordered_map<std::string, ISSUEDATA> data;
};
事件处理中心:
void CommunicateBase::PollingDataPub(CefRefPtr<CefFrame> _frame)
{
iter* data = CommunicateBase::GetInstance()->GetUnorderMap();
for (auto& [k, v] : (*data))
{
if (!v.isChange || !v.isObserver)
continue;
v.isChange = false;
_PollingDataSwitch(_frame, k, v.value);
}
}
bool CommunicateBase::_PollingDataSwitch(CefRefPtr<CefFrame> _frame, std::string _key, std::any _value)
{
/*
* 子线程一直调用该函数
* 查询被订阅数据是否更新
* 如果更新则判断更新的类型
* 向 Renderer 进程发送更新后的数据
*/
switch (hash_str_to_uint32(_key.c_str())) // hash_str_to_uint32、 _hash 是哈希转换功能,加快switch字符串匹配速度
{
case "isString"_hash:
{
std::string value = std::any_cast<std::string>(_value);
/*
* 发送给 Renderer 消息事件参数为:
* 消息名: PUB, 参数: ["isString", std::any_cast<std::string>]
*/
CefRefPtr<CefProcessMessage> msg = CefProcessMessage::Create("PUB");
CefRefPtr<CefListValue> msgArgs = msg->GetArgumentList();
msgArgs->SetString(0, _key);
msgArgs->SetString(1, value);
_frame->SendProcessMessage(PID_RENDERER, msg);
}break;
case "isBool"_hash:
{
bool value = std::any_cast<bool>(_value);
CefRefPtr<CefProcessMessage> msg = CefProcessMessage::Create("PUB");
CefRefPtr<CefListValue> msgArgs = msg->GetArgumentList();
msgArgs->SetString(0, _key);
msgArgs->SetBool(1, value);
_frame->SendProcessMessage(PID_RENDERER, msg);
}break;
case "isNum"_hash:
{
double value = std::any_cast<double>(_value);
CefRefPtr<CefProcessMessage> msg = CefProcessMessage::Create("PUB");
CefRefPtr<CefListValue> msgArgs = msg->GetArgumentList();
msgArgs->SetString(0, _key);
msgArgs->SetDouble(1, value);
_frame->SendProcessMessage(PID_RENDERER, msg);
}break;
default:
break;
}
return true;
}
// 在CommunicateBase当下进程内的一个线程启动此轮询
CommunicateBase::GetInstance()->timer = new CTimer();
CommunicateBase::GetInstance()->timer->Start(55, std::bind(&CommunicateBase::PollingDataPub, CommunicateBase::GetInstance(),frame));
由于是在CEF开发环境中,并且处于不同进程下,所以代码中还存在着一些进程交互相关,但剔除替换掉并不影响,只要满足必要的因素就可以设计出观察者模式。
推荐阅读
博客主页:https://blog.youkuaiyun.com/weixin_45068267
(客官逛一逛,有许多其它有趣的专栏博文)
C/C++专栏:https://blog.youkuaiyun.com/weixin_45068267/category_12268204.html
(内含其它设计模式的介绍和实现)