简介
文章的主要内容是有限状态机的定义,在编程中将有限状态机做适当调整,适应真实的业务场景,最后提供完整的代码示例。
什么是有限状态机
这里关于有限状态机的介绍是出自通义千问的回答
定义
有限状态机(Finite State Machine, FSM)是一种数学模型,用于描述一个系统在不同状态下对外部事件的响应方式。它由一组有限的状态、一个初始状态、一个或多个终态(可选)以及一组状态之间的转移规则构成。
核心概念
- 初始状态(Initial State):系统开始时所处的状态。
- 状态(States):系统可能处于的任何一种情况。
- 终态(Final States):系统可以达到的结束状态,也可能不存在。
- 输入(Input):外部事件或信号,触发状态间的转移。
- 输出(Output):与每个状态关联的动作或结果。
- 状态转移(State Transitions):根据输入从一个状态转移到另一个状态的规则。
使用场景
- 在计算机科学中,用于协议解析、编译器设计、游戏AI、硬件电路设计等领域。
- 在商业逻辑中,如订单流程管理、用户权限控制等场景,有限状态机帮助清晰地定义和管理复杂的多步骤过程。
在编程中对有限状态机的改造
以用有限状态机实现商品状态的管理为例
上面的定义划分的很细、很清晰,有时在实际应用中是不需要完全依照一个理论实施的,毕竟要实事求是,不能生搬硬套。
简化核心组件
我们可以把输出和状态转移合并成一个概念:动作。动作是状态遇到事件应执行的相应操作。
把初始状态和状态合并成“现态”,也就是当前的状态。
根据有限状态机的定义,它适用于系统从一个初始状态,不断接收事件并不断改变状态的场景。在编程中,通常都是将实体从一个状态转变成另一个状态,很少涉及一次转变多个状态的情况。所以这里把核心概念进行简化适应真实的业务场景。
经过简化,有限状态机的核心概念应该如下:
- 现态:表示实体当前的状态
- 终态:实体接收事件后,在执行了相应动作转变到另一个状态
- 事件:触发现态到终态转变的条件(将输入改名为事件)
- 动作:现态接收事件应执行的操作
示例
1.定义商品状态
public enum ProductStatusEnum {
WAITING_AUDIT,
FAILED_OF_AUDIT,
SUCCESS_OF_AUDIT,
ON_SHELVES,
OFF_SHELVES,
SOLD_OUT,
;
}
2.实现业务中的有限状态机
2.1为有限状态机实现必要的抽象
有限状态机入口的抽象
为使用有限状态机提供一个入口。由于有限状态机的组件会比较多,如果不提供一个统一的使用入口,对不了解有限状态机原理的人来说不太友好。
/**
* 商品状态机的执行器
* 接收现态、事件和终态,进行状态转换
* @author manem
*/
@Component
@Data
public class FSMProductExecutor {
@Resource
ApplicationContext ctx;
private AbstractStatusHandler current;
private IEvent event;
private FSMProductExecutor() {
}
public void init(ProductStatusEnum current, ProductStatusEnum target) {
// 设置现态对应的处理器
setCurrent(matchHandler(current));
// 设置转变到终态应触发的事件
setEvent(matchEvent(target));
}
public IAction transitionStatus() {
// 提取现态可接受的事件
for (AbstractStatusHandler.EventMatcher matcher : current.getMatchers()) {
// 判断事件是否匹配
if (matcher.getEvent().equals(this.event)) {
// 调用匹配事件对应的操作
return matcher.getAction();
}
}
throw MightException.ins(
CodeEnum.INTERNAL_ERR.getCode(),
String.format("当前状态<%s>不支持处理该事件<%s>", this.current.getStatus(), this.event)
);
}
private AbstractStatusHandler matchHandler(ProductStatusEnum status) {
AbstractStatusHandler fsmStatus;
switch (status) {
case SUCCESS_OF_AUDIT:
fsmStatus = ctx.getBean(SuccessAuditHandler.class);
break;
// 这里定义不同状态的处理器
...
default:
throw new MightException(CodeEnum.INTERNAL_ERR);
}
return fsmStatus;
}
private EventEnum matchEvent(ProductStatusEnum status) {
EventEnum fsmEvent;
switch (status) {
case SUCCESS_OF_AUDIT:
fsmEvent = EventEnum.AUDIT_PASS;
break;
// 这里定义不同状态对应的事件枚举
...
default:
throw new MightException(CodeEnum.INTERNAL_ERR);
}
return fsmEvent;
}
}
动作的抽象
不同事件会触发不同的动作,通过将动作这一行为进行抽象,有利于将业务逻辑解耦。后续增加事件,只需要添加不同的执行动作即可,不需要变动相关的业务代码。
/**
* 将状态机的动作抽象出来,以应对不同业务的状态实现状态机
* @author manem
*/
public interface IAction<P, R> {
/**
* 状态接收对应事件后可执行的操作
* @param params
* @return
*/
R run(P params);
}
事件的抽象
为了实现有限状态机执行器的复用,这里为事件提供一个统一的接口。
public interface IEvent {
}
现态处理器的抽象
定义现态可接收的事件以及相应的动作
/**
* 定义现态可接受的事件和相应的执行动作
* @author manem
*/
public abstract class AbstractStatusHandler {
protected ProductStatusEnum status;
/**
* 现态在这里定义可接受的事件和动作,触发相应事件可以转变到对应的终态
* 未定义的事件则表示不支持转变到的终态
*/
protected List<EventMatcher> matchers;
public List<EventMatcher> getMatchers() {
return matchers;
}
public ProductStatusEnum getStatus() {
return status;
}
/**
* 定义现态接受的事件和执行的动作
*/
@Data
public static final class EventMatcher {
private IEvent event;
/**
* 接受事件后允许变更到的下一个状态
*/
private IAction action;
public EventMatcher(IEvent event, IAction action) {
this.event = event;
this.action = action;
}
}
}
2.2定义事件
public enum EventEnum implements IEvent {
/**
* 审核相关的事件
*/
AUDIT_PASS, AUDIT_FAIL,
/**
* 上下架相关的事件
*/
PUSH_SHELVE, PULL_SHELVE,
/**
* 售罄
*/
STORE_EMPTY,
/**
* 编辑
*/
EDIT
}
2.3创建使用有限状态机的入口
参考上面的章节"有限状态机入口的抽象"
2.4实现不同状态在不同事件下的切换
商品的状态比较多,这里以其中一个“待审核”状态为例,其他状态可以以此类推实现
定义待审核的状态处理器
import cn.hutool.core.collection.CollUtil;
import javax.annotation.Resource;
@Component
public class WaitingAuditHandler extends AbstractStatusHandler {
// 由于IAction的实现类比较多,这里使用beanName的方式指定应绑定哪个bean
// 默认情况,bean的name是类名首字母小写
@Resource(name = "auditSuccessAction")
IAction auditSuccess;
@Resource(name = "auditFailAction")
IAction auditFail;
@Resource(name = "reAuditAction")
IAction reEdit;
// 定义现态
protected ProductStatusEnum status = ProductStatusEnum.WAITING_AUDIT;
@Override
public List<EventMatcher> getMatchers() {
if (CollUtil.isEmpty(this.matchers)) {
this.matchers = new ArrayList<>();
// 定义待审核状态下可接收的事件和对应的动作
this.matchers.add(new EventMatcher(EventEnum.AUDIT_PASS, auditSuccess));
this.matchers.add(new EventMatcher(EventEnum.AUDIT_FAIL, auditFail));
this.matchers.add(new EventMatcher(EventEnum.EDIT, reEdit));
}
return matchers;
}
}
定义待审核状态相关的动作实现
下面是审核通过动作的实现,其他两个动作的实现可以参考它完善
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.might.components.fsm.IAction;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
// 审核通过应该执行的操作
@Component
public class AuditSuccessAction implements IAction<Long, Boolean> {
@Resource
IProductService productService;
@Resource
IProductSkuService productSkuService;
@Override
public Boolean run(Long params) {
// 审核通过应执行的具体业务逻辑
LambdaUpdateWrapper<ProductPO> productUpdateWrapper = Wrappers.lambdaUpdate(ProductPO.class).eq(ProductPO::getId, params)
.set(ProductPO::getStatus, ProductStatusEnum.SUCCESS_OF_AUDIT);
if (productService.update(productUpdateWrapper)) {
LambdaUpdateWrapper<ProductSkuPO> skuUpdateWrapper = Wrappers.lambdaUpdate(ProductSkuPO.class).eq(ProductSkuPO::getPid, params)
.eq(ProductSkuPO::getStatus, ProductStatusEnum.WAITING_AUDIT)
.set(ProductSkuPO::getStatus, ProductStatusEnum.SUCCESS_OF_AUDIT);
return productSkuService.update(skuUpdateWrapper);
}
return false;
}
}
完整目录结构
补充
由此可见一个完整的有限状态机会有很多组件,整体实现起来也较为复杂。所以当业务的状态数比较少时,就不要用有限状态机了,只会徒增系统的复杂度。