有限状态机在国际计费中的应用探索

(给ImportNew加星标,提高Java技能)


今天的话题,我们从一个案例开始谈起。


国际计费系统会定期自动生成账单,然后每个账单会按照预设的规则自动进入结算流程,账单从生成之后到结算完成,这期间需要销售支持、结算岗、客户(商家或服务商)、财务、资金等多个不同岗位角色的人员共同参与处理,每个角色处理的环节和操作内容不同,账单的状态也持续发生着改变。
1、为什么要使用状态机
下面这张图,描述了海外应收账单整个生命周期内的全部状态,以及每个状态下可以进行哪些操作行为。

对着这张图,我们思考一个问题,在 “客户已确认” 状态下,能否进行 “运营作废” 操作呢?
从图中可以看出,“客户已确认” 方框上只有一个出发箭头 “推送结算”,就是说这个状态下,只能进行 “推送结算” 这一个操作,因此 “客户已确认” 状态下是不允许操作 “运营作废” 的。
这一点,从业务角度很好理解,如果一个账单已经让商家确认完毕,这时候我们再把它作废掉,后续势必涉及让商家重新确认,这对商家来说体验是不好的。
那我们在开发系统时,怎样才能避免这种情况发生呢?
有很多种方式可以实现,比如说,我们采用 if 判断,代码示例如下:
if (状态 = “客户已确认”) {    if (操作行为 = “推送结算”) {        pushToSettle();    } else {        throw new UnsupportedOperationException(“客户已确认状态下不能操作除推送结算以外的其他操作”);    }} else if (状态 = 其他XXX) {    // 其他判断处理…}

这种方式实现起来最简单,但是存在的问题也较为明显:
难以通过代码直观体现出 “当前状态 - 操作行为 - 变更后的新状态这” 3 者之间的对应关系;
当状态增加或减少时,要修改 if-else 代码块,当状态和操作行为较多时,容易改错;
如果开发不规范,把这种涉及状态管理的逻辑放到了前端去控制,不仅会使得前端逻辑复杂,还会导致实体状态不一致的严重风险;
我们可以考虑通过状态机来实现,这是一种更加有效稳妥的方式。
那么什么是状态机呢?
通常讨论的都是有限状态机。是表示有限个状态以及在这些状态之间的转移和动作等行为的数学模型。

其实,上面描述账单状态变化的这张图就是一个状态机。通过状态机可以集中、统一、规范地管理实体的状态变化。这种管理方式应用非常广泛也很成熟,比如程序代码编译、正则表达式、电子电器设备等领域。

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 个诉求点。
/** * 在当前状态下执行某个事件 * * @param event 事件 * @return 若执行成功则返回变更后的新状态 * @throws UnsupportedOperationException 如果当前状态不支持该操作则抛出此异常 */State onEvent(Event event) throws UnsupportedOperationException;
/** * 当前的状态 * * @return */State getState();
/** * 当前状态可执行的事件清单 * * @return */List < Event > acceptableEvents();
/** * 当前状态是否可以执行指定的某个事件(仅给出是否允许执行的判断结论,不会真的执行) * * @param event 事件 * @return */boolean canPerformEvent(Event event);

  • 组件 2:State 状态接口

规范了作为 “状态” 概念的对象应当具备的最基本的行为。
  • 组件 3:Event 事件接口

规范了作为 “事件” 概念的对象应当具备的最基本的行为
  • 组件 4:Transition 状态转换关系接口

定义了在一个条状态与事件的转换关系中,哪些对象应当参与其中以及各个对象在其中所扮演的角色。
  • 组件 5:SimpleFSMFrame 轻量级有限状态机框架

提供状态机基本概念与行为的实现。使用者只需继承此类即可实现一个状态机实例。
关键设计
首先看这个类的构造方法:
/** * 初始化一个状态机 * * @param initialState 初始状态 * @param transitions  状态与事件之间的转换关系 */public SimpleFSMFrame(State initialState, Transition[] transitions) {    state = initialState;    this.transitionBox = new TransitionBox(transitions);}

构造方法要求必须传入一个初始状态,这个参数在创建状态机时直接可以把状态置为指定的初始状态,而不必让状态机从真正的初始状态开始,避免了类似 SSM 中需要先对状态机本身进行序列化以及持久化,然后再反序列化恢复状态的繁杂过程。
对于状态机中最为关键,对于框架程序来说最需要解耦的部分,即状态转换关系配置部分,是整个设计中的重中之重。需要考虑灵活易配置、来源方式开放、对框架程序无任何耦合这几个目标。
因此在构造方法的第二个参数中,要求传入该状态机的完整转换关系,形式为数组。用户程序(即继承此类的子类)可以按照自己最方便的方式来 “整理” 状态转换关系。比如,将状态转换关系存到数据库中,构建状态机时从数据库中读出来即可;再比如,通过专门的图形化状态机绘制工具将画好的状态机图形转换为这里要求的数组数据,以便构造一个新的状态机。因此对于状态关系的配置方式是支持扩展的。
但是这里之所以设计为数组形式,其实是有另有考虑的。可以用枚举 enum 来定义状态转换关系,然后用 values() 方法就能轻松获取到全部的转换关系了,而且是数组形式。—— 利用了 Java 语言的特性,如果是非 Java 语言可以考虑类似方式。
下面给出这个类的详细代码:
import java.util.*;import java.util.stream.Collectors;
/** * 轻量级的状态机框架,通过集成此类可快速实现一个简易的有限状态机。 * <br> * 线程安全 * * @author xieyipei * @date 2021/8/13 18:13 */public class SimpleFSMFrame implements StateMachine { /** * 存放有当前状态机中的状态与事件转换关系的box */ private final TransitionBox transitionBox; /** * 状态机当前状态 */ private State state;
/** * 初始化一个状态机 * * @param initialState 初始状态 * @param transitions 状态与事件之间的转换关系 */ public SimpleFSMFrame(State initialState, Transition[] transitions) { state = initialState; this.transitionBox = new TransitionBox(transitions); }

@Override synchronized public State onEvent(Event event) throws UnsupportedOperationException { state = execute(state, event); return state; }
@Override public State getState() { return state; }
@Override public List < Event > acceptableEvents() { return acceptableEvents(state); }
@Override public boolean canPerformEvent(Event event) { return canPerformEvent(state, event); }
/** * 在指定状态下执行某个事件,执行成功返回变更后的新状态 * * @param currentState 状态 * @param event 事件 * @return 变更后的新状态 * @throws UnsupportedOperationException 如果当前状态不支持该操作则抛出此异常 */ private State execute(State currentState, Event event) throws UnsupportedOperationException { List < Transition > transitions = transitionBox.getTransitionBySource(currentState);
return transitions .stream() .filter(transition - > transition.getEvent().equals(event)) .findAny() .orElseThrow(() - > new UnsupportedOperationException("Event:" + event.name() + " can not be performed on State:" + currentState.name())) .getTarget(); }
/** * 当前状态可执行的事件清单 * * @param state 状态 * @return */ private List < Event > acceptableEvents(State state) { List < Transition > transitions = transitionBox.getTransitionBySource(state); return transitions .stream() .map(transition - > transition.getEvent()) .collect(Collectors.toList()); }
/** * 当前状态是否可以执行指定的某个事件(仅给出是否允许执行的判断结论,不会真的执行) * * @param state 状态 * @param event 事件 * @return */ private boolean canPerformEvent(State state, Event event) { List < Transition > transitions = transitionBox.getTransitionBySource(state); return transitions .stream() .anyMatch(transition - > transition.getEvent().equals(event)); }
/** * 检验状态与事件转换关系是否合法 * * @param transitions * @throws IllegalArgumentException 如果校验不通过则抛出此异常 */ private void verifyTransition(Transition[] transitions) throws IllegalArgumentException { //检查源状态+事件不能重复 Set < String > set = new HashSet < > (); for (Transition transition: transitions) { String key = transition.getSource().name() + "" + transition.getEvent().name(); boolean flag = set.add(key); if (!flag) throw new IllegalArgumentException(String.format("reduplicate transition source=%s event=%s", transition.getSource().name(), transition.getEvent().name())); } }
/** * 存放整理后的状态与事件转换关系,并提供相应的访问方法 */ private class TransitionBox {
private Map < State, List < Transition >> sourceMap = new HashMap < > (); private Map < State, List < Transition >> targetMap = new HashMap < > (); private Map < Event, List < Transition >> eventMap = new HashMap < > ();
/** * 根据状态与事件的转换关系初始化一个box * * @param transitions 状态与事件的转换关系 */ public TransitionBox(Transition[] transitions) { //校验转换关系是否存在异常情况,如果存在则抛出异常 verifyTransition(transitions);
for (Transition transition: transitions) { //sourceMap List < Transition > sourceList = sourceMap.get(transition.getSource()); if (sourceList == null) { sourceList = new ArrayList < > (); sourceMap.put(transition.getSource(), sourceList); } sourceList.add(transition);
//targetMap List < Transition > targetList = targetMap.get(transition.getTarget()); if (targetList == null) { targetList = new ArrayList < > (); targetMap.put(transition.getTarget(), targetList); } targetList.add(transition);
//eventMap List < Transition > eventList = eventMap.get(transition.getEvent()); if (eventList == null) { eventList = new ArrayList < > (); eventMap.put(transition.getEvent(), eventList); } eventList.add(transition); } }
/** * 获取指定源状态的所有转换关系 * * @param source 源状态 * @return */ public List < Transition > getTransitionBySource(State source) { List < Transition > list = sourceMap.get(source); return list != null ? list : new ArrayList < > (); }
/** * 获取指定目标状态的所有转换关系 * * @param target 目标状态 * @return */ public List < Transition > getTransitionByTarget(State target) { List < Transition > list = targetMap.get(target); return list != null ? list : new ArrayList < > (); }
/** * 获取与指定事件相关的所有转换关系 * * @param event 事件 * @return */ public List < Transition > getTransitionByEvent(Event event) { List < Transition > list = eventMap.get(event); return list != null ? list : new ArrayList < > (); }
}}

整体思路是,将构造方法传入的所有状态转换关系放到定义为私有内部类 TransitionBox 这样一个容器中保管,避免对外暴露内部实现细节,在 TransitionBox 中会对关系配置进行校验,以及整理为 3 个不同的 map,并通过这些 map 实现状态机的行为判断。

4、使用案例
4.1 定义状态机
对于使用者来说,只需 3 步即可完成一个全新的状态机实现:
  1. 实现 State 和 Event 接口,定义自己的状态和事件;
  2. 定义枚举类并实现 Transition 接口,状态转换关系通过枚举值形式配置出来;
  3. 继承 SimpleFSMFrame 类,调用上一步枚举类的 values() 方法并传入构造方法

下面给出一个项目中实际使用的案例:
/** * 适用于海外应收账单状态(相比跨境应收增加了3个新状态) * * @author xieyipei * @date 2021/9/23 14:57 */public class ARBillStateMachine extends SimpleFSMFrame {
/** * 初始化一个状态机 * * @param initialState 初始状态 */ public ARBillStateMachine(State initialState) { //调用自定义的状态转换关系枚举的values()方法获取到全部转换关系,然后传给父类的构造方法 super(initialState, ARTransition.values()); }

@Getter private enum ARTransition implements Transition { //状态转换关系通过枚举值形式配置出来。形式为:sourceState+event+targetState T111(BillState.INIT, BillEvent.CONFIRM, BillState.MERCHANT_CLIENT_PENDING), T121(BillState.INIT, BillEvent.DISCARD, BillState.DISCARDED),
T211(BillState.MERCHANT_CLIENT_PENDING, BillEvent.CLIENT_REJECT, BillState.OPERATING_PENDING), T212(BillState.MERCHANT_CLIENT_PENDING, BillEvent.MERCHANT_CLIENT_REJECT, BillState.OPERATING_PENDING), T213(BillState.MERCHANT_CLIENT_PENDING, BillEvent.ON_BEHALF_OF_CLIENT_REJECT, BillState.OPERATING_PENDING), T214(BillState.MERCHANT_CLIENT_PENDING, BillEvent.ON_BEHALF_OF_MERCHANT_CLIENT_REJECT, BillState.OPERATING_PENDING), T221(BillState.MERCHANT_CLIENT_PENDING, BillEvent.CLIENT_ACCEPT, BillState.MERCHANT_CLIENT_CONFIRMED), T222(BillState.MERCHANT_CLIENT_PENDING, BillEvent.MERCHANT_CLIENT_ACCEPT, BillState.MERCHANT_CLIENT_CONFIRMED), T223(BillState.MERCHANT_CLIENT_PENDING, BillEvent.ON_BEHALF_OF_CLIENT_ACCEPT, BillState.MERCHANT_CLIENT_CONFIRMED), T224(BillState.MERCHANT_CLIENT_PENDING, BillEvent.ON_BEHALF_OF_MERCHANT_CLIENT_ACCEPT, BillState.MERCHANT_CLIENT_CONFIRMED),
T311(BillState.OPERATING_PENDING, BillEvent.DISCARD, BillState.DISCARDED), T321(BillState.OPERATING_PENDING, BillEvent.CONFIRM, BillState.MERCHANT_CLIENT_PENDING),

T411(BillState.MERCHANT_CLIENT_CONFIRMED, BillEvent.PUSH_TO_SETTLE, BillState.SETTLEMENT_PENDING), T421(BillState.MERCHANT_CLIENT_CONFIRMED, BillEvent.DISCARD, BillState.DISCARDED),
T511(BillState.SETTLEMENT_PENDING, BillEvent.PARTIAL_PAYMENT_WAS_RECEIVED, BillState.PARTIAL_PAYMENT_WAS_RECEIVED), T521(BillState.SETTLEMENT_PENDING, BillEvent.OPERATING_CANCEL, BillState.FINANCIAL_REJECTED), T522(BillState.SETTLEMENT_PENDING, BillEvent.FINANCIAL_REJECT, BillState.FINANCIAL_REJECTED), T523(BillState.SETTLEMENT_PENDING, BillEvent.REJECT_IN_SETTLEMENT, BillState.FINANCIAL_REJECTED), T531(BillState.SETTLEMENT_PENDING, BillEvent.COMPLETE_SETTLEMENT, BillState.SETTLEMENT_FINISHED), T533(BillState.SETTLEMENT_PENDING, BillEvent.PUSH_TO_SETTLE, BillState.SETTLEMENT_PENDING),
T611(BillState.PARTIAL_PAYMENT_WAS_RECEIVED, BillEvent.FULL_PAYMENT_WAS_RECEIVED, BillState.SETTLEMENT_FINISHED), T612(BillState.PARTIAL_PAYMENT_WAS_RECEIVED, BillEvent.PARTIAL_PAYMENT_WAS_RECEIVED, BillState.PARTIAL_PAYMENT_WAS_RECEIVED), T613(BillState.PARTIAL_PAYMENT_WAS_RECEIVED, BillEvent.COMPLETE_SETTLEMENT, BillState.SETTLEMENT_FINISHED),
T711(BillState.FINANCIAL_REJECTED, BillEvent.DISCARD, BillState.DISCARDED), ;
private final State source; private final State target; private final Event event;
ARTransition(State source, Event event, State target) { this.source = source; this.target = target; this.event = event; } }}

4.2 使用状态机
private boolean canPerformEvent(Bill bill, BillEvent billEvent) {    //根据账单状态初始化状态机    StateMachine stateMachine = new ARBillStateMachine(bill.getBillState());    //通过状态机判断是否允许操作指定的行为    return stateMachine.canPerformEvent(billEvent);}

5、改进空间讨论
分层多级状态如何支持?
例如,账单第一级状态可分为,初始、客户确认中、待结算、完成。其中待结算状态又细分二级状态为:已推送结算、财务审批通过、资金撤单、结算完成。这样,状态之间不再是简单的互不包含,而是存在包含关系,也就是出现了复合状态。
针对这个问题,大家是如何看的,欢迎讨论~


转自:谢益培 / 京东物流

链接:blog.csdn.net/jdcdev_/article/details/133698328



- EOF -

推荐阅读  点击标题可跳转

1、秒杀架构设计的 7 个锦囊

2、高并发下秒杀商品,你必须知道的 9 个细节

3、Java 生鲜电商平台 - API 接口设计之 token、timestamp、sign 具体架构与实现


看完本文有收获?请转发分享给更多人

关注「ImportNew」,提升Java技能

点赞和在看就是最大的支持❤️


相关推荐

  • 时间序列去趋势化和傅里叶变换
  • 免费体验AI绘画,GPT联网、编程助手
  • 对话 “互联网教父” Kevin Kelly:探索《宝贵的人生建议》|问题征集
  • 基于 Dubbo,如何利用APISIX 构建跨网 RPC
  • 对标 FAISS,百度开源自研高性能检索引擎 Puck
  • 可部署手机、适配国产芯……全新升级后的 ChatGLM3 真的有点东西:智谱 AI 选择继续开源!
  • 都是 大前端 技术专家 级别的!!!
  • 一款适合任何团队的问答平台软件,无论是社区论坛、帮助中心还是知识管理平台!
  • GPT-4V初步测试
  • 妙用computed拦截v-model,面试管都夸我细
  • B 站广州研发工作室解散;外媒曝光苹果中国区丑闻;OpenAI 被曝已叫停新大模型项目 | Q资讯
  • AI创业:3个月做20+场景签8家单背后的秘密
  • 前端架构带你封装axios,一次封装终身受益
  • 北京内推 | 百度AIGC团队招聘多模态方向计算机视觉算法工程师/实习生
  • NeurIPS 2023 | 大模型时代自监督预训练的隐性长尾偏见
  • 预训练机器阅读理解模型:对齐生成式预训练与判别式下游场景
  • [开源]企业级在线办公系统,基于实时音视频完成在线视频会议功能
  • 再看大模型之文档智能训练数据生成方法:PublayNet、TableBank、TableGeneration等数据生成方
  • 一起来卷下整洁架构
  • 波士顿动力用ChatGPT训练机器狗;小米推出大模型版澎湃OS系统;智谱推出ChatGLM3新一代大模型丨AIGC大事日报