面向流程的编程思想有了
(详情见
面向流程编程
)
,那如何设计面向流程的框架呢?本文说细说面向流程框架的通用设计,其内容包含了面向流程框架的概念、模块生命周期等方面的设计,但并不包含具体的API,而面向流程框架中的具体模块、成员、API 以及 类图 的设计应由具体的面向流程框架 实现;
目录
内容
根据
面向流程编程
的思想,如果要做一个 面向流程编程 的框架,则需要框架实现以下功能
-
抽象好的节点,让用户(框架的使用者)可以自定义节点的业务逻辑,其它的所有节点共有的特性 和 功能 均由框架来实现;
-
抽象好的管道,让用户可以自定义数据的转换逻辑,其它的所有管道共有的特性 和 功能 均由框架来实现;
-
提供 元素 (管理 和 节点 统称元素) 互相连接的便捷方法;
-
提供 通过 配置化 或 形象化语言 或 可视化 的连接方式;
其中第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。