「本文废弃了 1.0 文档中的部分内容,并对大部分内容进行了修订」
在经历了 PUG
上一设计版本的实现困难之后,我开始重新审视 PUG
的抽象结构。
Pipe 的存在意义(废弃)
PUG
上一个版本将处理的最小单元设计为 Pipe
,用以划分某些未知的操作。当时举出的例子就是用户登录,但经过分析发现登录可能是目前 Pipe
存在的唯一意义。
并且,通过 Pipe
处理还存在一系列的问题。
首先,这个过程是线性的。以错误处理为例,Pipe
的存在意味着用户必须手动声明所有的错误处理。这个过程很 Golang
,但不符合用户直觉,增加了使用成本。
其次,对于大量的通用内容,以处理后通知(Notify after process)为例,如果我们想把每个 Pipe
的处理结果都通过 Telegeam Bot
反馈给用户,那么用户就需要手动配置大量 Bot
配置相关的内容。但这并不是最致命的,如果要考虑 Bot
运行时出现的错误,那么用户就需要配置一条 Pipeline
而不是 Pipe
。而 Pipeline
是线性的,这就导致错误处理只能简单地处理单种错误,要同时处理多种错误就必须使用:线性传递错误内容;或将错误处理集成到一个 Pipe
中——这两种中的一种。
最后,Pipe
的局限用途导致了不必要的抽象层次增加。
综合这三点来看,Pipe
的设计是失败的。因此我们选择废弃 Pipe
的概念。
Pipeline 的变化(更名)
在 Pipe
废弃之后,Pipeline
的工作方式也就发生了变化。现在 Pipeline
作为设计的最小单元,使用 Pipeline
这个名字就有点不大合适了。
最终,我们决定将 Pipeline
更名为 Pick
,取自 PUG
第一个单词的这个名字更能反映处理节这一功能本质。
Workflow 的控制功能(废弃)
在 1.0
版本中,Workflow
是作为全程序的最大框架存在的。为了实现更复杂的功能,还计划给 Workflow
增加分支选择等控制功能。
但是仔细想想,这一切真的值得吗?
分支选择的需求是针对错误处理而提出的,但在实现的过程中存在各种各样的问题。因此最终,我们决定将 Workflow
的控制功能废弃
事件:消息与钩子
为了弥补上述设计削减导致的功能缺失,我们引入了事件和钩子两个概念。
事件是每个 Pick
和 Workflow
都存在的公共属性。Pick
和 Workflow
通过事件向外部发布消息,而外部模块通过钩子影响 Workflow
的执行。
我们将不影响 Workflow
执行的事件称为 Message
,而将影响 Workflow
执行的事件称为 Hook
。
此时,对于执行过程中发生的错误,其处理方式就交由事件处理器处理了。我们将每次事件的调用称为一次 Up
,意味在设计上事件处理位于流程执行的上层。
从实现上来看,错误处理的过程也是一种 Hook
处理流程的过程。因为在错误处理之后我们需要重启或中止当前 Pick
或 Workflow
,而这个过程明显是会影响工作流程的。
事件:优先级与传递
对于不同的事件,抽象出优先级的概念。较高优先级的事件优先执行,同级优先级的事件根据注册时间先后依次执行。
事件优先级最高为 0,默认为 100。
事件具有传递性,即一个事件处理器处理完后会将处理后的事件交由下一级处理器处理。
较高级事件能够选择屏蔽较低级事件的执行,这个过程称为传递屏蔽。
事件:内部/外部处理器
对于有些事件,特别是错误处理事件,我们往往希望定义一个默认的错误事件处理器,而不是对每个 Workflow
配置相同的错误处理器。由此,我们抽象出内部处理器和外部处理器两种不同的事件处理方式。
内部处理器是一种对用户透明的事件处理器,它实际上是对 Pick
实现的一种强制解耦。内部处理器拥有最高的优先级,在事件发生时最优先执行。
对于最常见的内部错误处理器,其通常还会在错误处理成功(如成功登录)后屏蔽事件的传递。
外部处理器则是用户声明使用的事件处理器,其优先级根据用户配置而定,按照优先级的设置执行。
持久化:公共存储区
为了实现公共内容的持久化(如 Cookies
),我们为所有 PUG
的部分提供了公共存储区。
公共存储区用于存储下次处理必须用到的信息,以方便 PUG
快速从内存/磁盘重启,减少诸如登录错误的错误处理次数。
例如,http
客户端需要在每次请求后将 cookies
同步到公共存储区。
结语
从结果来看,上一个版本的 PUG
在某些地方确实过度设计了,但基本框架是没有问题的。比如将 Workflow
的执行结果分为多份,在新的设计中可以通过事件消息简单实现,可以说是只是换了个实现的皮。
有一些问题没有被讨论,如 Preprocessor
对 Workflow
的选择,这样的预处理器是否必要仍然在思考中。以及对公共存储区/ Workflow
存储区变量类型的限定等,这些相对细节的问题也处于 Pending
状态。
上一个设计版本中可能过于强调 Pipe
的存在性了。由于 Pipe
/ Pipeline
系统的印象过于深刻,因此在 Workflow
提出后仍然保留了这一组处理单元。这也算是历史遗留导致的设计问题了吧(笑)