Skip to content

【设计文档】对 PUG 的大规模设计修订(1.1)

Published: at 10:46

「本文废弃了 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 的控制功能废弃反正还没有实现,将所有的分支功能交由初始状态和外部处理。

事件:消息与钩子

为了弥补上述设计削减导致的功能缺失,我们引入了事件钩子两个概念。

事件是每个 PickWorkflow 都存在的公共属性。PickWorkflow 通过事件向外部发布消息,而外部模块通过钩子影响 Workflow 的执行。

我们将不影响 Workflow 执行的事件称为 Message,而将影响 Workflow 执行的事件称为 Hook

此时,对于执行过程中发生的错误,其处理方式就交由事件处理器处理了。我们将每次事件的调用称为一次 Up,意味在设计上事件处理位于流程执行的上层

从实现上来看,错误处理的过程也是一种 Hook 处理流程的过程。因为在错误处理之后我们需要重启或中止当前 PickWorkflow,而这个过程明显是会影响工作流程的。

事件:优先级与传递

对于不同的事件,抽象出优先级的概念。较高优先级的事件优先执行,同级优先级的事件根据注册时间先后依次执行。

事件优先级最高为 0,默认为 100。

事件具有传递性,即一个事件处理器处理完后会将处理后的事件交由下一级处理器处理。

较高级事件能够选择屏蔽较低级事件的执行,这个过程称为传递屏蔽

事件:内部/外部处理器

对于有些事件,特别是错误处理事件,我们往往希望定义一个默认的错误事件处理器,而不是对每个 Workflow 配置相同的错误处理器。由此,我们抽象出内部处理器外部处理器两种不同的事件处理方式。

内部处理器是一种对用户透明的事件处理器,它实际上是对 Pick 实现的一种强制解耦。内部处理器拥有最高的优先级,在事件发生时最优先执行

对于最常见的内部错误处理器,其通常还会在错误处理成功(如成功登录)后屏蔽事件的传递。

外部处理器则是用户声明使用的事件处理器,其优先级根据用户配置而定,按照优先级的设置执行。

持久化:公共存储区

为了实现公共内容的持久化(如 Cookies),我们为所有 PUG 的部分提供了公共存储区。

公共存储区用于存储下次处理必须用到的信息,以方便 PUG 快速从内存/磁盘重启,减少诸如登录错误的错误处理次数。

例如,http 客户端需要在每次请求后将 cookies 同步到公共存储区。

结语

从结果来看,上一个版本的 PUG 在某些地方确实过度设计了,但基本框架是没有问题的。比如将 Workflow 的执行结果分为多份,在新的设计中可以通过事件消息简单实现,可以说是只是换了个实现的皮。

有一些问题没有被讨论,如 PreprocessorWorkflow 的选择,这样的预处理器是否必要仍然在思考中。以及对公共存储区/ Workflow 存储区变量类型的限定等,这些相对细节的问题也处于 Pending 状态。

上一个设计版本中可能过于强调 Pipe 的存在性了。由于 Pipe / Pipeline 系统的印象过于深刻,因此在 Workflow 提出后仍然保留了这一组处理单元。这也算是历史遗留导致的设计问题了吧(笑)