Skip to content

Latest commit

 

History

History
631 lines (519 loc) · 39.2 KB

File metadata and controls

631 lines (519 loc) · 39.2 KB

五、可观察对象介绍

在最后三章中,我们学习了现代 C++ 的语言特性:多线程、无锁编程模型等等。这里涉及的主题可以被认为是开始学习反应式编程模型的一种先决条件。反应式编程模型保证了函数式编程、并发编程、调度器、对象/函数式编程、设计模式和事件流处理等技能。在前一章中,我们已经讨论或触及了函数式编程、对象/函数式编程以及一些与调度相关的主题。这一次,我们将覆盖设计模式的奇妙世界,来理解反应式编程的关键,特别是 Observables。在下一章中,我们将在进入 RxCpp 库之前处理事件流编程的主题。随着四人组 ( GoF )出版了一本名为设计模式:可重用面向对象软件的元素的书,设计模式运动达到了临界质量。他将一组 23 种模式归类为创造型、结构型和行为型。GoF 目录在行为模式类别中定义了观察者模式。我们在这里要传递的一个关键信息是,可以通过对古老的 GoF 模式的了解来理解反应式编程模型。在本章中,我们将介绍:

  • GoF 观察者模式
  • GoF 观测器模式的局限性
  • 从整体上看待设计模式和可观察对象
  • 使用复合设计模式建模真实世界的层次结构
  • 使用访问者的复合材料行为处理
  • 展平复合并在迭代器模式中导航
  • 通过反转凝视从迭代器到可观察/观察者的转换!

GoF 观察者模式

GoF 观察者模式在 GoF 书中也被称为发布-订阅模式。想法很简单。EventSource(发出事件的类)将与事件接收器(监听事件通知的类)具有一对多的关系。每个EventSource都会有一个事件接收器订阅的机制,以便获得不同类型的通知。一个EventSource可能会发出多个事件。当一个EventSource的状态发生变化或者在它的领域中发生重大事情时,它可以向成千上万的订阅者(事件接收器或监听器)发送通知。EventSource将浏览订户名单,并逐一通知他们。GoF 书是在世界大部分时间都在进行顺序编程的时候写的。并发性等主题大多与平台特定的库或POSIX线程库相关。我们将编写一个简单的 C++ 程序来演示观察者模式的整个思想。目的是快速理解观察者模式,而像健壮性这样的想法已经被赋予了次要的优先权。列表是独立的,易于理解:

//-------------------- Observer.cpp 
#include <iostream> 
#include  <vector> 
#include <memory> 
using namespace std; 
//---- Forward declaration of event sink 
template<class T> 
class EventSourceValueObserver; 
//----------A toy implementation of EventSource
template<class T> 
class EventSourceValueSubject{ 
   vector<EventSourceValueObserver<T> *> sinks;  
   T State; // T is expected to be a value type 
  public: 
   EventSourceValueSubject() { State = 0; } 
   ~EventSourceValueSubject() { 
       sinks.clear(); 
   } 
   bool Subscribe( EventSourceValueObserver<T> *sink ) { sinks.push_back(sink);} 
   void NotifyAll() { for (auto sink : sinks) { sink->Update(State); }} 
   T GetState() { return State; } 
   void SetState(T pstate) { State = pstate; NotifyAll(); } 
};

前面的代码片段实现了一个微不足道的EventSource,它可能会存储一个整数值作为状态。在现代 C++ 中,我们可以使用类型特征来检测消费者是否已经用整型实例化了这个类。由于我们的重点是阐明,我们没有添加与类型约束相关的断言。在下一个 C++ 标准中,有一个概念叫做概念(在其他语言中被称为约束),它将有助于直接强制执行(没有类型特征)。在现实场景中,EventSource可能会存储大量变量或值流。其中的任何更改都将广播给所有订户。在SetState方法中,当EventSource类的消费者(事件接收器本身就是该类中的消费者)发生状态突变时,NotifyAll()方法将被触发。NotifyAll()方法通过接收器列表工作,并调用Update()方法。然后,事件接收器可以执行特定于其上下文的任务。我们没有实施退订等方法来关注核心问题:

//--------------------- An event sink class for the preceding EventSources 
template <class T> 
class EventSourceValueObserver{ 
    T OldState; 
  public: 
    EventSourceValueObserver() { OldState = 0; } 
    virtual ~EventSorceValueObserver() {} 
    virtual void Update( T State ) { 
       cout << "Old State " << OldState << endl; 
       OldState = State; 
       cout << "Current State " << State << endl;  
    } 
}; 

EventSourceValueObserver类已经实现了Update方法来完成与其上下文相关的任务。在这里,它只是将旧状态和当前状态的值打印到控制台上。在现实生活中,接收器可能会修改 UX 元素,或者通过通知将状态传播给其他对象。我们再写一个事件接收器,它将继承自EventSourceValueObserver:

//------------ A simple specialized Observe 
class AnotherObserver : public EventSourceValueObserver<double> { 
  public: 
    AnotherObserver():EventSourceValueObserver() {} 
    virtual ~AnotherObserver() {} 
    virtual void Update( double State )  
    { cout << " Specialized Observer" << State <<  endl; } 
};

为了演示的目的,我们实现了观察者的一个专门版本。这样做是为了表明我们可以拥有两个类实例的订阅者(可以从EventSourceObserver<T>继承)。此外,当我们收到EventSource的通知时,我们不会做太多事情:

int main() { 
   unique_ptr<EventSourceValueSubject<double>> 
                 evsrc(new EventSourceValueSubject<double>()); 
    //---- Create Two instance of Observer and Subscribe 
   unique_ptr<AnotherObserver> evobs( new AnotherObserver());
   unique_ptr<EventSourceValueObserver<double>> 
               evobs2( new EventSourceValueObserver<double>());
   evsrc->Subscribe( evobs.get() );
   evsrc->Subscribe( evobs2.get());
   //------ Change the State of the EventSource 
   //------ This should trigger call to Update of the Sink 
   evsrc->SetState(100); 
} 

前面的代码片段实例化了一个EventSource对象,并添加了两个订阅者。当我们更改EventSource的状态时,订阅者会收到通知。这是观察者模式的关键。在一个普通的面向对象程序中,对象的消耗是通过以下方式完成的:

  1. 实例化对象
  2. 调用一个方法来计算一些值或改变状态
  3. 根据返回值或状态变化做一些有用的事情

在这里,就观察者而言,我们做了以下工作:

  1. 实例化对象(EventSource)
  2. 通过实现观察者订阅通知(用于事件监听)
  3. EventSource发生变化时,会通知你
  4. 用通过通知收到的值做一些事情

这里概述的Method功能有助于分离关注点,并且已经实现了模块化。这是实现事件驱动代码的好机制。您要求得到通知,而不是轮询事件。如今大多数图形用户界面工具包都使用类似的范例。

GoF 观察者模式的局限性

GoF 模式书是在世界真正进行顺序编程的时候写的。从当前的编程模型世界观来看,观察者模式实现的架构有很多异常。以下是其中的一些:

  • 主体和观察者之间的紧密耦合。
  • EventSource的寿命由观察者控制。
  • 观察者(水槽)可以挡住EventSource
  • 该实现不是线程安全的。
  • 事件过滤在接收器级别完成。理想情况下,应该在数据所在的位置过滤数据(在主题级别,通知之前)。
  • 大多数时候,观察者不会做太多事情,CPU 周期会被浪费。
  • EventSource应该理想地向环境发布该值。环境应该通知所有订户。这种间接级别可以促进诸如事件聚合、事件转换、事件过滤和规范化事件数据等技术。

随着不可变变量、函数组合、函数风格转换、无锁并发编程等函数式编程技术的出现,我们可以规避经典 Observer 模式的限制。该行业概述的解决方案是可观察的概念。

在经典的观察者模式中,勤奋的读者可能已经看到了异步编程模型被合并的潜力。EventSource可以异步调用订阅者方法,而不是顺序循环订阅者。通过使用火灾和遗忘机制,我们可以将EventSource与其水槽分离。调用可以从后台线程、异步任务、打包任务或合适的上下文机制中完成。通知方法的异步调用还有一个额外的优点,即如果任何客户端阻塞(通过进入无限循环或崩溃),其他客户端仍然可以获得通知。异步方法适用于以下模式:

  1. 定义处理数据、异常和数据结尾的方法(在事件接收器端)

  2. 观察者(事件接收器)接口应该有OnDataOnErrorOnCompleted方法

  3. 每个事件接收器都应该实现观察者界面

  4. 每个EventSource(可观察)都应该有订阅和取消订阅的方法

  5. 事件接收器应该通过订阅方法订阅可观察的实例

  6. 当一个事件发生时,观察者会被可观察到的事物通知

其中一些已经在第 1 章反应式编程模型-概述和历史中提到。我们当时没有讨论异步部分。在本章中,我们将重温这些想法。根据作者基于技术演示和与开发人员的交互所获得的经验,直接进入编程的可观察/观察者模型无助于理解。大多数开发人员对可观察/观察者感到困惑,因为他们不知道这个模式解决了什么特殊问题。这里给出的经典 GoF 观察者实现是为可观察流的讨论设置上下文。

从整体上看 GoF 模式

设计模式运动开始的时候,世界正在努力适应面向对象软件设计方法的复杂性。GoF 书籍和相关的模式目录为开发人员提供了一套设计大规模系统的技术。诸如并发性和并行性之类的主题并不在设计目录的人的脑海中。(至少,他们的工作没有体现这一点!)

我们已经看到,通过经典的观察者模式进行事件处理有一些局限性,这在某些情况下可能是一个问题。出路是什么?我们需要退一步重新看待事件处理的问题。对于反应式编程模型(用可观察流编程!)正在努力解决。我们的旅程将帮助我们很好地从 GOF 模式过渡到使用函数式编程结构的反应式编程世界。

