面向状态机编程:复杂业务逻辑应对之道
创始人
2025-05-31 09:42:07
0

一、背景

在研发项目中,经常能遇到复杂的状态流转类的业务场景,比如游戏编程中 NPC 的跳跃、前进、转向等状态变化,电商领域订单的状态变化等。这类情况其实可以有一种优雅的实现方法:状态机。如下图,是操作系统对进程调度的状态机:

图 操作系统进程调度状态机

二、实现方式

面对以上场景,通常情况下的实现有以下几种,下面分别比较它们适用范围和优缺点:

2.1 if/else

优点:实现简单、直观。

缺点:状态多了代码可读性,业务与状态判断深度耦合,维护和扩展困难。

2.2 状态模式

状态模式类图及使用方式如下:

public class StatePatternDemo {public static void main(String[] args) {Context context = new Context();StartState startState = new StartState();startState.doAction(context);System.out.println(context.getState().toString());StopState stopState = new StopState();stopState.doAction(context);System.out.println(context.getState().toString());}
}

优点:状态单独实现,可读性比 if/else 好。

缺点:扩展状态需增加状态类,状态多了会出现很多状态类;并没有完全实现状态与业务解耦,不利于维护和了解整个系统状态全貌。

2.3 有限状态机

优点:严谨的数学模型,状态转移和业务逻辑基于事件完全解耦,能看到整个系统状态全貌便于维护和扩展。

缺点:需引入状态机实现方式,具备一定理解成本。

三、有限状态机

3.1 定义

有限状态机(英语:finite-state machine,缩写:FSM)又称有限状态自动机(英语:finite-state automaton,缩写:FSA),简称状态机,是表示有限个状态以及在这些状态之间的转移和动作等行为的数学计算模型。

3.2 关键概念

状态 State:一般在状态转移图中用圆圈表示。

事件 Event:表示从一种状态迁移到另一种状态的触发机制。对应状态转换图中的箭头部分。

动作 Action: 表示状态转换后,执行的动作,但不是必须的,也可以状态转换后不执行任何动作。

转移 Transition:表示状态转换,从原始状态迁移到目的状态的一个过程。

条件 Guard:表示发生状态转移需满足的条件。

3.3 技术选型

在 Java 项目中,比较常用的有 Spring Statemachine 和 Squirrel-foundation。

综上,在下面的项目中,由于团队使用 SpringBoot 作为开发框架,并且项目不涉及高并发场景,故选择 Spring Statemachine。

四、项目实战

在现实项目中,碰到多种状态转换的复杂业务流程,可以通过以下几个步骤进行分级,逐步将产品需求清晰的实现出来:

4.1 需求背景

零售采销在维护 SKU 物流属性(长、宽、高和重量)时,会因为和物流仓库侧实际属性存在不一致的情况,导致带来物流成本的偏差。为解决这个问题,需设计一个系统供采销通过操作 SKU 的方式,将属性下发给物流侧审核,得到最终的准确物流属性。在对 SKU 进行审核操作的过程中,分别存在未操作、任务下发中、下发失败、已审核、自行联系供应商和挂起 6 种状态(状态转换详见 4.2),考虑到这些状态转换的条件分布在不同的场景下,处于对可维护性和扩展性的考虑,采用状态机实现该需求。

4.2 状态转换图

通过梳理状态转换关系,画出状态转换图如下:

SKU 属性审核状态转换图

4.3 配置状态机

4.3.1 定义状态枚举

public enum SkuStateEnum {/*** 未操作*/INIT(0, "未操作"),/*** 任务下发中*/TASK_DELIVERY(1, "任务下发中"),/*** 下发失败*/DELIVERY_FAIL(2, "下发失败"),/*** 复核中*/RECHECKING(3, "复核中"),/*** 已复核*/RECHECKED(4, "已复核"),/*** 自行联系供应商*/CONCAT_SUPPLIER(5, "自行联系供应商"),/*** 挂起*/SUSPEND(6, "挂起");/*** 状态代码*/private Integer state;/*** 描述信息*/private String desc;SkuStateEnum(Integer state, String desc) {this.state = state;this.desc = desc;}public static SkuStateEnum getByState(Integer state) {for (SkuStateEnum skuStateEnum : SkuStateEnum.values()) {if (skuStateEnum.getState().equals(state)) {return skuStateEnum;}}return null;}public Integer getState() {return state;}public String getDesc() {return desc;}
}

