今天看啥  ›  专栏  ›  科研者

面向流程框架的设计

科研者  · 简书  ·  · 2020-10-14 10:30

面向流程的编程思想有了 (详情见 面向流程编程 ) ,那如何设计面向流程的框架呢?本文说细说面向流程框架的通用设计,其内容包含了面向流程框架的概念、模块生命周期等方面的设计,但并不包含具体的API,而面向流程框架中的具体模块、成员、API 以及 类图 的设计应由具体的面向流程框架 实现;

目录

内容

根据 面向流程编程 的思想,如果要做一个 面向流程编程 的框架,则需要框架实现以下功能

  1. 抽象好的节点,让用户(框架的使用者)可以自定义节点的业务逻辑,其它的所有节点共有的特性 和 功能 均由框架来实现;
  2. 抽象好的管道,让用户可以自定义数据的转换逻辑,其它的所有管道共有的特性 和 功能 均由框架来实现;
  3. 提供 元素 (管理 和 节点 统称元素) 互相连接的便捷方法;
  4. 提供 通过 配置化 或 形象化语言 或 可视化 的连接方式;

其中第1、2、3条是必须的,第4条 是加分项;

从上可以初步得出,框架中的核心概念包含 节点 管道 ,接下来分别详细设计 框架中的各个重要模块。

1. 节点

节点 是 程序的流程结构中每一步的逻辑单元。

1.1. 生命周期

为了能够让使用者能够在适当的时候执行自定义的业务逻辑,节点需要提供生命周期钩子,经过思考,节点可能需要具备下面的生命周期钩子:

  • 创建:为了使性能最大化,节点不应该在一开始就全部创建好,它可以在需要被激活时再创建, 创建 这个生命周期钩子是为了通知使用者节点已创建完毕,已经可以使用了;
  • 聚焦:当程序的执行逻辑执行到当前节点时,当前节点就会被激活,这时就需要执行该节点的业务逻辑,所以 聚焦 钩子就是提供给使用者来写节点业务逻辑的地方;
  • 失焦:当程序的执行逻辑执行到下一个节点时,当前节点即失焦了,这时需要给使用提供一个收尾的钩子,即 失焦 钩子,用来释放一些在 聚焦时创建的资源;
  • 销毁:当节点不再需要时,需要给使用者提供一个释放、清理资源的机会,这里主要释放的是在 创建 阶段生成的资源;

其中, 聚焦 失焦 这两个生命周期 最开始时 叫 激活 失活 激活 失活 这两个名字也更形象,但后来考虑到一类情况:如果节点内的业务逻辑 如果有 定时 或者 多次触发激活下一个节点的情况,那么按照 激活失活 的逻辑,该节点在第一次激活下一个节点后,就是 失活 的状态了,但是它还会触发激活下一个节点,把这种节点状态定为 失活 ,有些不合 失活 语意,这种状态应该叫 聚焦 失焦 更合理,因为 失焦 不一定意味着 失活,所以现在把 激活 失活 的生命周期 改名为 聚焦 失焦

有了 聚焦 失焦 ,那节点需要不需要 激活 失活

仔细想了想, 激活 的状态和界面是很明显的,但 失活 的状态似乎很难定了,就像刚才那种 “定时 或者 多次触发激活下一个节点” 的情况,你很难确定 节点的业务逻辑什么时候完全执行完;所以,目前来看,暂时不需要 激活 失活 的生命周期钩子;

1.2. 特性

  • 连接元素:节点要有连接其它元素的接口;

  • 输入和输出分开:节点激活时,可能需要上个节点传来的数据;当节点激活一个节点时,也可能会给其传递数据,这两种数据可能不一样,处理逻辑可能也不一样,所以,输入 和 输出没必须放在一块;

  • 可以连接多个元素:由于像下图这样的节点关系是很常见的,也为了更强大、更可扩展、更多的可能性,节点是允许连接多个元素的;

    连接多个元素
  • 可以有多个输出端口:元素与节点的连接 可以指定连接到节点的某个输出端口;当激活一下个节点时,也可以指定激活哪个输出端口上的节点;

  • 一个节点的多个输出端口可以连接同一个元素:这种需要也是可能的,比如(如下图):之前有 a 分别连接 b1 和 b2,后来 b1、b2 的逻辑合并成 b 一个节点来完成了;

    多个输出端口连接同一元素
  • 输入端口可有可无,但若提供了输入端口的机制,则可实现更多的特性;

  • 名字:节点要有名字,可用于通过名字激活指定的节点;

