本文通过设计一个简单的任务中心来展示一个使用有限状态机思想设计该如何做。
本文使用 JSDOC 添加注释。
关于有限状态机
有限状态机 (FSM) 维基 百度百科 是用来表示有限个状态及在这些状态之间转移和动作的数学模型。早在07年就有文章讲解 js 与状态机的结合。
在程序中,我们用这种数学模型抽象业务的复杂行为。作为状态机,有以下特征特点:
- 事物拥有的状态是有限的。
- 事物在任一时刻,只处在一种状态。
- 可以通过触发事物的某些行为,可以导致事物从一种状态过渡到另外一种状态。
- 一种行为一般只能将其他状态变为一种特定状态,但不能将其变为多种状态。
设计实现组件
任务中心具有以下功能:
- 任务中心可以在任何时刻加入新任务
- 任务中心在启动时才会运行任务
- 任务在任务中心运行时会被即时执行
- 任务中心可以被终止,暂停,恢复
如果针对以上需求,整理整个流程, 可以认为任务中心的流程如下:
因为需求中存在“可以在任何时间塞入任务”,所以对于不同的流程设定任务的不同执行状态。
- 任务中心创建后即可允许塞入任务
- 任务中心运行状态中,任何任务塞入会被即时执行
- 任务中心暂停或停止时,任何任务塞入也不会被执行
- 任务中心恢复运行时,会执行所有之前塞入并没有被执行的任务
分离了任务中心和任务,这两种并行的状态,我们就可以分开设计,分开编码了。
那么先做任务中心组件的实际设计,关于这个组件可能有的状态如下:
-
初始状态
-
运行状态
-
暂停状态
-
恢复状态
-
恢复后的运行状态
-
停止状态
-
重启状态
........
但由于我们组件有的功能,我们仅为组件设计了四个状态就可以描述其整个生命流程:
- IDLE 初始化状态
- RUNNING 运行状态
- PAUSED 暂停状态
- STOPPED 停止状态
因为我们的这个任务中心是给任务调度用的,所以仅跟任务有关的状态才是有用的状态,这也是状态机设计思维的核心之一,仅设计使用相关联的状态, 并保证剩余的状态可成为完整的流程, 任务中心的四个状态组成流程如下图所示:
实现组件
描述状态
为了更好的展示事务状态,我们在组件状态切换中加入明确的状态标识。并在类的状态列表定义它们:
/**
* 枚举 InstantTask 组件状态
* @enum {Number}
* @readonly
*/
InstantTask.State = Object.freeze({
/**
* @memberof InstantTask.State
* @type {Number}
*/
IDLE: 0,
/**
* @memberof InstantTask.State
* @type {Number}
*/
RUNNING: 1,
/**
* @memberof InstantTask.State
* @type {Number}
*/
PAUSED: 2,
/**
* @memberof InstantTask.State
* @type {Number}
*/
STOPPED: 3,
});
复制代码
描述行为
组件一共存在五个行为, 这些行为在数学中可以被认为是变换器,它们就是上述状态机运转图的具体行为体现。
-
start()
用于启动组件,设置组件为
RUNNING
状态。上一个状态可以是任何状态。
用于启动任务中心,并会执行所有之前塞入并没有被执行的任务
-
pause()
用于暂停组件,设置组件为
PAUSED
状态。上一个状态只能是
RUNNING
状态。用于暂停任务中心,在暂停后任何任务塞入也不会被执行
-
resume()
用于恢复组件的暂停状态,设置组件为
RUNNING
状态上一个状态只能是
PAUSED
状态用于启动任务中心,并会执行所有之前塞入并没有被执行的任务,和start()之间功能差距不大,但是不会像start()一样重置所有设定,而且主要用途是和pause方法做一一对应。一个方法或者状态的多用途对于状态机设计来说会增加架构复杂度,尽量避免吧。
-
stop()
用于停止任务中心,设置组件为
STOPPED
状态上一个状态只能是
RUNNING
状态用于停止任务中心,和pause的区别是它会记录停止状态,并且不能被resume,和pause内部逻辑差距不大。主用途是和start()做对立方法。
-
reset()
用于重置任务中心,设置组件为
IDLE
状态上一个状态可以是任何状态, 但主要是
STOPPED
状态用于重置任务中心,此方法等于生成一个新的任务中心。
实现组件
当我们确定组件的抽象运行状态和运行行为后,就可以结合实际业务逻辑来实现具体的组件了。 本文中使用 EventEmitter
来作为事件的广播和处理,前端也可以借用 eventemitter3
这样的近似的类实现同样的功能。
同时接下来本文使用代码中注释的方式继续说明每个方法的详细用途:
具体代码已经放在: https://gist.github.com/Suixinlei/4b2da4ee1ef84e89b5cfccc1b88b3e4f
测试代码放在: https://gist.github.com/Suixinlei/d8441babe5174b7b1d4326f39b0fcff2
测试结果如下所示,我们可以看到实际运行结果和我们的抽象设计完全一致,充分证明抽象设计对最终逻辑的影响:
添加任务 instant1
添加任务 instant2
任务中心开始运行
instant1 run
instant2 run
添加任务 instant3
instant3 run
任务中心已暂停
添加任务 instant4
恢复运行
instant4 run
复制代码
拓展延伸
目前来说,所有任务都是即时任务,所以对于任务中心的状态仅有四种即可满足需求。如果这些任务有一些是定时任务,甚至可以循环执行并规定执行次数呢?读完这篇文章我想你心中应该已经有些想法, 那么看看下面的最终实现和你想的是否有些出入呢?
这里是最终实现: https://gist.github.com/Suixinlei/e245812799fa160cdf0bbcdf279e68bc
总结
使用 FSM 设计组件可以
- 让组件变得逻辑清晰
- 组件逻辑和业务逻辑分离,从概念上有良好的分层结构
- 组件易测试,组件实现初期仅需测试组件逻辑部分而非复杂的业务逻辑
- 梳理组件逻辑闭环,可以较容易的发现组件生命周期中缺少的部分,反推业务进行改进
关注查看更多原创内容
关注公众号投递简历 (招聘视觉、交互、前端)