{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# 有限状态机" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "有限状态机(*finite state machine*,简称 *FSM*),有时也被称为 *finite state automation*,有时就简单地叫 *state machine*,不属于一看就知道大概是什么的概念(这一点和前面我们讲过的都不一样)。有限状态机有相当深刻的理论背景,算是比较高级的东西了,很多程序员别说学校里,工作十年可能都没碰过这东西,但其实真的不难理解,而且学会了就爱不释手,因为它解决某些问题真是太好用了。" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 什么是有限状态机" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "其实我们身边到处都是“有限状态机”的例子,最简单的一个是灯:灯有两种状态:“亮”和“熄”,灯可以从一种状态变成另一种,“亮”的状态下接收到“关”的指令就会变成“熄”,“熄”的状态下接收到“开”的指令就会变成“亮”,就像下图这样:" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "<img src=\"assets/fsm-1.png\" width=\"360\">" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "在这个图里,圆圈表示“状态(*state*)”,箭头代表状态间可以发生的“转换(*transition*)”,而箭头上标注的文字代表触发状态转换的“输入(*input*)”。这基本上就是状态机的三大要件了。\n", "\n", "灯只有两个状态,不算很有意思,我们可以再看一个常见的例子:红绿灯,我们熟知的红绿灯的颜色按照“绿 -> 黄 -> 红 -> 绿”这样的顺序循环变化——嗯,我知道有的还有“绿灯闪烁”之类的状态,不过我们这里简化一下,用有限状态机来描述大致如下图:" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "<img src=\"assets/fsm-2.png\" width=\"500\">" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "这里:\n", "* 有三种状态:绿,黄,红;\n", "* 状态转换是受限的,绿只能转黄,黄只能转红,红只能转绿;诸如黄转绿这样的状态转换是不允许的;\n", "* 状态转换的输入条件很简单,接收到 1 就转换到下一个状态。" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "所以简单来说,有限状态机就是一台包含了预先定义好的一组状态的机器,当机器接收到一个指令,就根据指令内容查一张预先定义好的表:\n", "1. 检查当前状态是否接受这个指令;\n", "2. 如果不接受,那就当无事发生;\n", "3. 如果接受,再检查表中“当前状态x该指令”对应的目标状态是什么,然后把机器状态转换为目标状态。\n", "\n", "至于何时发送指令给状态机,是由外部系统决定的,比如红绿灯的例子里,外部系统是几个定时器,时间到了就发信号给有限状态机切换状态。\n", "\n", "有了现实生活中的例子打底,我们现在可以来看看抽象的“有限状态机(*FSM*)”是怎么定义的了。" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "<img src=\"assets/fsm-3.png\">" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "如上图所示,每一个 *FSM* 都包含五个要素:\n", "* *Q* 包含了有限个状态(*states*)的集合;\n", "* *Σ* 包含了有限个、非空的有效输入(*input*)的集合;\n", "* *δ* 一系列转换函数(*transition functions*),定义了什么样的当前状态结合什么输入可以变成什么目标状态,通常可以定义为一张二维表(见上图);\n", "* *q<sub>0</sub>* 起始状态,并不是所有 *FSM* 都关心起始状态,比如红绿灯就无所谓起始状态;\n", "* *F* 包含了所有“结束状态(*final states*)”的集合,这个名字容易误解,它的作用和有限状态机的具体类型及面对的问题有关,我们可以简单理解为“标记出来有特别含义的状态的集合”就可以了,注意这个集合可以是空的。" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "以上图所示的 *FSM* 为例,\n", "* 这个 *FSM* 有四个有效状态,*Q = { A, B C, D}*;\n", "* 这个 *FSM* 只接受两个合法输入,*Σ = { 0, 1 }*;\n", "* 当这个 *FSM* 接收到输入时,不在 *Σ = { 0, 1 }* 中的输入会被丢弃;如果输入在 *Σ* 中(是 *0* 或者 *1*),就查 *δ* 表,看看当前状态对应行和输入对应列的交叉点是什么状态,比如当前状态是 *A*,输入是 *1*,那么对应状态为 *C*,也就是说应该转换到状态 *C*。\n", "* 起始状态 *q<sub>0</sub> = A* 和结束状态集 *F = { D }* 这两个对某些 *FSM* 来说很重要,比如正则表达式。\n", "\n", "> 正则表达式对应一大类有限状态机,主要用来解决“在一系列输入之后是什么状态”的问题,通过回答这个问题来判断输入序列是不是我们想要的,或者输入序列属于什么分类,这种状态机有很深刻的理论背景,有兴趣的话可以读一下计算理论(*computation theory*)的经典教材,比如[这本](https://www.amazon.com/Introduction-Theory-Computation-Michael-Sipser/dp/113318779X);这类状态机还被广泛应用于人工智能(比如图像识别算法)中。" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "在计算机编程领域 *FSM* 最广泛的应用之一是流程和行为控制(*flow and behavior control*),简单说就是管理:\n", "* 在某个状态下什么能做什么不能做;\n", "* 做了什么会变成另外的什么状态。\n", "\n", "游戏里[玩家行为控制](https://gameprogrammingpatterns.com/state.html)、[NPC(*non-player character*,非玩家角色)的 AI](https://gamedevelopment.tutsplus.com/tutorials/finite-state-machines-theory-and-implementation--gamedev-11867)、剧情任务流程等都是用 *FSM* 来实现的;所有的[工作流系统](http://b.xfreeservice.com/redir/clickGate.php?u=8otB939m&m=12&p=3b121G4eNI&t=33&splash=0&s=&q=state%20machine%20workflow&url=https%3A%2F%2Fdocs.microsoft.com%2Fen-us%2Fdotnet%2Fframework%2Fwindows-workflow-foundation%2Fstate-machine-workflows)都包含 *FSM*;还有电商核心系统之一的“订单系统”(*order system*)。\n", "\n", "我们用过淘宝都知道,一个订单从创建开始要经历好几个状态,中间也有不同的操作可以进行,下面是一个比较典型的流程设计,经过一定简化,并以“状态”的主视角来描绘:" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "<img src=\"assets/fsm-4.png\">" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "图中圆边的矩形代表状态,最上面一排是“正常”的状态和流程;第二排的矩形则表示一些“逆向”子流程,通常是由用户或客户发起的特殊操作,这些操作会带来其他一些订单状态,为了简单起见没有在这里展开。" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "流程说明:\n", "* 当买家点击下单时订单生成,处于“已创建”状态;\n", "* 这个状态下的正常操作是“支付”,如果输入“支付成功”会进入下一个状态“已支付”,“支付失败”或者没有任何操作则停在本状态;\n", "* 这个状态下其他可选操作包括“修改”、“取消”等,分别会去到订单修改和订单取消子流程(这里略去细节);\n", "* 支付成功后进入处于“已支付”状态;\n", "* 这个状态下需要等待商家发货,商家输入“已发货”会进入下一个状态“配送中”;\n", "* 这个状态下不能修改订单了,但仍然可以取消订单;\n", "* 商家发货后进入“配送中”状态;\n", "* 当配送到货,买家签收成功输入则进入下一个状态“已签收”;如果配送失败(买家不在家之类的情况)则留在“配送中”状态(另外择时重新送货);\n", "* 这个状态下已不能修改和取消订单,但是可以发起退货申请,进入退货子流程(这里略去细节);\n", "* 买家签收后进入“已签收”状态;\n", "* 买家满意,确认订单完成则进入最后状态“已完成”,订单生命周期结束;\n", "* 否则买家可以发起退货进入退货子流程(略)。\n", "\n", "从这里我们可以看到,实际业务系统中状态和转换的规则相当复杂(我们这还是大大简化的版本),每个状态下允许的操作和可能转换的下一个状态都是严格受控的,现在我们思考一下,我们可以如何用程序来实现这样的流程呢?" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 利用有限状态机编写易于维护的代码" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "回忆我们之前提到的,流程和行为控制(*flow and behavior control*)的关键是管理:\n", "* 在某个状态下什么能做什么不能做;\n", "* 做了什么会变成另外的什么状态。\n", "\n", "最简单直接的办法就是书写一堆 `if...else` 的判断规则,大致会是这个样子:" ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "class Order:\n", " STATES = {'created', 'paid', 'delivering', 'received', 'done', 'cancelling', 'returning', 'closed'}\n", " state = 'created'\n", " \n", " def can_pay(self):\n", " return state == 'created'\n", " \n", " def can_deliver(self):\n", " return state == 'paid'\n", " \n", " def can_cancel(self):\n", " return state == 'created' or state == 'paid'\n", " \n", " def can_receive(self):\n", " return state == 'delivering'\n", " \n", " # 还有一大堆类似这样的规则,不写了\n", " \n", " def payment_service(self):\n", " # 调用远程接口完成实际支付\n", " pass\n", " \n", " # 然后是关键操作的实现,比如支付\n", " def pay(self):\n", " if self.can_pay(self):\n", " result = payment_service(self)\n", " if result.succeeded:\n", " state = 'paid'\n", " return True\n", " else:\n", " return False\n", " else:\n", " raise ValueError()\n", " \n", " def cancel(self):\n", " if self.can_cancel(self):\n", " self.state = 'cancelling'\n", " # 取消订单,申请审批和清理数据,如果顺利成功再——\n", " self.state = 'closed'\n", " else:\n", " raise ValueError()\n", " \n", " # 还有一大堆操作的函数,每一个里面都要判断状态是不是可以做想做操作\n", " # 然后根据执行的情况修改 self.state 为对应的新状态" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "这样的代码非常冗长和重复,难以维护且难以修改,设想一下,假设在上面的基础上再增加一个状态,要连带修改不确定几处地方,做完这样的修改还需要相应修改所有的测试用例,累就不说了,关键是容易出错。\n", "\n", "有限状态机实际上是这些“八股”的通用实现,然后提供一个非常简洁的接口供我们使用。有兴趣的话可以自己尝试用 Python 写一个 *FSM* 的实现出来,只做最基本功能的话也不是很难,但我们实际上没必要自己写,Python 有不少 *FSM* 的第三方实现,比如 [transitions](https://github.com/pytransitions/transitions) 这个库,我们下面就用它来演示一下上面的流程如何用 *FSM* 实现。\n", "\n", "运行下面的例子之前需要在命令行界面运行 `pip install transitions` 来安装这个库。也可以在 *notebook* 里打开一个新的 *cell* 直接输入 `!pip install transitions`,和在命令行运行的效果是一样的。" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "支付服务申请中……\n", "已通知用户:商品配送中\n" ] }, { "data": { "text/plain": [ "'done'" ] }, "execution_count": 2, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# 引入 transitions 库里的核心类\n", "from transitions import Machine\n", "\n", "class Order:\n", " # 定义状态集\n", " states = ['created', 'paid', 'delivering', 'received', 'done', 'cancelling', 'returning', 'closed']\n", " \n", " def __init__(self, order_id):\n", " self.order_id = order_id\n", " \n", " # 创建 FSM\n", " self.machine = Machine(model=self, states=Order.states, initial='created')\n", " \n", " # 定义状态转换函数\n", " # 基本的语法很好懂,trigger 参数是输入函数名,source 和 dest 分别是当前和转换后的状态\n", " # before 参数表示进行这个状态转换之前要调用的函数,如果该函数运行时出现异常,状态转换会中止\n", " self.machine.add_transition(trigger='t_pay', source='created', dest='paid', before='payment_service')\n", " # after 参数表示当这个状态转换完成后调用的函数,我们用这个函数来通知用户已经发货在途了\n", " self.machine.add_transition(trigger='t_deliver', source='paid', dest='delivering', after='notify_delivering')\n", " self.machine.add_transition(trigger='t_receive', source='delivering', dest='received')\n", " self.machine.add_transition(trigger='t_confirm', source='received', dest='done')\n", " # 可以一次定义多个状态向同一个状态的装换\n", " self.machine.add_transition(trigger='t_cancel', source=['created', 'paid'], dest='cancelling')\n", " self.machine.add_transition(trigger='t_return', source=['delivering', 'received'], dest='returning')\n", " self.machine.add_transition(trigger='t_close', source=['cancelling', 'returning'], dest='closed')\n", " \n", " def payment_service(self):\n", " print('支付服务申请中……')\n", " # 调用远程接口完成实际支付,如果失败可抛出异常,对应的状态转换会中止(即,不会转换到 'paid' 状态)\n", " return\n", " \n", " def notify_delivering(self):\n", " # 通知用户已发货在途\n", " print('已通知用户:商品配送中')\n", " return\n", "\n", "# 然后就可以测试一下了\n", "order1 = Order(1)\n", "order1.state # => 'created'\n", "# order1.t_receive() # => 如果运行这一句会抛出 MachineError 异常,因为当前状态与此 trigger(输入)不匹配,转换不被允许\n", "order1.t_pay() # => 会先调用 payment_service(),成功的话返回 True\n", "order1.state # => 'paid'\n", "order1.t_deliver() # => 成功后调用 notify_delivering() 通知用户\n", "order1.t_receive()\n", "order1.t_confirm()\n", "order1.state # => 'done'" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "除了具体业务执行的代码,上面基本上完整实现了流程控制的部分,值得注意的是,借助 *FSM* 的实现,不仅简洁易懂,而且易于维护,假定我们需要对流程规则进行修改,或者在某些状态转换的前后添加一些操作,我们通常都只需要修改一处代码,而不用到处找哪里还要改。\n", "\n", "顺便说一下, [transitions](https://github.com/pytransitions/transitions) 这个库还有不少强大的功能,有兴趣可以自行发掘下。" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 小结" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "本章介绍了重要的数据模型“有限状态机(*FSM*)”,需要理解其背后的现实世界模型、具体应用及其带来的好处。\n", "* *FSM* 是对程序中一组的状态进行管理的工具;\n", "* *FSM* 能够精简程序里的逻辑判断,我们只需要陈述规则,*FSM* 自动管理什么可以什么不可以;\n", "* 尝试体会和理解 *FSM* 背后的抽象思维方式,如何从特定问题中抽象出可以普遍应用的通用工具。" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.7.4" } }, "nbformat": 4, "nbformat_minor": 4 }