我们已经讨论了在 C++ 中使用反应式编程模型的很多内容。到目前为止,我们已经了解了 RxCpp 库及其编程模型、RxCpp 库的关键元素以及反应式 GUI 编程。
在本章中,我们将涵盖以下主题:
- 图案和图案运动介绍
- 设计模式和反应式编程
- 一些反应式编程模式和习惯用法
面向对象编程 ( OOP )已经达到了临界质量,这要归功于 90 年代初优秀 C++ 编译器的大量涌现。20 世纪 90 年代初的程序员经常努力理解面向对象程序设计,以及如何在大型项目中有效地使用它。没有互联网这样的病毒媒介,这是一场相当艰难的斗争。早期采用者发表技术报告,在期刊/期刊上写作,并举办研讨会来普及 OOP 技术。像《多布博士杂志》和《C++ 报告》这样的杂志过去都有面向对象的专栏。
需要将专家的智慧传递给不断增长的编程社区,但这种知识传播并没有发生。德国传奇数学家卡尔·弗里德里希·高斯说过:“一个人总是向大师们学习。尽管高斯心中有数学,但他的说法对任何非平凡的人类努力都是正确的。然而,以前,面向对象技术的大师很少,学徒模式没有很好地扩展。
James Coplien published an influential book entitled Advanced C++ Styles and Idioms, which dealt with the low-level patterns (idioms) associated with usage of the C++ programming language. Even though it is not widely cited, authors consider it a notable book for cataloging the best practices and techniques for OOP.
埃里希·伽马从一位名叫克里斯托弗·亚历山大的建筑建筑师那里获得灵感,开始着手设计一个图案目录,作为他博士论文的一部分。克里斯托弗·亚历山大的《城镇和建筑的模式》是埃里希·伽马灵感的来源。在此之后,拥有类似想法的人,即拉尔夫·约翰逊、约翰·弗利西德斯和理查德·赫尔姆,与埃里希·伽马联手创建了一个包含 23 种设计模式的目录,现在被亲切地称为四人帮 ( GOF )设计模式。爱迪生·韦斯利在 1994 年出版了《设计模式:可重用面向对象软件的元素》一书。这很快成为程序员的一个很好的参考,并推动了面向模式的软件开发。GOF 目录主要集中在软件设计上。
1996 年,西门子的一群工程师出版了《面向模式的软件架构》一书,主要关注构建系统的架构方面。约翰·威利父子出版的五本书记录了整个 POSA 模式目录。加入该小组的还有道格拉斯·施密特,他是自适应通信环境 ( ACE )网络编程库和 TAO(ACE ORB)的创建者。他后来成为对象管理组 ( OMG )的主席,该组开发、采用和维护标准,如 CORBA 和 UML。
在前两项倡议之后,活动如潮水般涌来;其他值得注意的模式目录如下:
- 企业应用架构的模式,马丁·福勒等著。
- 企业整合模式,作者:格雷格·霍普和波比·沃尔夫。
- 核心 J2EE 模式,Deepak Alur 等人。
- 领域驱动设计,埃里克·埃文斯。
- 企业模式和 MDA ,作者吉姆·阿洛和伊拉·纽斯塔德。
尽管这些书本身意义重大,但它们倾向于当时新兴的企业软件开发领域。对于 C++ 开发人员来说,GOF 目录和 POSA 目录是最重要的。
模式是软件设计中常见问题的命名解决方案。模式通常被编目在某种模式库中。其中一些是作为书籍出版的。最受欢迎和广泛使用的模式目录是 GOF。
以目录创造者命名的“四人帮”(g of)发起了模式运动。创作者主要集中于设计和构建面向对象的软件。克里斯托弗·亚历山大的思想被借用到软件工程学科,并被应用到应用架构、并发性、安全性等方面。“四人帮”将目录分为结构模式、创造模式和行为模式。原著用 C++ 和 Smalltalk 来解释这些概念。这些模式已经在当今大多数编程语言中得到移植和利用。看看这张桌子:
| Sl。编号 | 图案类型 | 图案 | | one | 创造模式 | 抽象工厂,构建器,工厂方法,原型,单例 | | Two | 结构模式 | 适配器、桥、复合材料、装饰器、门面、飞轮、代理 | | three | 行为模式 | 责任链、命令、解释器、迭代器、中介器、纪念品、观察者、状态、策略、模板方法、访问者 |
我们相信,对于任何程序员来说,很好地理解 g of 模式都是必要的。这些模式无处不在,与应用领域无关。GOF 模式帮助我们以语言不可知的方式交流和推理系统。它们在。NET 和 Java 世界。Qt 框架广泛利用了 GOF 存储库中的模式,给出了一个直观的编程模型。
软件架构的模式(五卷)是一个有影响力的书系列,它涵盖了开发任务关键系统的大多数适用模式。该目录适用于编写大型软件关键任务子系统的人员,尤其是数据库引擎、分布式系统、中间件系统等。目录的另一个优点是它非常适合 C++ 程序员。
下表列出了涵盖五个已发布卷的目录:
| Sl。号码 | 模式类型 | 模式 | | one | 建筑学的 | 层、管道和过滤器、黑板、代理、MVC、表示-抽象-控制、微内核、反射 | | Two | 设计 | 整体-部分、主从、代理、命令处理器、视图处理器、转发器-接收器、客户端-调度器-服务器、发布者-订阅者 | | three | 服务访问和配置模式 | 包装器外观、组件配置器、拦截器、扩展接口 | | four | 事件处理模式 | 反应器、主动方、异步完成令牌、接受方连接器 | | five | 同步模式 | 范围锁定,策略锁定,线程安全接口,双重检查锁定优化 | | six | 并发模式 | 活动对象、监控对象、半同步/半异步、领导者/追随者、线程特定存储 | | seven | 资源获取模式 | 查找,惰性获取,急切获取,部分获取 | | eight | 资源生命周期 | 缓存、池、协调器、资源生命周期管理器 | | nine | 资源释放模式 | 租赁,驱逐者 | | Ten | 分布式计算的模式语言 | 分布式编程环境中不同目录模式的合并 | | Eleven | 论模式和模式语言 | 最后一卷给出了一些关于模式、模式语言和用法的元信息 |
需要研究 POSA 目录,以深入了解部署在世界各地的大规模系统的架构基础。我们认为,尽管这个目录很重要,但它没有得到应有的重视。
GOF 模式和反应式编程有更深层次的联系,这从表面上看是显而易见的。GOF 模式主要与编写基于面向对象的程序有关。反应式编程是关于函数式编程、流编程和并发性的。我们已经了解到反应式编程涵盖了经典 GOF 观测器模式中的一些缺陷(在第 5 章、观测器介绍的第一节中,我们涵盖了这一缺陷)。
OOP 程序基本上是关于建模层次的,从模式世界来看,复合模式是建模部分/整体层次的方式。无论哪里有组合,访问者实现的集合都会随之而来。换句话说,复合访问者二人组是编写面向对象系统的规范模式。
访问者实现应该对复合结构有所了解。随着访问者数量的激增,使用访问者模式的行为处理变得困难。此外,向处理中添加转换和过滤器会使问题进一步复杂化。
输入迭代器模式,这有利于序列、流或项目列表的导航。使用对象/函数编程结构,我们可以非常容易地过滤和转换序列。微软的语言集成查询和 Java 中的 Lambda/Streams 处理(8 及以上)就是迭代器模式的很好例子。
现在,我们将如何将分层数据转换为线性结构?大多数层次结构可以展平成一个流,以便进一步处理。最近,人们开始做以下事情:
- 使用复合模式建模它们的层次结构。
- 使用访问者将层次结构展平成一个序列。
- 使用迭代器模式导航这些序列。
- 在对序列执行操作之前,对它们应用一系列转换和过滤器。
前面的方法称为编程的pull
方法。消费者或客户端从事件或数据源中提取数据进行处理。该方案存在以下问题:
- 数据被不必要地拉入客户端。
- 转换和过滤器应用于事件接收器端。
- 事件接收器可以阻止服务器。
- 这种风格不适合异步处理,因为异步处理中的数据会随着时间的推移而变化。
这个问题的一个好的解决方案是反向凝视,其中数据作为流从服务器异步推送,事件接收器将对流做出反应。这种系统的另一个优点是在事件源端放置转换和过滤器。这导致只有绝对重要的数据需要在接收器端处理的情况。
方案如下:
- 数据以流的形式处理,这被称为可观测值。
- 我们可以对它们应用一系列运算符,或者更高阶的运算符。
- 一个运算符总是接受一个可观测值,然后返回另一个可观测值。
- 我们可以订阅一个可观察的通知。
- 观察者有标准的机制来处理它们。
在这一节中,我们学习了 OOP 模式和反应式编程是如何密切相关的。两种范例的明智混合产生了高质量、可维护的代码。我们还讨论了如何转换面向对象设计模式(复合/访问者)来利用迭代器模式。我们讨论了如何通过轻微的推动来改进迭代方案(事件源端的一个火了就忘了的习惯用法)。在下一节中,我们将通过编写代码来演示整个技术。
尽管设计模式运动与面向对象程序设计相一致,而反应式编程与面向对象程序设计相一致,但它们之间有着密切的相似之处。在前一章中,我们学习了以下内容:
- 面向对象模型有利于对系统的结构方面进行建模。
- FP 模型很适合对系统的行为方面进行建模。
为了说明 OOP 和反应式编程之间的联系,我们将编写一个程序,该程序将遍历目录来枚举给定文件夹中的文件和子文件夹。
我们将创建一个包含以下内容的复合结构:
- 一个
FileNode
(继承自EntryNode
)建模文件信息 - 一个
DirectoryNode
(继承自EntryNode
)建模文件夹信息
在定义了前面的组合之后,我们将为以下内容定义访问者:
- 打印文件名和文件夹名
- 将复合层次结构转换为文件名列表
不用多说,让我们进入正题。看看这段代码:
//---------- DirReact.cpp
#include <rxcpp/rx.hpp>
#include <memory>
#include <map>
#include <algorithm>
#include <string>
#include <vector>
#include <windows.h> // This is omitted in POSIX version
#include <functional>
#include <thread>
#include <future>
using namespace std;
////////////////////////////////////
//-------------- Forward Declarations
//-------------- Model Folder/File
class FileNode;
class DirectoryNode;
////////////////////////////////
//------------- The Visitor Interface
class IFileFolderVisitor;
前面的正向声明是为了防止编译器在编译程序时发出错误和警告。FileNode
存储文件名及其大小作为实例变量。DirectoryNode
存储文件夹名称和FileNode
列表,以指示目录中的文件和文件夹。FileNode
/ DirectoryNode
层级由IFileFolderVisitor
界面处理。我们可以看到如下声明:
/////////////////////////////////
//------ a Type to store FileInformation
struct FileInformation{
string name;
long size;
FileInformation( string pname,long psize )
{ name = pname;size = psize; }
};
//////////////////////////////
//-------------- Base class for File/Folder data structure
class EntryNode{
protected:
string name;
int isdir;
long size;
public:
virtual bool Isdir() = 0;
virtual long getSize() = 0;
virtual void Accept(IFileFolderVisitor& ivis)=0;
virtual ~EntryNode() {}
};
当我们创建复合时,我们需要创建一个节点类,作为层次结构中所有成员的基类。在我们的例子中,EntryNode
类就是这样做的。我们将文件或文件夹的名称、大小等存储在基类中。除了应该由派生类实现的三个虚函数之外,我们还有一个虚拟析构函数。虚拟析构函数的存在确保了治疗中的析构函数被正确调用,如下所示:
//-------------The Visitor Interface
class IFileFolderVisitor{
public:
virtual void Visit(FileNode& fn )=0;
virtual void Visit(DirectoryNode& dn )=0;
};
每当我们使用复合模式样式实现定义层次结构时,我们就定义一个访问者接口来处理层次结构中的节点。对于层次结构中的每个节点,在访问者界面中都会有一个visit
方法。组合的类层次结构中的每个节点都有一个accept
方法,访问者接口将调用分派给相应节点的accept
方法。accept
方法将呼叫调度回访问者中正确的visit
方法。这个过程叫做双派单:
// The Node which represents Files
class FileNode : public EntryNode {
public:
FileNode(string pname, long psize) { isdir = 0; name = pname; size = psize;}
~FileNode() {cout << "....Destructor FileNode ...." << name << endl; }
virtual bool Isdir() { return isdir == 1; }
string getname() { return name; }
virtual long getSize() {return size; }
virtual void Accept( IFileFolderVisitor& ivis ){ivis.Visit(*this);}
};
FileNode
类只是存储文件的名称和大小,存储在节点中。该类还实现了基类(EntryNode
)中声明的所有虚拟方法。accept
方法将调用重定向到正确的访问者级别方法,如下所示:
// Node which represents Directory
class DirectoryNode : public EntryNode {
list<unique_ptr<EntryNode>> files;
public:
DirectoryNode(string pname)
{ files.clear(); isdir = 1; name = pname;}
~DirectoryNode() {files.clear();}
list<unique_ptr<EntryNode>>& GetAllFiles() {return files;}
bool AddFile( string pname , long size) {
files.push_back(unique_ptr<EntryNode> (new FileNode(pname,size)));
return true;
}
bool AddDirectory( DirectoryNode *dn ) {
files.push_back(unique_ptr<EntryNode>(dn));
return true;
}
bool Isdir() { return isdir == 1; }
string getname() { return name; }
void setname(string pname) { name = pname; }
long getSize() {return size; }
void Accept( IFileFolderVisitor& ivis ){ivis.Visit(*this); }
};
DirectoryNode
类用文件和子文件夹的列表来建模一个文件夹。我们使用智能指针来存储条目。像往常一样,我们也实现了所有与EntryNode
类相关的虚拟功能。方法AddFile
和AddDirectory
意在填充列表。当使用操作系统特定的函数遍历目录时,我们用前面两种方法填充目录:
//------Directory Helper Has to be written for Each OS
class DirHelper {
public:
static DirectoryNode *SearchDirectory(const std::string& refcstrRootDirectory){
//--------------- Do some OS specific stuff to retrieve
//--------------- File/Folder hierarchy from the root folder
return DirNode;
}};
DirHelper
逻辑在 Windows 和 GNU Linux/macOS X 之间有所不同,我们从书中省略了源代码。相关网站包含上述程序的完整源代码。基本上,程序递归遍历目录来填充数据结构,如下所示:
/////////////////////////////////////
//----- A Visitor Interface that prints
//----- The contents of a Folder
class PrintFolderVisitor : public IFileFolderVisitor
{
public:
void Visit(FileNode& fn ) {cout << fn.getname() << endl; }
void Visit(DirectoryNode& dn ) {
cout << "In a directory " << dn.getname() << endl;
list<unique_ptr<EntryNode>>& ls = dn.GetAllFiles();
for ( auto& itr : ls ) { itr.get()->Accept(*this);}
}
};
PrintFolderVisitor
类是一个访问者实现,它将文件和文件夹信息吐到控制台。该类演示了如何为组合实现基本访问者。在我们的例子中,组合只有两个节点,编写访问者实现非常容易。在某些情况下,层次结构中的节点类型数量众多,编写一个访问者实现并不容易。为访问者编写过滤器和转换可能会很困难,而且逻辑是临时的。让我们编写一个程序来打印文件夹的内容。这是:
void TestVisitor( string directory ){
// Search files including subdirectories
DirectoryNode *dirs = DirHelper::SearchDirectory(directory);
if ( dirs == 0 ) {return;}
PrintFolderVisitor *fs = new PrintFolderVisitor ();
dirs->Accept(*fs); delete fs; delete dirs;
}
前面的函数递归遍历一个目录并创建一个组合(DirectoryNode *
)。我们使用PrintFolderVisitor
打印文件夹的内容,如下图所示:
int main(int argc, char *argv[]) { TestVisitor("D:\Java"); }
访问者实现必须对组合的结构有所了解。在复合实现的某些情况下,将有许多访问者需要实现。此外,在访问者界面的情况下,在节点上应用转换和过滤器有点困难。GOF 模式目录有一个迭代器模式,可以用来导航一系列项目。问题是:我们如何使用迭代器模式来线性化处理层次结构?大多数层次结构都可以通过编写访问者实现来展平为列表、序列或流。让我们为上述任务编写一个扁平化的访问者。
看看下面的代码:
// Flatten the File/Folders into a linear list
class FlattenVisitor : public IFileFolderVisitor{
list <FileInformation> files;
string CurrDir;
public:
FlattenVisitor() { CurrDir = "";}
~FlattenVisitor() { files.clear();}
list<FileInformation> GetAllFiles() { return files; }
void Visit(FileNode& fn ) {
files.push_back( FileInformation{ CurrDir +"\" + fn.getname(),fn.getSize()));
}
void Visit(DirectoryNode& dn ) {
CurrDir = dn.getname();
files.push_back( FileInformation( CurrDir, 0 ));
list<unique_ptr<EntryNode>>& ls = dn.GetAllFiles();
for ( auto& itr : ls ) { itr.get()->Accept(*this);}
}
};
FlattenVisitor
类收集 STL 列表中的文件和文件夹。对于每个目录,我们遍历文件列表并发出accept
方法。使用熟悉的双分派,让我们编写一个函数,返回一个FileInformation
列表供我们迭代。下面是代码:
list<FileInformation> GetAllFiles(string dirname ){
list<FileInformation> ret_val;
// Search files including subdirectories
DirectoryNode *dirs =DirHelper::SearchDirectory(dirname);
if ( dirs == 0 ) {return ret_val;}
FlattenVisitor *fs = new FlattenVisitor();
dirs->Accept(*fs);
ret_val = fs->GetAllFiles();
delete fs; delete dirs;
return ret_val;
}
int main(int argc, char *argv[]) {
list<FileInformation> rs = GetAllFiles("D:\JAVA");
for( auto& as : rs )
cout << as.name << endl;
}
FlattenVisitor
类遍历DirectoryNode
层次结构,并将完全扩展的路径名收集到 STL 列表结构中。一旦我们将层次线性化为一个列表,我们就可以迭代它。
我们已经学习了如何将层次结构建模为复合结构,并最终将其展平为适合用迭代器模式导航的形式。在下一节中,我们将学习迭代器如何被转换成可观察的。我们将使用 RxCpp 通过使用火来实现可观察对象,并忘记模型,将值从源推送到接收器。
迭代器模式是从 STL 容器、生成器和流中提取数据的标准机制。它们非常适合空间中聚合的数据。本质上,这意味着我们提前知道应该检索多少数据,或者数据已经被捕获。在有些情况下,数据异步到达,消费者不知道有多少数据,也不知道数据何时到达。在这种情况下,迭代器需要等待,或者我们需要借助超时策略来处理场景。在这种情况下,基于推送的方法似乎是更好的选择。利用 Rx 的主题结构,我们可以使用一种先发制人的策略。让我们编写一个发出目录内容的类,如下所示:
//////////////////////////////
// A Toy implementation of Active
// Object Pattern...
template <class T>
struct ActiveObject {
rxcpp::subjects::subject<T> subj;
// fire-and-forget
void FireNForget(T & item){subj.get_subscriber().on_next(item);}
rxcpp::observable<T> GetObservable()
{ return subj.get_observable(); }
ActiveObject(){}
~ActiveObject() {}
};
///////////////////////
// The class uses a FireNForget mechanism to
// push data to the Data/Event sink
//
class DirectoryEmitter {
string rootdir;
//-------------- Active Object ( a Pattern in it's own right )
ActiveObject<FileInformation> act; // more on this below
public:
DirectoryEmitter(string s ) {
rootdir = s;
//----- Subscribe
act.GetObservable().subscribe([] ( FileInformation item ) {
cout << item.name << ":" << item.size << endl;
});
}
bool Trigger() {
std::packaged_task<int()> task([&]() { EmitDirEntry(); return 1; });
std::future<int> result = task.get_future();
task();
//------------ Uncomment the below line
//------------ to return immediately
double dresult = result.get();
return true;
}
//----- Iterate over the list of files
//----- uses ActiveObject Pattern to do FirenForget
bool EmitDirEntry() {
list<FileInformation> rs = GetAllFiles(rootdir);
for( auto& a : rs ) { act.FireNForget(a); }
return false;
}
};
int main(int argc, char *argv[]) {
DirectoryEmitter emitter("D:\JAVA");
emitter.Trigger(); return 0;
}
DirectoryEmitter
类使用现代 C++ 的packaged_task
构造以一种火了就忘的方式进行异步调用。在前面的列表中,我们正在等待结果(使用std::future<T>
)。我们可以取消订单,立即返回。
我们已经了解到,反应式编程就是处理随时间变化的值。他们以可观察的概念为中心。有两种变体,如下所示:
- 单元格:单元格是一个实体(变量或内存位置),其中的值会随着时间的推移定期更新。在某些情况下,它们也被称为属性或行为。
- 流:流代表事件流。它们是经常与动作相关联的数据。当人们想到可观测值时,他们得到了可观测值的流变体。
我们将实现一个玩具版的细胞编程模式。我们将只专注于实现基本功能。代码需要整理以供生产使用。
如果我们正在实现一个单元控制器类,当每个单元发生变化时,每个单元都会通知,那么下面的实现可以被优化。然后,单元控制器类可以通过计算表达式来更新依赖关系。这个实现展示了单元模式如何成为独立计算的可行机制:
//------------------ CellPattern.cpp
#include <rxcpp/rx.hpp>
#include <memory>
#include <map>
#include <algorithm>
using namespace std;
class Cell
{
private:
std::string name;
std::map<std::string,Cell *> parents;
rxcpp::subjects::behavior<double> *behsubject;
public:
string get_name() { return name;}
void SetValue(double v )
{ behsubject->get_subscriber().on_next(v);}
double GetValue()
{ return behsubject->get_value(); }
rxcpp::observable<double> GetObservable()
{ return behsubject->get_observable(); }
Cell(std::string pname) {
name = pname;
behsubject = new rxcpp::subjects::behavior<double>(0);
}
~Cell() {delete behsubject; parents.clear();}
bool GetCellNames( string& a , string& b )
{
if ( parents.size() !=2 ) { return false; }
int i = 0;
for(auto p : parents ) {
( i == 0 )? a = p.first : b = p.first;
i++ ;
}
return true;
}
/////////////////////////////
// We will just add two parent cells...
// in real life, we need to implement an
// expression evaluator
bool Recalculate() {
string as , bs ;
if (!GetCellNames(as,bs) ) { return false; }
auto a = parents[as];
auto b = parents[bs];
SetValue( a->GetValue() + b->GetValue() );
return true;
}
bool Attach( Cell& s ) {
if ( parents.size() >= 2 ) { return false; }
parents.insert(pair<std::string,Cell *>(s.get_name(),&s));
s.GetObservable().subscribe( [=] (double a ) { Recalculate() ;});
return true;
}
bool Detach( Cell& s ) { //--- Not Implemented
}
};
单元格类假设每个单元格都有两个父依赖项,只要父值发生变化,单元格的值就会被重新计算。我们实现了一个加法运算符(以保持列表的小)。recalculate
方法实现逻辑,如下图所示:
int main(int argc, char *argv[]) {
Cell a("a");
Cell b("b");
Cell c("c");
Cell d("d");
Cell e("e");
//-------- attach a to c
//-------- attach b to c
//-------- c is a + b
c.Attach(a);
c.Attach(b);
//---------- attach c to e
//---------- attach d to e
//---------- e is c + d or e is a + b + d;
e.Attach(c);
e.Attach(d);
a.SetValue(100); // should print 100
cout << "Value is " << c.GetValue() << endl;
b.SetValue(200); // should print 300
cout << "Value is " << c.GetValue() << endl;
b.SetValue(300); // should print 400
cout << "Value is " << c.GetValue() << endl;
d.SetValue(-400); // should be Zero
cout << "Value is " << e.GetValue() << endl;
}
主程序演示了我们如何使用单元模式将更改向下传播到依赖项中。通过更改值,我们强制重新计算从属单元格中的值。
活动对象是一个将方法调用和方法执行解耦的类,非常适合激发和忘记异步调用。附加到类的调度程序处理执行请求。该模式由以下六个要素组成:
- 一个代理,它为具有公共可访问方法的客户端提供了一个接口
- 定义活动对象上的方法请求的接口
- 来自客户端的挂起请求列表
- 调度程序,决定下一步执行什么请求
- 活动对象方法的实现
- 客户端接收结果的回调或变量
我们将剖析活动对象模式的一个实现。这个程序是为了说明而写的;对于生产使用,我们需要使用更复杂一点的。尝试产品质量实现会使代码变得相当长。让我们看看代码:
#include <rxcpp/rx.hpp>
#include <memory>
#include <map>
#include <algorithm>
#include <string>
#include <vector>
#include <windows.h>
#include <functional>
#include <thread>
#include <future>
using namespace std;
//------- Active Object Pattern Implementation
template <class T>
class ActiveObject {
//----------- Dispatcher Object
rxcpp::subjects::subject<T> subj;
protected:
ActiveObject(){
subj.get_observable().subscribe([=] (T s )
{ Execute(s); });
}
virtual void Execute(T s) {}
public:
// fire-and-forget
void FireNForget(T item){ subj.get_subscriber().on_next(item);}
rxcpp::observable<T> GetObservable() { return subj.get_observable(); }
virtual ~ActiveObject() {}
};
前面的实现声明了subject<T>
类的一个实例,作为一个通知机制。FireNForget
方法通过调用get_subscriber
方法将值放入主题中。方法立即返回,订阅方法将检索该值并调用Execute
方法。这个类应该被一个具体的实现覆盖。让我们看看代码:
class ConcreteObject : public ActiveObject<double> {
public:
ConcreteObject() {}
virtual void Execute(double a ) { cout << "Hello World....." << a << endl;}
};
int main(int argc, char *argv[]) {
ConcreteObject temp;
for( int i=0; i<=10; ++ i )
temp.FireNForget(i*i);
return 0;
}
前面的代码片段调用了带有双精度值的FireNForget
方法。在控制台上,我们可以看到正在打印的值。被覆盖的Execute
方法被自动调用。
顾名思义,贷款模式将资源贷款给你的职能部门。它执行以下步骤:
- 它创建了一个您可以使用的资源
- 它将资源借给将要使用它的功能
- 这个函数由调用者传递
- 资源被破坏了
下面的代码实现了资源管理的资源借出模式。该模式有助于在编写代码时避免资源泄漏:
//----------- ResourceLoan.cpp
#include <rxcpp/rx.hpp>
using namespace std;
//////////////////////////
// implementation of Resource Loan Pattern. The Implementation opens a file
// and does not pass the file handle to user defined Lambda. The Ownership remains with
// the class
class ResourceLoan {
FILE *file;
string filename;
public:
ResourceLoan(string pfile) {
filename = pfile;
file = fopen(filename.c_str(),"rb");
}
////////////////////////////
// Read upto 1024 bytes to a buffer
// return the buffer contents and number of bytes
int ReadBuffer( std::function<int(char pbuffer[],int val )> func )
{
if (file == nullptr ) { return -1; }
char buffer[1024];
int result = fread (buffer,1,1024,file);
return func(buffer,result);
}
//---------- close the resource
~ResourceLoan() { fclose(file);}
};
////////////////////////////////
// A Sample Program to invoke the preceding
// class
//
int main(int argc, char *argv[]) {
ResourceLoan res("a.bin");
int nread ;
//------------- The conents of the buffer
//------------- and size of buffer is stored in val
auto rlambda = [] (char buffer[] , int val ) {
cout << "Size " << val << endl;
return val;
};
//------- The File Handle is not available to the
//------- User defined Lambda
while ((nread = res.ReadBuffer(rlambda)) > 0) {}
//---- When the ResourceLoan object goes out of scope
//---- File Handle is closed
return 0;
}
资源贷款模式适合避免资源泄漏。资源的持有者从不把资源的句柄或指针交给消费者。主程序演示了我们如何使用实现。
事件总线充当事件源和事件接收器之间的中介。事件源或生产者将事件发送到总线,订阅事件的类(消费者)将得到通知。模式可以是中介模式的一个实例。在事件总线实现中,我们有以下内容:
- 产生者:产生事件的类
- 消费者:消费事件的类
- 控制器:作为生产者和消费者的类
在接下来的实现中,我们省略了控制器的实现。下面的代码实现了一个事件总线:
//----------- EventBus.cpp
#include <rxcpp/rx.hpp>
#include <memory>
#include <map>
#include <algorithm>
using namespace std;
//---------- Event Information
struct EVENT_INFO{
int id;
int err_code;
string description;
EVENT_INFO() { id = err_code = 0 ; description ="default";}
EVENT_INFO(int pid,int perr_code,string pdescription )
{ id = pid; err_code = perr_code; description = pdescription; }
void Print() {
cout << "id & Error Code" << id << ":" << err_code << ":";
cout << description << endl;
}
};
EVENT_INFO
结构建模一个事件,它有以下内容:
Id
:事件标识err_code
:错误代码description
:事件描述
代码的其余部分相当明显;这是:
//----------- The following method
//----------- will be invoked by
//----------- Consumers
template <class T>
void DoSomeThingWithEvent( T ev )
{ev.Print();}
//---------- Forward Declarations
template <class T>
class EventBus;
//------------- Event Producer
//------------- Just Inserts event to a Bus
template <class T>
class Producer {
string name;
public:
Producer(string pname ) { name = pname;}
bool Fire(T ev,EventBus<T> *bev ) {
bev->FireEvent(ev);
return false;
}
};
生产者类的实现相当简单。骨架实现相当琐碎。Fire
方法以兼容的EventBus<T>
为参数,调用EventBus<T>
类的FireEvent
方法。生产实现需要一些花哨的东西。让我们看看代码:
//------------ Event Consumer
//------------ Subscribes to a Subject
//------------ to Retrieve Events
template <class T>
class Consumer {
string name;
//--------- The subscription member helps us to
//--------- Unsubscribe to an Observable
rxcpp::composite_subscription subscription;
public:
Consumer(string pname) { name = pname;}
//--------- Connect a Consumer to a Event Bus
bool Connect( EventBus<T> *bus ) {
//------ If already subscribed, Unsubscribe!
if ( subscription.is_subscribed() )
subscription.unsubscribe();
//------- Create a new Subscription
//------- We will call DoSomeThingWithEvent method
//------- from Lambda function
subscription = rxcpp::composite_subscription();
auto subscriber = rxcpp::make_subscriber<T>(
subscription,[=](T value){
DoSomeThingWithEvent<T>(value);
},[](){ printf("OnCompletedn");});
//----------- Subscribe!
bus->GetObservable().subscribe(subscriber);
return true;
}
//-------- DTOR ....Unsubscribe
~Consumer() { Disconnect(); }
bool Disconnect() {
if (subscription.is_subscribed() )
subscription.unsubscribe();
}
};
Consumer<T>
的功能性相当明显。Connect
方法的工作是在EventBus<T>
课上订阅主题的可观察的一面。每次新的连接请求到来时,现有订阅都会被取消订阅,如下所示:
//--- The implementation of the EventBus class
//--- We have not taken care of Concurrency issues
//--- as our purpose is to demonstrate the pattern
template <class T>
class EventBus
{
private:
std::string name;
//----- Reference to the Subject...
//----- Consumers get notification by
//----- Subscribing to the Observable side of the subject
rxcpp::subjects::behavior<T> *replaysubject;
public:
EventBus<T>() {replaysubject = new rxcpp::subjects::behavior<T>(T());}
~EventBus() {delete replaysubject;}
//------ Add a Consumer to the Bus...
bool AddConsumer( Consumer<T>& b ) {b.Connect(this);}
//------ Fire the Event...
bool FireEvent ( T& event ) {
replaysubject->get_subscriber().on_next(event);
return true;
}
string get_name() { return name;}
rxcpp::observable<T> GetObservable()
{ return replaysubject->get_observable(); }
};
EventBus<T>
充当生产者和消费者之间的管道。我们在引擎盖下使用一个replaysubject
,来通知消费者,如下图所示:
/////////////////////
//The EntryPoint
//
//
int main(int argc, char *argv[]) {
//---- Create an instance of the EventBus
EventBus<EVENT_INFO> program_bus;
//---- Create a Producer and Two Consumers
//---- Add Consumers to the EventBus
Producer<EVENT_INFO> producer_one("first");
Consumer<EVENT_INFO> consumer_one("one");
Consumer<EVENT_INFO> consumer_two("two");
program_bus.AddConsumer(consumer_one);
program_bus.AddConsumer(consumer_two);
//---- Fire an Event...
EVENT_INFO ev;
ev.id = 100;
ev.err_code = 0;
ev.description = "Hello World..";
producer_one.Fire(ev,&program_bus);
//---- fire another by creating a second
//---- Producer
ev.id = 100;
ev.err_code = 10;
ev.description = "Error Happened..";
Producer<EVENT_INFO> producer_two("second");
producer_two.Fire(ev,&program_bus);
}
在主功能中,我们执行以下任务:
- 创建
EventBus<T>
的实例 - 创建生产者的实例
- 创建消费者实例
- 将事件发送到总线
我们只介绍了适合编写反应式程序的设计模式的一个子集。首先,我们的重点是将 GOF 设计模式与反应式编程世界联系起来。事实上,作者认为反应式编程模型是经典 GOF 设计模式的增强实现。由于现代编程语言增加了函数式编程结构,增强是可能的。事实上,对象/函数编程是编写现代 C++ 代码的好方法。这一章主要是基于这个想法。
在这一章中,我们深入研究了设计模式和习惯用法的奇妙世界。从 GOF 设计模式开始,我们转向反应式编程模式。我们讨论了诸如单元、活动对象、资源借出和事件总线等模式。从 GOF 模式过渡到反应式编程有助于你从更广泛的意义上看待反应式编程。
在下一章中,我们将学习使用 C++ 进行微服务开发。