这一部分的以下内容有点抽象,在这里提供了一个概念背景,这本书的作者从这个背景开始探讨本章所涵盖的主题。我们解释可观察对象的方法从 GoF 复合/访问者模式开始,并迭代到可观察对象的主题。这种方法的想法来自一本关于 Advaita Vedanta 的书,这是一种起源于印度的神秘哲学传统。这个话题已经用西方哲学术语解释过了。如果一件事看起来有点抽象,请随意掩饰。

纳塔拉贾·古鲁(1895-1973)是一位印度哲学家,他是阿维达·韦丹塔哲学的支持者,这是一种基于支配我们所有人的至高力量的非二元论的印度哲学流派。根据这一哲学流派,无论我们在周围看到什么,无论是人类、动物还是植物,都是绝对(梵语中称为婆罗门)的表现形式,它唯一的肯定是 SAT-CHIT-ANAND(吠檀多哲学用矛盾的否定和证明来描绘婆罗门)。这可以翻译成英语,作为存在、本质和极乐(极乐的隐含意义在这里是“好”)。在新德里 DK Print World 出版的名为统一哲学的书中,他给出了 SAT-CHIT-ANAND 到本体论、认识论和价值论(哲学的三个主要分支)的映射。本体论、认识论和价值论分别是存在论、认识论和价值论。下表给出了 SAT-CHIT-ANAND 到其他实体的可能映射,它们的含义大致相同:

| SAT | CHIT | 菠萝 | | 存在 | 本质 | 布利斯 | | 本体论 | 认识论 | 价值论 | | 我是谁? | 我能知道什么? | 我该怎么办? | | 结构 | 行为 | 功能 |

在吠檀多哲学中,整个世界被视为存在、本质和极乐。从表中,我们将把软件设计世界中的问题映射到结构、行为和功能的问题上。世界上的每个系统都可以从结构、行为和功能的角度来看待。面向对象程序的规范结构是层次结构。我们将把我们感兴趣的世界建模为层次结构,并以规范的方式处理它们。GOF 模式目录有用于建模层次结构的复合模式(结构模式)和处理它们的访问者模式(行为模式)。

面向对象编程模型和层次结构

This section is bit conceptual in nature and those of you who have not dabbled with GoF design patterns will find it a bit difficult. The best strategy could be to skip this section and focus on the running example. Once you have understood the running example, this particular section can be revisited.

面向对象编程非常擅长建模层次结构。事实上,层次结构可以被认为是面向对象数据处理的规范数据模型。在 GoF 模式世界中,我们使用复合模式来建模层次结构。复合模式被归类为结构模式。每当使用复合模式时,访问者模式也将是系统的一部分。Visitor 模式有利于处理复合,从而为结构增加行为。访问者/组合模式在现实生活中是一对。当然,复合的一个实例可以由不同的访问者处理。在编译器项目中,抽象语法树 ( AST )将被建模为一个组合,并且将有用于类型检查、代码优化、代码生成和静态分析等的 Visitor 实现。

Visitor 模式的一个问题是,它必须对复合的结构有所了解才能进行处理。此外,在需要处理复合层次结构中可用数据的过滤子集的上下文中,这将导致代码膨胀。对于每个过滤标准,我们可能需要不同的访问者。GoF 模式目录有另一种属于行为范畴的模式,叫做迭代器,这是每个 C++ 程序员都熟悉的。迭代器模式擅长以结构不可知的方式处理数据。任何类型的层次结构都必须被线性化或展平,以形成一个适合迭代器处理的形状。一个例子可以是树,它可以用 BFS 迭代器或 DFS 迭代器来处理。对于应用程序员来说,突然之间,树变成了线性结构。我们需要展平层次结构,使其处于一种结构服从迭代器的状态。该过程将由实现该应用编程接口的人来实现。迭代器模式有一些限制(它是基于拉的),我们将使用一种称为 Observerable/Observer 的模式来反转凝视,并使系统基于推,Observer 模式的增强版本。这一部分有点抽象,但是在看完整个章节后,你可以回来理解正在发生的事情。简而言之,我们可以把整件事总结如下:

  • 我们可以使用复合模式来建模层次结构
  • 我们可以使用访问者模式来处理复合
  • 我们可以展平或线性化复合,通过迭代器进行导航
  • 迭代器遵循拉方法,对于基于推的方案,我们需要反向凝视
  • 现在,我们已经设法达到了实现事物的可观察/观察者方式
  • 可观测值和迭代器是二元对立的(一个人的推动就是另一个人的拉动!)

我们将实施上述所有要点,为可观察对象打下坚实的基础。

用于表达式处理的复合/访问者模式

为了演示从 GoF 模式目录到 Observables 的旅程,我们将模拟一个四功能计算器作为运行示例。因为表达式树或 AST 本质上是分层的,所以它们将是一个很好的例子来建模为复合模式。我们有意省略了编写解析器,以保持代码清单的小:

#include <iostream> 
#include <memory> 
#include <list> 
#include <stack> 
#include <functional> 
#include <thread> 
#include <future> 
#include <random> 
#include "FuncCompose.h" // available int the code base 
using namespace std; 
//---------------------List of operators supported by the evaluator 
enum class OPERATOR{ ILLEGAL,PLUS,MINUS,MUL,DIV,UNARY_PLUS,UNARY_MINUS };  

我们定义了一个枚举类型来表示四个二元运算符(+-*/)和两个一元运算符(+-)。除了标准的 C++ 头之外,我们还包括了一个自定义头(FuncCompose.h),它可以在与本书相关的 GitHub repo 上找到。它包含用于编写函数的代码和用于函数编写的管道操作符(|)。我们可以使用 Unix 管道样式组合将一组转换联系在一起:

//------------ forward declarations for the Composites  
class Number;  //----- Stores IEEE double precision floating point number  
class BinaryExpr; //--- Node for Binary Expression 
class UnaryExpr;  //--- Node for Unary Expression 
class IExprVisitor; //---- Interface for the Visitor  
//---- Every node in the expression tree will inherit from the Expr class 
class Expr { 
  public: 
   //---- The standard Visitor double dispatch method 
   //---- Normally return value of accept method are void.... and Concrete
   //---- classes store the result which can be retrieved later
   virtual double accept(IExprVisitor& expr_vis) = 0; 
   virtual ~Expr() {} 
}; 
//----- The Visitor interface contains methods for each of the concrete node  
//----- Normal practice is to use 
struct IExprVisitor{ 
   virtual  double Visit(Number& num) = 0; 
   virtual  double Visit(BinaryExpr& bin) = 0; 
   virtual  double Visit(UnaryExpr& un)=0 ; 
}; 

表达式类将作为表达式树中所有节点的基类。因为我们的目的是演示复合/访问者 GoF 模式,所以我们只支持常量、二进制表达式和一元表达式。Expr 类中的 accept 方法接受一个 Visitor 引用作为参数,并且该方法的主体对于所有节点都是相同的。该方法会将调用重定向到 Visitor 实现上的适当处理程序。要更深入地了解本节所涵盖的整个主题,请阅读关于双派单访客模式的内容,方法是使用您最喜欢的搜索引擎搜索网页。

访问者界面(IExprVisitor)包含处理层次结构支持的所有节点类型的方法。在我们的例子中,有处理常数、二进制运算符和一元运算符的方法。让我们看看节点类型的代码。我们从数字课开始:

//---------A class to represent IEEE 754 interface 
class Number : public Expr { 
   double NUM; 
  public: 
   double getNUM() { return NUM;}    
   void setNUM(double num)   { NUM = num; } 
   Number(double n) { this->NUM = n; } 
   ~Number() {} 
   double accept(IExprVisitor& expr_vis){ return expr_vis.Visit(*this);} 
}; 

Number 类包装了一个 IEEE 双精度浮点数。代码显而易见,我们只需要关心accept方法的内容。该方法接收类型为访问者(IExprVisitor&)的参数。该例程只是将调用反射回 Visitor 实现上的适当节点。在这种情况下,它会在IExpressionVisitor上调用Visit(Number&):

//-------------- Modeling Binary Expresison  
class BinaryExpr : public Expr { 
   Expr* left; Expr* right; OPERATOR OP; 
  public: 
   BinaryExpr(Expr* l,Expr* r , OPERATOR op ) { left = l; right = r; OP = op;} 
   OPERATOR getOP() { return OP; } 
   Expr& getLeft() { return *left; } 
   Expr& getRight() { return *right; } 
   ~BinaryExpr() { delete left; delete right;left =0; right=0; } 
   double accept(IExprVisitor& expr_vis) { return expr_vis.Visit(*this);} 
};  

BinaryExpr类用左右操作数模拟二进制运算。操作数可以是层次结构中的任何类。候选班级有NumberBinaryExprUnaryExpr。这可以达到任意深度。在我们的例子中,终端节点是数字。前面的代码支持四个二进制运算符:

//-----------------Modeling Unary Expression 
class UnaryExpr : public Expr { 
   Expr * right; OPERATOR op; 
  public: 
   UnaryExpr( Expr *operand , OPERATOR op ) { right = operand;this-> op = op;} 
   Expr& getRight( ) { return *right; } 
   OPERATOR getOP() { return op; } 
   virtual ~UnaryExpr() { delete right; right = 0; } 
   double accept(IExprVisitor& expr_vis){ return expr_vis.Visit(*this);} 
};  

UnaryExpr方法用一个运算符和一个右侧表达式对一元表达式进行建模。对于这个实现,我们支持一元正和一元负。右侧的表情可以依次是UnaryExprBinaryExprNumber。现在我们已经有了所有支持的节点类型的实现,让我们把重点放在访问者接口的实现上。我们将编写一个树行者和评估器来计算表达式的值:

//--------An Evaluator for Expression Composite using Visitor Pattern  
class TreeEvaluatorVisitor : public IExprVisitor{ 
  public: 
   double Visit(Number& num){ return num.getNUM();} 
   double Visit(BinaryExpr& bin) { 
     OPERATOR temp = bin.getOP(); double lval = bin.getLeft().accept(*this); 
     double rval = bin.getRight().accept(*this); 
     return (temp == OPERATOR::PLUS) ? lval + rval: (temp == OPERATOR::MUL) ?  
         lval*rval : (temp == OPERATOR::DIV)? lval/rval : lval-rval;   
   } 
   double Visit(UnaryExpr& un) { 
     OPERATOR temp = un.getOP(); double rval = un.getRight().accept(*this); 
     return (temp == OPERATOR::UNARY_PLUS)  ? +rval : -rval; 
   } 
};

这将对 AST 进行深度优先遍历,并递归评估节点。让我们编写一个表达式处理器(一个IExprVisitor的实现),它将以反向波兰符号 ( RPN )的形式将表达式树打印到控制台:

//------------A Visitor to Print Expression in RPN
class ReversePolishEvaluator : public IExprVisitor {
    public:
    double Visit(Number& num){cout << num.getNUM() << " " << endl; return 42;}
    double Visit(BinaryExpr& bin){
        bin.getLeft().accept(*this); bin.getRight().accept(*this);
        OPERATOR temp = bin.getOP();
        cout << ( (temp==OPERATOR::PLUS) ? " + " :(temp==OPERATOR::MUL) ?
        " * " : (temp == OPERATOR::DIV) ? " / ": " - " ) ; return 42;
    }
    double Visit(UnaryExpr& un){
        OPERATOR temp = un.getOP();un.getRight().accept(*this);
        cout << (temp == OPERATOR::UNARY_PLUS) ?" (+) " : " (-) "; return 42;
    }
};

RPN 符号也称为后缀概念,其中运算符位于操作数之后。它们适合使用评估堆栈进行处理。它们构成了基于堆栈的虚拟机体系结构的基础,Java 虚拟机和。NET CLR。现在,让我们编写一个主函数来将所有内容组合在一起:

int main( int argc, char **argv ){ 
     unique_ptr<Expr>   
            a(new BinaryExpr( new Number(10) , new Number(20) , OPERATOR::PLUS)); 
     unique_ptr<IExprVisitor> eval( new TreeEvaluatorVisitor()); 
     double result = a->accept(*eval); 
     cout << "Output is => " << result << endl; 
     unique_ptr<IExprVisitor>  exp(new ReversePolishEvaluator()); 
     a->accept(*exp); 
}

这段代码片段创建了一个复合的实例(一个BinaryExpr的实例),并且还实例化了一个TreeEvaluatorVisitorReversePolshEvaluator的实例。然后调用 Expr 的accept方法开始处理。我们将在控制台上看到该表达式的值和该表达式的一个 RPN 等价物。在这一节中,我们学习了如何创建一个组合并使用一个访问者界面来处理该组合。复合/访问者的其他潜在例子是存储目录内容及其遍历、XML 处理、文档处理等等。流行的观点认为,如果你知道复合/访问者二人组,你已经很好地理解了 GoF 模式目录。

我们已经看到复合模式和访问者模式作为一对来处理系统的结构和行为方面,并提供一些功能。访客必须以一种预先假定对复合材料结构的认知的方式书写。从抽象的角度来看,这可能是一个潜在的问题。层次结构的实现者可以提供一种机制,将层次结构展平成一个列表(这在大多数情况下是可能的)。这将使应用编程接口实现者能够提供一个基于迭代器的应用编程接口。基于迭代器的应用编程接口也很适合函数式处理。让我们看看它是如何工作的。

展平复合材料进行迭代处理

我们已经了解到,访问者模式必须知道组合的结构,才能有人编写访问者界面的实例。这可能会产生一个异常,称为抽象泄漏。GoF 模式目录有一个模式,可以帮助我们以结构不可知的方式导航树的内容。是的,你可能猜对了:迭代器模式是候选模式!为了让迭代器完成它的工作,复合必须被展平成一个列表序列或流。让我们编写一些代码来展平上一节中建模的表达式树。在编写展平复合的逻辑之前,让我们创建一个数据结构,将 AST 的内容存储为一个列表。列表中的每个节点都必须存储一个运算符或值,这取决于我们是否需要存储运算符或操作数。为此,我们描述了一个名为EXPR_ITEM的数据结构:

//////////////////////////// 
// A enum to store discriminator -> Operator or a Value? 
enum class ExprKind{  ILLEGAL_EXP,  OPERATOR , VALUE }; 
// A Data structure to store the Expression node. 
// A node will either be a Operator or Value 
struct EXPR_ITEM { 
    ExprKind knd; double Value; OPERATOR op; 
    EXPR_ITEM():op(OPERATOR::ILLEGAL),Value(0),knd(ExprKind::ILLEGAL_EXP){} 
    bool SetOperator( OPERATOR op ) 
    {  this->op = op;this->knd = ExprKind::OPERATOR; return true; } 
    bool SetValue(double value)  
    {  this->knd = ExprKind::VALUE;this->Value = value;return true;} 
    string toString() {DumpContents();return "";} 
   private: 
      void DumpContents() { //---- Code omitted for brevity } 
}; 

list<EXPR_ITEM>数据结构将以线性结构存储复合的内容。让我们编写一个类来展平复合:

//---- A Flattener for Expressions 
class FlattenVisitor : public IExprVisitor { 
        list<EXPR_ITEM>  ils; 
        EXPR_ITEM MakeListItem(double num) 
        { EXPR_ITEM temp; temp.SetValue(num); return temp; } 
        EXPR_ITEM MakeListItem(OPERATOR op) 
        { EXPR_ITEM temp;temp.SetOperator(op); return temp;} 
        public: 
        list<EXPR_ITEM> FlattenedExpr(){ return ils;} 
        FlattenVisitor(){} 
        double Visit(Number& num){ 
           ils.push_back(MakeListItem(num.getNUM()));return 42; 
        } 
        double Visit(BinaryExpr& bin) { 
            bin.getLeft().accept(*this);bin.getRight().accept(*this); 
            ils.push_back(MakeListItem(bin.getOP()));return 42; 
        } 
         double Visit(UnaryExpr& un){ 
            un.getRight().accept(*this); 
            ils.push_back(MakeListItem(un.getOP())); return 42; 
        } 
};  

FlattenerVistor类将复合Expr节点展平为一个EXPR_ITEM列表。一旦组合被线性化,就可以使用迭代器模式来处理项目。让我们编写一个小的全局函数,将Expr树转换为list<EXPR_ITEM>:

list<EXPR_ITEM> ExprList(Expr* r) { 
   unique_ptr<FlattenVisitor> fl(new FlattenVisitor()); 
    r->accept(*fl); 
    list<EXPR_ITEM> ret = fl->FlattenedExpr();return ret; 
 }

全局子程序ExprList将展平一列EXPR_ITEM的任意表达式树。一旦我们展平了复合,我们就可以使用迭代器来处理内容。将结构线性化为列表后,我们可以使用堆栈数据结构来评估表达式数据,以生成输出:

//-------- A minimal stack to evaluate RPN expression 
class DoubleStack : public stack<double> { 
   public: 
    DoubleStack() { } 
    void Push( double a ) { this->push(a);} 
    double Pop() { double a = this->top(); this->pop(); return a; } 
};  

DoubleStack是 STL 堆栈容器的包装器。这可以被认为是某种帮助例程,以保持列表的简洁。让我们为扁平表达式编写一个赋值器。如果遇到值,我们将遍历列表<EXPR_ITEM>并将值推送到堆栈。如果遇到运算符,我们将从堆栈中弹出值并应用该操作。结果再次被推入堆栈。迭代结束时,堆栈中的现有元素将是与表达式关联的值:

//------Iterator through eachn element of Expression list 
double Evaluate( list<EXPR_ITEM> ls) { 
   DoubleStack stk; double n; 
   for( EXPR_ITEM s : ls ) { 
     if (s.knd == ExprKind::VALUE) { stk.Push(s.Value); } 
     else if ( s.op == OPERATOR::PLUS) { stk.Push(stk.Pop() + stk.Pop());} 
     else if (s.op == OPERATOR::MINUS ) { stk.Push(stk.Pop() - stk.Pop());} 
     else if ( s.op ==  OPERATOR::DIV) { n = stk.Pop(); stk.Push(stk.Pop() / n);} 
     else if (s.op == OPERATOR::MUL) { stk.Push(stk.Pop() * stk.Pop()); } 
     else if ( s.op == OPERATOR::UNARY_MINUS) { stk.Push(-stk.Pop()); } 
    } 
   return stk.Pop(); 
} 
//-----  Global Function Evaluate an Expression Tree 
double Evaluate( Expr* r ) { return Evaluate(ExprList(r)); } 

让我们编写一个主程序,它将调用这个函数来计算表达式。评估器中的代码列表很容易理解,因为我们正在减少一个列表。在基于树的解释器中,事情并不明显:

int main( int argc, char **argv ){      
     unique_ptr<Expr>
         a(new BinaryExpr( new Number(10) , new Number(20) , OPERATOR::PLUS)); 
     double result = Evaluate( &(*a)); 
     cout << result << endl; 
} 

列表上的映射和过滤操作

Map 是一个函数运算符,函数将应用于列表。Filter 将对一个列表应用谓词,并返回另一个列表。它们是任何功能处理管道的基石。它们也被称为高阶函数。我们可以使用std::liststd::transformstd::vector编写一个通用的地图函数:

template <typename R, typename F> 
R Map(R r , F&& fn) { 
      std::transform(std::begin(r), std::end(r), std::begin(r), 
         std::forward<F>(fn)); 
      return r; 
} 

让我们也写一个函数来过滤一个std::list(我们假设只传递一个列表)。同样可以在std::vector上工作。我们可以使用管道操作符组成一个更高阶的函数。复合函数也可以作为谓词传递:

template <typename R, typename F> 
R Filter( R r , F&& fn ) { 
   R ret(r.size()); 
   auto first = std::begin(r), last = std::end(r) , result = std::begin(ret);  
   bool inserted = false; 
   while (first!=last) { 
    if (fn(*first)) { *result = *first; inserted = true; ++ result; }  
    ++ first; 
   } 
   if ( !inserted ) { ret.clear(); ret.resize(0); } 
   return ret; 
}

在 Filter 的这个实现中,由于std::copy_if的限制,我们被迫滚动自己的迭代逻辑。一般建议使用函数的 STL 实现来编写包装器。对于这个特定的场景,我们需要检测一个列表是否为空:

//------------------ Global Function to Iterate through the list  
void Iterate( list<EXPR_ITEM>& s ){ 
    for (auto n : s ) { std::cout << n.toString()  << 'n';} 
} 

让我们写一个主函数来把所有的东西放在一起。该代码将演示如何在应用代码中使用MapFilter。功能组合逻辑和管道操作器可在FuncCompose.h获得:

int main( int argc, char **argv ){ 
     unique_ptr<Expr>   
        a(new BinaryExpr( new Number(10.0) , new Number(20.0) , OPERATOR::PLUS)); 
      //------ExprList(Expr *) will flatten the list and Filter will by applied 
      auto cd = Filter( ExprList(&(*a)) , 
            [](auto as) {  return as.knd !=   ExprKind::OPERATOR;} ); 
      //-----  Square the Value and Multiply by 3... used | as composition Operator 
      //---------- See FuncCompose.h for details 
      auto cdr = Map( cd, [] (auto s ) {  s.Value *=3; return s; } |  
                  [] (auto s ) { s.Value *= s.Value; return s; } ); 
      Iterate(cdr);  
} 

Filter例程创建一个新的list<Expr>,它只包含表达式中使用的值或操作数。Map例程对值列表应用一个复合函数来返回一个新列表。

逆转可观察的凝视!

我们已经了解到,我们可以将一个组合转换成一个列表,并通过迭代器遍历它们。迭代器模式从数据源提取数据,并在消费者级别操作结果。我们面临的最重要的问题是我们正在耦合我们的EventSource和事件接收器。GoF 观察者模式在这里也没有帮助。

让我们编写一个可以充当事件中枢的类,接收器将订阅该类。通过拥有一个事件中枢,我们现在将拥有一个对象,它将充当EventSource和事件接收器之间的中介。这种间接性的一个优势显而易见,因为我们的类可以在事件到达消费者之前进行聚合、转换和过滤。消费者甚至可以在事件中心级别设置转换和过滤标准:

//----------------- OBSERVER interface 
struct  OBSERVER { 
    int id; 
    std::function<void(const double)> ondata; 
    std::function<void()> oncompleted; 
    std::function<void(const std::exception &)> onexception; 
}; 
//--------------- Interface to be implemented by EventSource 
struct OBSERVABLE { 
   virtual bool Subscribe( OBSERVER * obs ) = 0; 
    // did not implement unsuscribe  
}; 

我们已经在第 1 章反应式编程模型–概述和历史第 2 章现代 C++ 及其关键习惯用法之旅中介绍了OBSERVABLEOBSERVEREventSource实现OBSERVABLE,事件接收器实现OBSERVER接口。从OBSERVER派生的类将实现以下方法:

  • ondata(用于接收数据)
  • onexception(异常处理)
  • oncompleted(数据结束)

EventSource类将从OBSERVABLE派生,并且必须实现:

  • 订阅(订阅通知)
  • 取消订阅(在我们的案例中未实现)
//------------------A toy implementation of EventSource 
template<class T,class F,class M, class Marg, class Farg > 
class EventSourceValueSubject : public OBSERVABLE { 
   vector<OBSERVER> sinks;  
   T *State;  
   std::function<bool(Farg)> filter_func; 
   std::function<Marg(Marg)> map_func;

map_funcfilter_func是可以帮助我们在以异步方式将值分派给订阅者之前对其进行转换和过滤的函数。当我们实例化EventSource类时,我们给出这些值作为参数。目前,我们编写代码时假设只有Expr对象将存储在EventSource中。我们可以有一个表达式的列表或向量,并将值流式传输给订阅者。为此,可以将标量值推送给侦听器:

  public: 
   EventSourceValueSubject(Expr *n,F&& filter, M&& mapper) { 
       State = n; map_func = mapper; filter_func = filter; NotifyAll();  
   } 
   ~EventSourceValueSubject() {  sinks.clear(); } 
   //------ used Raw Pointer ...In real life, a shared_ptr<T>
   //------ is more apt here
   virtual  bool Subscribe( OBSERVER  *sink ) { sinks.push_back(*sink); return true;} 

我们做了一些假设Expr对象将由调用者拥有。我们还省略了取消订阅方法的实现。构造函数接受一个Expr对象、Filter谓词(可以是使用|运算符的复合函数)和一个Mapping函数(可以是使用|运算符的复合函数):