2. 管道

当连接两个节点 或 两个节点的端口时,被连接的两个目标可能有不同的数据要求,仅有这一点,还不能实现节点间的完全解耦;可让目标通过通过 管道 来连接,管道可以负责数据的转换,甚至也可以主动传递数据,如:上一个节点没有输出数据,但下一个节点需要输入数据时,这样可以让连接请求数据后,再传给目标;管道里也可以做延迟操作等等;

总之, 管道 就是用来在节点之间起连接作用并可传递、转换数据的。

2.1. 特征

  • 连接元素:管道要有连接其它元素的接口;
  • 管道不需要设计生命周期,因为它的主要作用就是传递、转换数据,如果还要给他设计生命周期的话 就跟节点太像了,这种情况下也就没有必要设计管道这个概念了;
  • 有可以让使用者定制转换逻辑的机制;

3. 包装元素

如果一个 流程F 中的 某个 元素E 的逻辑 也是一个流程(记为流程 S),当执行元素E时,则会执行 流程S,当流程S 执行完毕后,会继续沿着流程F执行元素E后面的元素;那么 就称 元素E 是一个 包装元素 ,流程S 记作 元素E的 包装流程

3.1. 特征

  • 包装元素应作为其 包装流程的代理,即,包装元素应将其所接收的所有信息 都 透传给 包装流程的第一个元素;
  • 在包装流程之外,包装元素应该可以作为普通元素来对待;
  • 当包装流程执行结束后,应当继续执行包装元素后面的元素(如果包装元素后面有元素的话),即:从执行效果上来看,包装元素后面的元素 就像 连接在 包装流程 后面一样;

4. 模块

在通过给定的流程描述(如:配置、语言)创建元素实例时,当解析完描述之后,首先需要先查找到元素的类型的定义(如:元素的类、构造函数 等),然后再用定义创建对应元素的实例,然后再将实例连接成流程;

这样,对于同一个流程描述,如果应用不同的元素类型定义集合,则会生成具有不同程序逻辑的流程(如下图),这种效果提供了更多的可能性 和 扩展性;

描述与类型集合

像这种,在通过流程描述创建流程实例的过程中 提供所需的类型集合 及 其它相关环境的程序角色称之为 模块

4.1. 特征

  • 提供了元素类型的定义;
  • 模块与模块之间可有父子关系;
  • 子模块会继承父模块所提供的上下文(如:元素类型定义 等),即:当通过 子模块查找元素的类型定义时,如果 子模块没有,但其父模块有,则通过父模块查找到的元素类型定义 即是 通过子模块查找的元素类型定义;
  • 子模块可覆盖父模块提供的上下文,,即:当通过 子模块查找元素的类型定义时,如果 子模块有相应的定义,则会采用该定义,而忽略 父模块中的定义;

5. 定义元素类型的方式

元素类型通常会被设计成类,在 JavaScript 中也有几种生成类的方式:

  • 类继承:通过语言提供的类继承的机制来定义类;示例如下:
    class SubNode extends Node {
      constructor(){}
      //成员
    }
    
  • 配置选项:有个工厂方法 接收 配置选项,根据配置选项的描述 生成相应的类;示例如下:
    var SubNode = extendNode({
      //选项
    })
    
  • 装饰器:通过装饰器来改造已有的类为相应的类型;示例如下:
    @NodeConf({
      //选项
    })
    class SubNode extends Node {
      constructor(){}
      //成员
    }
    

这几种方式各有优缺点,如下:

类继承:
通过语言提供的类继承的机制来定义类。

  • 优点:

    • 直观:能直观、明确地表明类型之间的继承关系;
    • 能充分利用类的特性,规划出组织合理的类继续,可根据影响范围灵活地在任一继承层级更改
    • 对已有的逻辑,子类拥有更大的定制性;
    • 对于类型的直接成员可较方便的定制;
  • 缺点:

    • 不方便处理需要经过处理的配置类成员;
    • 不利于对整个类作处理(例如:类的初初始化);
    • 对于需要兼容ES5以下的项目必须得经过降级编译处理;