4.3.2 定义事件枚举

public enum SkuAttrEventEnum {/*** 调用OMC属性采集接口成功*/INVOKE_OMC_ATTR_COLLECT_API_SUCCESS,/*** 调用OMC属性采集接口失败*/INVOKE_OMC_ATTR_COLLECT_API_FAIL,/*** 调用OMC下发查询接口并已经生成采集单*/INVOKE_OMC_SKU_DELIVERY_API_GATHER_FINISH,/*** 调用OMC下发查询接口失败*/INVOKE_OMC_SKU_DELIVERY_API_FAIL,/*** OMC的MQ返回SKU属性已变更*/MQ_OMC_SKU_ATTR_CHANGED,/*** 调用商品中台jsf接口,返回SKU属性已变更*/INVOKE_SKU_ATTR_API_CHANGED,/*** 京东有库存*/HAS_JD_STOCK,/*** 京东无库存,VMI有库存*/NO_JD_STOCK_HAS_VMI_STOCK,/*** 京东和VMI均无库存*/NO_JD_STOCK_NO_VMI_STOCK,/*** 上传并复核*/UPLOAD_AND_RECHECK;
}

4.3.3 配置状态机

@Configuration
@EnableStateMachineFactory
@Slf4j
public class SkuAttrStateMachineConfig extends StateMachineConfigurerAdapter {/*** 配置状态** @param states* @throws Exception*/@Overridepublic void configure(StateMachineStateConfigurer states) throws Exception {states.withStates().initial(SkuStateEnum.INIT).states(EnumSet.allOf(SkuStateEnum.class));}@Overridepublic void configure(StateMachineConfigurationConfigurer config) throws Exception {config.withConfiguration().listener(listener()).autoStartup(false);}/*** 配置状态转换和事件的关系** @param transitions* @throws Exception*/@Overridepublic void configure(StateMachineTransitionConfigurer transitions)throws Exception {transitions.withExternal().source(SkuStateEnum.INIT).target(SkuStateEnum.TASK_DELIVERY).event(SkuAttrEventEnum.INVOKE_OMC_ATTR_COLLECT_API_SUCCESS).action(ctx -> {log.info("[调用OMC属性采集接口成功],状态变更:{} -> {}.", SkuStateEnum.INIT.getDesc(),SkuStateEnum.TASK_DELIVERY.getDesc());}).and().withExternal().source(SkuStateEnum.INIT).target(SkuStateEnum.DELIVERY_FAIL).event(SkuAttrEventEnum.INVOKE_OMC_ATTR_COLLECT_API_FAIL).action(ctx -> {log.info("[调用OMC属性采集接口失败],状态变更:{} -> {}.", SkuStateEnum.INIT.getDesc(),SkuStateEnum.DELIVERY_FAIL.getDesc());}).and().withExternal().source(SkuStateEnum.INIT).target(SkuStateEnum.CONCAT_SUPPLIER).event(SkuAttrEventEnum.NO_JD_STOCK_HAS_VMI_STOCK).action(ctx -> {log.info("[京东无库存,VMI有库存],状态变更:{} -> {}.", SkuStateEnum.INIT.getDesc(),SkuStateEnum.CONCAT_SUPPLIER.getDesc());}).and().withExternal().source(SkuStateEnum.INIT).target(SkuStateEnum.SUSPEND).event(SkuAttrEventEnum.NO_JD_STOCK_NO_VMI_STOCK).action(ctx -> {log.info("[京东和VMI均无库存],状态变更:{} -> {}.", SkuStateEnum.INIT.getDesc(),SkuStateEnum.SUSPEND.getDesc());}).and().withExternal().source(SkuStateEnum.SUSPEND).target(SkuStateEnum.TASK_DELIVERY).event(SkuAttrEventEnum.HAS_JD_STOCK).action(ctx -> {log.info("[京东有库存],状态变更:{} -> {}.", SkuStateEnum.SUSPEND.getDesc(),SkuStateEnum.TASK_DELIVERY.getDesc());}).and().withExternal().source(SkuStateEnum.SUSPEND).target(SkuStateEnum.RECHECKING).event(SkuAttrEventEnum.INVOKE_OMC_ATTR_COLLECT_API_SUCCESS).action(ctx -> {log.info("[调用OMC属性采集接口成功],状态变更:{} -> {}.", SkuStateEnum.SUSPEND.getDesc(),SkuStateEnum.RECHECKING.getDesc());}).and().withExternal().source(SkuStateEnum.SUSPEND).target(SkuStateEnum.CONCAT_SUPPLIER).event(SkuAttrEventEnum.NO_JD_STOCK_HAS_VMI_STOCK).action(ctx -> {log.info("[京东无库存,VMI有库存]:{} -> {}.", SkuStateEnum.SUSPEND.getDesc(),SkuStateEnum.CONCAT_SUPPLIER.getDesc());}).and().withExternal().source(SkuStateEnum.TASK_DELIVERY).target(SkuStateEnum.RECHECKING).event(SkuAttrEventEnum.INVOKE_OMC_SKU_DELIVERY_API_GATHER_FINISH).action(ctx -> {log.info("[调用OMC下发查询接口并已经生成采集单]:{} -> {}.", SkuStateEnum.TASK_DELIVERY.getDesc(),SkuStateEnum.RECHECKING.getDesc());}).and().withExternal().source(SkuStateEnum.TASK_DELIVERY).target(SkuStateEnum.RECHECKED).event(SkuAttrEventEnum.MQ_OMC_SKU_ATTR_CHANGED).action(ctx -> {log.info("[OMC的MQ返回SKU属性已变更]:{} -> {}.", SkuStateEnum.TASK_DELIVERY.getDesc(),SkuStateEnum.RECHECKED.getDesc());}).and().withExternal().source(SkuStateEnum.DELIVERY_FAIL).target(SkuStateEnum.TASK_DELIVERY).event(SkuAttrEventEnum.INVOKE_OMC_ATTR_COLLECT_API_SUCCESS).action(ctx -> {log.info("[调用OMC属性采集接口成功]:{} -> {}.", SkuStateEnum.DELIVERY_FAIL.getDesc(),SkuStateEnum.TASK_DELIVERY.getDesc());}).and().withExternal().source(SkuStateEnum.CONCAT_SUPPLIER).target(SkuStateEnum.RECHECKED).event(SkuAttrEventEnum.INVOKE_SKU_ATTR_API_CHANGED).action(ctx -> {log.info("[调用商品中台jsf接口,返回SKU属性已变更]:{} -> {}.", SkuStateEnum.CONCAT_SUPPLIER.getDesc(),SkuStateEnum.RECHECKED.getDesc());});}/*** 全局监听器** @return*/private StateMachineListener listener() {return new StateMachineListenerAdapter() {@Overridepublic void transition(Transition transition) {//当状态的转移在configure方法配置中时,会走到该方法。log.info("[{}]状态变更:{} -> {}", transition.getKind().name(),transition.getSource() == null ? "NULL" : ofNullableState(transition.getSource().getId()),transition.getTarget() == null ? "NULL" : ofNullableState(transition.getTarget().getId()));}@Overridepublic void eventNotAccepted(Message event) {//当发生的状态转移不在configure方法配置中时,会走到该方法,此处打印error日志,方便排查状态转移问题log.error("事件未收到: {}", event);}private Object ofNullableState(SkuStateEnum s) {return Optional.ofNullable(s).map(SkuStateEnum::getDesc).orElse(null);}};}
}

4.4 业务逻辑处理

4.4.1 构建状态机

对每个 sku 的操作,通过状态机工厂 stateMachineFactory.getStateMachine

//注入状态机工厂实例
@Autowired
private StateMachineFactory stateMachineFactory;
//构建状态机
public StateMachine buildStateMachine(String skuId) throws BusinessException {if (StringUtils.isEmpty(skuId)) {return null;}StateMachine stateMachine = null;try {//从DB中获取当前skuId对应的状态LambdaQueryWrapper query = Wrappers.lambdaQuery(SkuAttrRecheckState.class);query.eq(SkuAttrRecheckState::getSkuId, skuId);SkuAttrRecheckState skuAttrRecheckState = this.baseMapper.selectOne(query);SkuStateEnum skuStateEnum = SkuStateEnum.getByState(skuAttrRecheckState == null ? SkuStateEnum.INIT.getState() : skuAttrRecheckState.getState());//从状态机工厂获取一个状态机stateMachine = stateMachineFactory.getStateMachine(skuId);stateMachine.stop();//配置状态机参数stateMachine.getStateMachineAccessor().doWithAllRegions(sma -> {//配置状态机拦截器,当状态发生转移时,会走到该拦截器中sma.addStateMachineInterceptor(new StateMachineInterceptorAdapter() {@Overridepublic void preStateChange(State state,Message message,Transition transition,StateMachine stateMachine,StateMachine rootStateMachine) {//获取状态转移时,对应的SKU详细信息SkuAttrRecheckState result = JSON.parseObject(String.class.cast(message.getHeaders().get(JSON_STR)), SkuAttrRecheckState.class);//更新状态机转移后的状态(来自于4.3.3中的配置)result.setState(state.getId().getState());//将状态机转移后的状态写入DBLambdaQueryWrapper query = Wrappers.lambdaQuery(SkuAttrRecheckState.class);query.eq(SkuAttrRecheckState::getSkuId, result.getSkuId());if (baseMapper.exists(query)) {UpdateWrapper updateQuery = new UpdateWrapper<>();updateQuery.eq("sku_id",result.getSkuId());log.info("更新状态信息:{}", JSON.toJSONString(result));baseMapper.update(result, updateQuery);} else {log.info("写入状态信息:{}", JSON.toJSONString(result));baseMapper.insert(result);}}});//将状态机的初始状态配置为DB中的skuId对应状态sma.resetStateMachine(new DefaultStateMachineContext(skuStateEnum, null, null, null));});//启动状态机stateMachine.start();} catch (Exception e) {log.error("skuId={},构建状态机失败.", skuId, e);throw new BusinessException("状态机构建失败", e);}return stateMachine;}

4.4.2 封装事件

public synchronized Boolean sendEvent(StateMachine stateMachine,SkuAttrEventEnum skuAttrEventEnum, SkuAttrRecheckState skuAttrRecheckState) throws BusinessException {try {//发送事件,并将需要传递的信息写入headerstateMachine.sendEvent(MessageBuilder.withPayload(skuAttrEventEnum).setHeader(SKU_ID, skuAttrRecheckState.getSkuId()).setHeader(STATE, skuAttrRecheckState.getState()).setHeader(JSON_STR, JSON.toJSONString(skuAttrRecheckState)).build());} catch (Exception e) {log.error("发送事件失败", e);throw new BusinessException("发送事件失败", e);}return true;}

4.4.3 业务逻辑应用

当用户在界面上对“未操作”状态的 SKU 点击审核按钮时,会调用物流 OMC 接口将 SKU 属性下发到物流侧,当下发成功时,状态会转换为“任务下发中”,当调用接口失败,则会将状态转换为"下发失败",核心代码如下:

public Boolean recheck(List skuIds) throws BusinessException {if (CollectionUtils.isEmpty(skuIds)) {log.error("参数错误,sku列表为空");throw new BusinessException("参数错误,sku列表为空");}List skuDetails = skuAttrExceptionDetailMapper.queryBySkuIdList(skuIds);if (CollectionUtils.isEmpty(skuDetails)) {log.error("查询sku异常明细结果集为空,skuIds={}", JSON.toJSONString(skuIds));return false;}for (SkuAttrExceptionDetail detail : skuDetails) {if (detail.getState() != SkuStateEnum.INIT.getState()) {log.info("{}不是未操作状态sku不进行复核", detail.getSkuId());continue;}//构建SKU对应的状态机StateMachine stateMachine = buildStateMachine(detail.getSkuId());SkuAttrRecheckState skuAttrRecheckState = DomainBuilderUtil.buildSkuAttrRecheckState(detail);//判定库存并发送事件adjustAndSendEvents(detail, stateMachine, skuAttrRecheckState);}return true;}public void adjustAndSendEvents(SkuAttrExceptionDetail detail,StateMachine stateMachine,SkuAttrRecheckState skuAttrRecheckState) throws BusinessException {//1、京东有库存,调用物流属性接口,下发SKU属性if (detail.getSpotInventoryQtty() > 0) {invokeOmcSkuAttrCollectApiAndSendEvent(detail, stateMachine, skuAttrRecheckState);return;}//2、京东无库存,有VMI库存if (detail.getSpotInventoryQtty() <= 0 && detail.getVmiInventoryQtty() > 0) {sendEvent(stateMachine, SkuAttrEventEnum.NO_JD_STOCK_HAS_VMI_STOCK, skuAttrRecheckState);return;}//3、京东和VMI均无库存if (detail.getSpotInventoryQtty() <= 0 && detail.getVmiInventoryQtty() <= 0) {sendEvent(stateMachine, SkuAttrEventEnum.NO_JD_STOCK_NO_VMI_STOCK, skuAttrRecheckState);return;}}private void invokeOmcSkuAttrCollectApiAndSendEvent(SkuAttrExceptionDetail detail,StateMachine stateMachine,SkuAttrRecheckState skuAttrRecheckState) throws BusinessException {DistrustAttributeGatherRequest request = RequestUtil.buildOmcAttrCollectRequest(detail, reqSource);try {if (jsfInvokeService.invokeSkuAttrCollectApi(request)) {sendEvent(stateMachine, SkuAttrEventEnum.INVOKE_OMC_ATTR_COLLECT_API_SUCCESS,skuAttrRecheckState);} else {sendEvent(stateMachine, SkuAttrEventEnum.INVOKE_OMC_ATTR_COLLECT_API_FAIL, skuAttrRecheckState);}} catch (Exception e) {log.error("调用物流Sku属性采集接口错误,request={}", JSON.toJSONString(request), e);sendEvent(stateMachine, SkuAttrEventEnum.INVOKE_OMC_ATTR_COLLECT_API_FAIL, skuAttrRecheckState);}}

五、总结

本文通过介绍有限状态机,并结合具体项目,通过状态机的应用将状态和业务逻辑解耦,便于简化复杂业务逻辑,降低理解成本。另外,状态机的应用场景对于那种复杂的流程也同样适合,可以在实际项目中根据状态机核心要素梳理出隐性的状态转换关系,从而将一个复杂的流程问题转换为状态机的模式问题,再利用状态机模式来实现,可以有助于我们优雅的解决更广泛的复杂业务问题。

相关内容

热门资讯

u盘安装win10系统2 好不容易完成系统安装u盘的制作,心想接下来应该会比较顺利吧,结果被第一个...
描写家禽的词语有哪些 描写家禽的词语有哪些  二字词:  划行  油亮  雪白  银灰  飞翔  奋飞  戏水  俯冲  ...
“怏怏不悦”的意思 “怏怏不悦”的意思 成语拼音: [yàng yàng bù yuè] ...
“片言只字”的意思 “片言只字”的意思 成语拼音: [piàn yán zhǐ zì] ...
“天怒民怨”的意思 “天怒民怨”的意思 成语拼音: [tiān nù mín yuàn] ...
Anaconda的一些配置修改... 文章目录一. 修改虚拟环境安装位置二. 添加虚拟环境三. 修改源 一. 修改虚拟环境安装位置 修改...
基于Java+SpringBo...  博主介绍:专注于Java技术领域和毕业项目实战 🍅文末获取源码联系&...
使用Midjourney与Ch... Midjourney 和 ChatGPT 都是目前比较先进的自然语言处理技术,Midj...
形容非常伤心的成语 形容非常伤心的成语  人生是伤心与快乐的综合体,形容一个人非常伤心的成语有哪些?小编给大家提供形容非...
“集腋成裘”的意思 “集腋成裘”的意思 成语拼音: [jí yè chéng qiú] ...
“一拍即合”的意思 “一拍即合”的意思 成语拼音: [yī pāi jí hé] ...
马的四字成语 马的四字成语大全  众所周知,成语是中华文化的瑰宝——之一。关于马的四字成语你知道多少?汉语中有关马...
Python作用域详述 作用域是指变量的生效范围,例如本地变量、全局变量描述的就是不同的生效范围。 pytho...
java使用tess4j进行图... java使用tess4j进行图片文字识别一、简介二、使用过程1.maven依赖引入pom.xml2....
“躬耕乐道”的意思 “躬耕乐道”的意思 成语拼音: [gōng gēng lè dào] ...
“铭刻心骨”的意思 “铭刻心骨”的意思 成语拼音: [míng kè xīn gǔ] ...
“珍藏密敛”的意思 “珍藏密敛”的意思 成语拼音: [zhēn cáng mì liǎn] ...
“上陵下替”的意思 “上陵下替”的意思 成语拼音: [shàng líng xià tì] ...
黑马程序员——前端HTML5+... 黑马程序员——前端HTML5+CSS3(女神版)——day04—...
Android 解包paylo... 解析payload.bin获取.img文件 payload.bin payload.bin是Andr...