Skip to content

Latest commit

 

History

History
804 lines (683 loc) · 36.6 KB

File metadata and controls

804 lines (683 loc) · 36.6 KB

十、C++ 反应式编程的设计模式和习惯用法

我们已经讨论了在 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。

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 目录,以深入了解部署在世界各地的大规模系统的架构基础。我们认为,尽管这个目录很重要,但它没有得到应有的重视。

设计模式 redux

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类相关的虚拟功能。方法AddFileAddDirectory意在填充列表。当使用操作系统特定的函数遍历目录时,我们用前面两种方法填充目录:

//------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方法被自动调用。

资源贷款模式

顾名思义,贷款模式将资源贷款给你的职能部门。它执行以下步骤:

  1. 它创建了一个您可以使用的资源
  2. 它将资源借给将要使用它的功能
  3. 这个函数由调用者传递
  4. 资源被破坏了

下面的代码实现了资源管理的资源借出模式。该模式有助于在编写代码时避免资源泄漏:

//----------- 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); 
} 

在主功能中,我们执行以下任务:

  1. 创建EventBus<T>的实例
  2. 创建生产者的实例
  3. 创建消费者实例
  4. 将事件发送到总线

我们只介绍了适合编写反应式程序的设计模式的一个子集。首先,我们的重点是将 GOF 设计模式与反应式编程世界联系起来。事实上,作者认为反应式编程模型是经典 GOF 设计模式的增强实现。由于现代编程语言增加了函数式编程结构,增强是可能的。事实上,对象/函数编程是编写现代 C++ 代码的好方法。这一章主要是基于这个想法。

摘要

在这一章中,我们深入研究了设计模式和习惯用法的奇妙世界。从 GOF 设计模式开始,我们转向反应式编程模式。我们讨论了诸如单元、活动对象、资源借出和事件总线等模式。从 GOF 模式过渡到反应式编程有助于你从更广泛的意义上看待反应式编程。

在下一章中,我们将学习使用 C++ 进行微服务开发。