配置选项:
有个工厂方法 接收 配置选项,根据配置选项的描述 生成相应的类。

  • 优点:

    • 方便处理需要经过处理的配置类成员;
    • 方便对整个类作处理(例如:类的初初始化);
    • 可直接在ES5以下的项目环境中运行;
  • 缺点:

    • 不能能直观、明确地表明类型之间的继承关系;
    • 不能充分利用类的特性;
    • 对已有的逻辑,子类拥有的定制性 相比 类继承 方案要弱的多;
    • 如果要实现灵活的类继续,需要框架暴露出灵活强大的类继承接口;

装饰器:
因为装饰器 可灵活应用在类 及 其成员上,又灵对类作灵活的定制,所以 装饰器 方案 具备 类继承 配置选项 这两种方案的优点于一身。

  • 优点:

    • 直观:能直观、明确地表明类型之间的继承关系;
    • 能充分利用类的特性,规划出组织合理的类继续,可根据影响范围灵活地在任一继 承层级更改
    • 对已有的逻辑,子类拥有更大的定制性;
    • 对于类型的直接成员可较方便的定制;
    • 方便处理需要经过处理的配置类成员;
    • 方便对整个类作处理(例如:类的初初始化);
  • 缺点:

    • 不能在不支持次装饰器语法的浏览器中直接运行,如果需要在任意低版本的JS环境中运行,则需要经过降级编译处理;

6. 构建流程的方式

定义好 节点 与 管道 之后,接下来就可以连接搭建 节点、管道 等 之间的 有向关系

节点、管道、有向关系 的集合 就称之为 流程

构建流程的方式有以下几种:

  • 手动连接实例:通过连接API,将 节点、管道 等个连接起来;示例如下:
    pipe1_2.connect(node1,node2);
    pipe2_3.connect(node2,node3);
    connect(node3,node4);
    
  • 配置对象:通过一定格式的对象来描述流程的各个元素和其有向关系;示例如下:
    {
      element: node1,
      pipe: pipe1_2,
      next: {
        element: node2,
        pipe: pipe2_3,
        next: {
          element: node3,
          next: node4
        }
      }
    }
    
  • 语言:通过形像化语法来描述流程的各个元素和其有向关系;示例如下:
    n1->n2->n3
    n3->n4
    n2->n5
    

这几种方式各有优缺点,如下:

手动连接实例:
通过连接API,将 节点、管道 等个连接起来。

  • 优点:

    • 灵活:可根据条件自由连接;
    • 不需要编译;
    • 不需要解析;
  • 缺点:

    • 不直观:一眼看上去,不能直观地反应整个流程的节点、管道、有向关系;
    • 分散:流程的各个部分可能会分散在不同的位置;
    • 复用性不强:因为是直接使用API作用于代码实体,所以依赖代码实体的上下文,所以不方便移植 和 复用;

配置对象:
通过一定格式的对象来描述流程的各个元素和其有向关系。

  • 优点:
    • 流程成员、有向关系 集中、完整:流程的所有元素、有向关系 等 都可以集中、完整地在配置中体现;
    • 较直观:通过配置对象的层级关系 能相对较直观地(相对 手动连接实例 方案)呈现出流程的节点、元素、有向关系;
    • 复用性强:因为整个配置集中、完整,且不依赖于运行时代码实体上下文,所以可以很方便的复用;
  • 缺点:
    • 灵活性低:相比 手动连接实例 方案,灵活性低,不能灵活地根据运行时上下文来定制流程;
    • 需要解析:需要解析对象,然后根据配置对象生成对应的流程;

语言:
通过形像化语法来描述流程的各个元素和其有向关系。

  • 优点:
    • 非常直观:通过形像化的语法 能非常直观地呈现出流程的节点、元素、有向关系;
    • 流程成员、有向关系 集中、完整:流程的所有元素、有向关系 等 都可以集中、完整地在语言语句中体现;
    • 复用性强:因为语言代码集中、完整,且不依赖于运行时代码实体上下文,所以可以很方便的复用;
  • 缺点:
    • 灵活性低:相比 手动连接实例 方案,灵活性低,不能灵活地根据运行时上下文来定制流程;
    • 需要编译、解析:需要将 语言 编译,并根据 语言描述 解析生成 流程;

7. Mixin

Mixin 是一种具备多继承的效果但解决了多继承路径的一种极具扩展性、复用性的设计模式。因此,如果想让框架更具灵活性、扩展性、复用性,则应提供 Mixin 模式的实现 API。




原文地址:访问原文地址
快照地址: 访问文章快照