   void NotifyAll() { 
      double ret = Evaluate(State); 
      list<double> ls; ls.push_back(ret); 
      auto result = Map( ls, map_func);; // Apply Mapping Logic 
      auto resulttr = Filter( result,filter_func); //Apply Filter 
      if (resulttr.size() == 0 ) { return; } 

评估表达式后,标量值将被放入 STL 列表。然后,将在列表中应用映射函数来转换该值。将来,我们将处理一个值列表。一旦我们映射或转换了值,我们将对列表应用过滤器。如果列表中没有值,则该方法返回而不通知订阅者:

      double dispatch_number = resulttr.front(); 
      for (auto sink : sinks) {  
           std::packaged_task<int()> task([&]()  
           { sink.ondata(dispatch_number); return 1;  }); 
           std::future<int> result = task.get_future();task(); 
           double dresult = result.get(); 
         } 
     }

在这段代码中,我们将调用packaged_task将数据分派给事件接收器。工业级的库使用一段名为 Scheduler 的代码来完成这部分任务。既然是用火忘了,水槽就挡不住EventSource。这是 Observables 最重要的用例之一:

      T* GetState() { return State; } 
      void SetState(T *pstate) { State = pstate; NotifyAll(); } 
}; 

现在,让我们编写一个基于现代 C++ 随机数生成器的方法来发出具有均匀概率分布的随机表达式。这种分布的选择相当随意。我们也可以尝试其他发行版,以获得不同的结果:

Expr *getRandomExpr(int start, int end) { 
    std::random_device rd; 
    std::default_random_engine reng(rd()); 
    std::uniform_int_distribution<int> uniform_dist(start, end); 
    double mean = uniform_dist(reng); 
    return  new  
          BinaryExpr( new Number(mean*1.0) , new Number(mean*2.0) , OPERATOR::PLUS); 
} 

现在,让我们编写一个主函数来将所有内容组合在一起。我们将用一个Expr、一个Filter和一个Mapper来实例化EventSourceValueSubject类:

int main( int argc, char **argv ){ 
     unique_ptr<Expr>   
         a(new BinaryExpr( new Number(10) , new Number(20) , OPERATOR::PLUS)); 
     EventSourceValueSubject<Expr,std::function<bool(double)>, 
                    std::function<double(double)>,double,double>  
                    temp(&(*a),[] (auto s ) {   return s > 40.0;  }, 
                    []  (auto s ) { return s+ s ; }  | 
                    []  (auto s ) { return s*2;} ); 

在实例化对象时,我们使用了管道操作符来合成两个 Lambdas。这是为了演示我们可以组成一个任意的函数列表来形成一个复合函数。当我们编写 RxCpp 程序时,我们会大量利用这种技术:

     OBSERVER obs_one ;     OBSERVER obs_two ; 
     obs_one.ondata = [](const double  r) {  cout << "*Final Value " <<  r << endl;}; 
     obs_two.ondata = [] ( const double r ){ cout << "**Final Value " << r << endl;}; 

在这段代码中,我们已经实例化了两个OBSERVER对象,并使用 Lambda 函数将它们分配给 ondata 成员。我们没有实现其他方法。这仅用于演示目的:

     temp.Subscribe(&obs_one); temp.Subscribe(&obs_two);   

我们使用OBSERVER实例订阅了事件通知。我们只实现了 ondata 方法。实现onexceptiononcompleted是琐碎的任务:

     Expr *expr = 0; 
     for( int i= 0; i < 10; ++ i ) { 
           cout << "--------------------------" <<  i << " "<< endl; 
           expr = getRandomExpr(i*2, i*3 ); temp.SetState(expr); 
           std::this_thread::sleep_for(2s); delete expr; 
     } 
} 

我们通过将表达式设置为EventSource对象来评估一系列随机表达式。经过变换和过滤后,如果还有剩余值,该值会通知到OBSERVER并打印到控制台。有了这个,我们已经设法用packaged_taks写了一个无阻塞的EventSource。我们在本章中演示了以下内容:

  • 使用组合为表达式树建模
  • 通过访问者界面处理组合
  • 将表达式树展平成一个列表,并通过迭代器进行处理(拉)
  • 将视线从EventSource反向至事件接收器(推动)

摘要

在这一章中,我们已经讲了很多内容,慢慢走向反应式编程模式。我们了解了 GoF 观察者模式,并了解了它的缺点。然后,我们进入哲学,从结构、行为和功能的角度来理解看待世界的方法。我们在建模表达式树的上下文中学习了 GoF 复合/访问者模式。我们学习了如何将层次结构展平成一个列表,并通过迭代器导航它们。最后,我们稍微改变了一下事物的模式,以达到可观察对象。通常,Observables 与 Streams 一起工作,但是在我们的例子中,它是一个标量值。在下一章中,我们将学习事件流处理,以完成学习反应式编程的先决条件。