有限状态机在国际计费中的应用探索 | 京东物流技术团队
今天的话题,我们从一个案例开始谈起。
国际计费系统会定期自动生成账单,然后每个账单会按照预设的规则自动进入结算流程,账单从生成之后到结算完成,这期间需要销售支持、结算岗、客户(商家或服务商)、财务、资金等多个不同岗位角色的人员共同参与处理,每个角色处理的环节和操作内容不同,账单的状态也持续发生着改变。
1 为什么要使用状态机
下面这张图,描述了海外应收账单整个生命周期内的全部状态,以及每个状态下可以进行哪些操作行为。
对着这张图,我们思考一个问题,在“客户已确认”状态下,能否进行“运营作废”操作呢?
从图中可以看出,“客户已确认”方框上只有一个出发箭头“推送结算”,就是说这个状态下,只能进行“推送结算”这一个操作,因此“客户已确认”状态下是不允许操作“运营作废”的。
这一点,从业务角度很好理解,如果一个账单已经让商家确认完毕,这时候我们再把它作废掉,后续势必涉及让商家重新确认,这对商家来说体验是不好的。
那我们在开发系统时,怎样才能避免这种情况发生呢?
有很多种方式可以实现,比如说,我们采用 if 判断,代码示例如下:
这种方式实现起来最简单,但是存在的问题也较为明显:
难以通过代码直观体现出“当前状态-操作行为-变更后的新状态这”3 者之间的对应关系;
当状态增加或减少时,要修改 if-else 代码块,当状态和操作行为较多时,容易改错;
如果开发不规范,把这种涉及状态管理的逻辑放到了前端去控制,不仅会使得前端逻辑复杂,还会导致实体状态不一致的严重风险;
我们可以考虑通过状态机来实现,这是一种更加有效稳妥的方式。
那么什么是状态机呢?
通常讨论的都是有限状态机。是表示有限个状态以及在这些状态之间的转移和动作等行为的数学模型。
(以下截图来自 zhihu.com)
其实,上面描述账单状态变化的这张图就是一个状态机。通过状态机可以集中、统一、规范地管理实体的状态变化。这种管理方式应用非常广泛也很成熟,比如程序代码编译、正则表达式、电子电器设备等领域。
2 主流状态机实现都有哪些,为什么自己开发
最开始需要用状态机时,首先想到的是,这种通用性的东西一定有现成的成熟开源框架。于实网上搜了一番,的确找到很多内容。有教你如何用 switch 方式写出比 if-else 更加优雅代码的,有利用枚举值做判断实现的,以及 Spirng 子项目 Spring State Machine。
首先说 switch 或枚举判断的方式,这种方式的问题在于框架性代码与状态配置代码紧密耦合在一起,对于有代码洁癖的我,将不同职能的代码混在一起我是难以接受的。
那按说 Spring 提供的框架总该可以吧,没错,Spirng State Machine(简称 SSM)在抽象层次、规范化、理解方面表现都很出色。但是,由于功能过于强大,导致对于简单的场景来说使用起来有些繁琐,有一种杀鸡用牛刀的感觉。
下面从 Spring State Machine 项目官网帮助文档中截取了一张图,通过目录中的关键词可以直观感受一下使用 SSM 的门槛。
本文一开始给出的应收账单状态机,看着似乎有一点点复杂。但是在实际的程序开发中,要实现这个状态机,只需要用到最简单的状态机类型和最基本的概念及特性即可。
因此,决定来开发一个适合自己当前需求的轻量级有限状态机框架(SimpleFSMFrame)。
3 设计思路及关键点
3.1 产品设计目标
一般的状态管理场景,对于状态机的主要诉求只有 2 点:
判定在某个状态(State)下是否允许进行某个指定的操作行为(Event);
反馈在某个状态(State)下都允许进行哪些操作行为(Event);
对于更加复杂的场景,不在本次设计考虑范围内,将作为未来扩展的方向。
3.2 技术实现目标
既然定位成框架,那么就需要具备以下特性:
可复用,该框架可以开源或者以 jar 包形式提供给别人使用;
简单易用,只需了解状态机最基本的 3 个概念即可:State(状态)、Event(事件)、Transition(转换);
与业务无关,框架本身只实现状态机本身的基本概念和功能特性,不包含任何具体实体的状态转换关系管理,也就是说不能对使用者产生干扰。
能扩展,模块粒度以及层级拆分合理,高内聚低耦合
3.3 框架详细设计
组件 1:StateMachine 状态机接口
定义了状态机的行为,包含了上述 2 个诉求点。
组件 2:State 状态接口
规范了作为“状态”概念的对象应当具备的最基本的行为。
组件 3:Event 事件接口
规范了作为“事件”概念的对象应当具备的最基本的行为
组件 4:Transition 状态转换关系接口
定义了在一个条状态与事件的转换关系中,哪些对象应当参与其中以及各个对象在其中所扮演的角色。
组件 5:SimpleFSMFrame 轻量级有限状态机框架
提供状态机基本概念与行为的实现。使用者只需继承此类即可实现一个状态机实例。
关键设计
首先看这个类的构造方法:
构造方法要求必须传入一个初始状态,这个参数在创建状态机时直接可以把状态置为指定的初始状态,而不必让状态机从真正的初始状态开始,避免了类似 SSM 中需要先对状态机本身进行序列化以及持久化,然后再反序列化恢复状态的繁杂过程。
对于状态机中最为关键,对于框架程序来说最需要解耦的部分,即状态转换关系配置部分,是整个设计中的重中之重。需要考虑灵活易配置、来源方式开放、对框架程序无任何耦合这几个目标。
因此在构造方法的第二个参数中,要求传入该状态机的完整转换关系,形式为数组。用户程序(即继承此类的子类)可以按照自己最方便的方式来“整理”状态转换关系。比如,将状态转换关系存到数据库中,构建状态机时从数据库中读出来即可;再比如,通过专门的图形化状态机绘制工具将画好的状态机图形转换为这里要求的数组数据,以便构造一个新的状态机。因此对于状态关系的配置方式是支持扩展的。
但是这里之所以设计为数组形式,其实是有另有考虑的。可以用枚举 enum 来定义状态转换关系,然后用 values()方法就能轻松获取到全部的转换关系了,而且是数组形式。——利用了 java 语言的特性,如果是非 java 语言可以考虑类似方式。
下面给出这个类的详细代码:
整体思路是,将构造方法传入的所有状态转换关系放到定义为私有内部类 TransitionBox 这样一个容器中保管,避免对外暴露内部实现细节,在 TransitionBox 中会对关系配置进行校验,以及整理为 3 个不同的 map,并通过这些 map 实现状态机的行为判断。
4 使用案例
4.1 定义状态机
对于使用者来说,只需 3 步即可完成一个全新的状态机实现:
实现 State 和 Event 接口,定义自己的状态和事件;
定义枚举类并实现 Transition 接口,状态转换关系通过枚举值形式配置出来;
继承 SimpleFSMFrame 类,调用上一步枚举类的 values()方法并传入构造方法;
下面给出一个项目中实际使用的案例:
4.2 使用状态机
5 改进空间讨论
分层多级状态如何支持?
例如,账单第一级状态可分为,初始、客户确认中、待结算、完成。其中待结算状态又细分二级状态为:已推送结算、财务审批通过、资金撤单、结算完成。这样,状态之间不再是简单的互不包含,而是存在包含关系,也就是出现了复合状态。
针对这个问题,大家是如何看的,欢迎讨论~
作者:京东物流 谢益培
来源:京东云开发者社区 自猿其说 Tech 转载请注明来源
版权声明: 本文为 InfoQ 作者【京东科技开发者】的原创文章。
原文链接:【http://xie.infoq.cn/article/a9faececebd57d71c08df5da0】。文章转载请联系作者。
评论