状态机是一个非常有用的设计模式,可以大大简化使用应用代码对复杂状态管理、校验、流转时的复杂性。用好状态机,可以让代码更加精致,也更简洁。
01 为什么需要状态机?
假设我们正在开发一个电子商务平台的订单处理系统。订单通常会经历多个状态,例如:
- 新订单
- 已支付
- 已发货
- 已交付
- 已取消
在这个场景中,订单状态的变化必须符合业务规则,比如:
- 新订单可以被取消或支付。
- 已支付的订单可以被发货或取消。
- 已发货的订单可以被交付。
如果用Java 代码来写的话,每一次状态变化都需要校验一次。如果不使用状态机,可能会通过多个if-else或switch-case语句来管理这些状态转换。这种方式不仅代码繁琐,而且随着业务逻辑的复杂化,会导致代码难以维护。
例如:
public class Order {
private OrderState state;
public Order() {
this.state = OrderState.NEW;
}
public void pay() {
if (state == OrderState.NEW) {
state = OrderState.PAID;
} else {
throw new IllegalStateException("Order cannot be paid in state: " + state);
}
}
public void ship() {
if (state == OrderState.PAID) {
state = OrderState.SHIPPED;
} else {
throw new IllegalStateException("Order cannot be shipped in state: " + state);
}
}
// 下面同理不浪费篇幅了
}
我们可以把状态控制的部分抽出来,然后提前约定一个流转规则就行了,实现这个流转规则的代码我们就成为状态机。我们可以用 UML 把上面的状态流转,绘制成状态图(使用 PlantUML 绘制)。
状态机的本质是把流程控制逻辑和普通业务逻辑做分离。如果流程控制逻辑没有那么明显,也就没有那么必要使用状态机,引入不必要的复杂性,同理,如果希望流程更加灵活,可以通过非编程的方式动态组织这些流程,那么可能流程引擎更适合,而不是状态机。
状态机为什么这么小众,还是因为使用的场景过于狭窄。
02 实现一个状态机
其实实现一个状态机很简单,可以像下面这样,一个类就能完成。
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
class StateMachine<S, E> {
private S currentState;
private final Map<S, Map<E, S>> transitions = new HashMap<>();
private final Map<S, Consumer<E>> entryActions = new HashMap<>();
private final Map<S, Consumer<E>> exitActions = new HashMap<>();
public StateMachine(S initialState) {
this.currentState = initialState;
}
public void addTransition(S fromState, S toState, E event) {
transitions.computeIfAbsent(fromState, k -> new HashMap<>()).put(event, toState);
}
public void setEntryAction(S state, Consumer<E> action) {
entryActions.put(state, action);
}
public void setExitAction(S state, Consumer<E> action) {
exitActions.put(state, action);
}
public boolean handleEvent(E event) {
Map<E, S> stateTransitions = transitions.get(currentState);
if (stateTransitions != null) {
S nextState = stateTransitions.get(event);
if (nextState != null) {
// Perform exit action for the current state
if (exitActions.containsKey(currentState)) {
exitActions.get(currentState).accept(event);
}
// Transition to the next state
currentState = nextState;
// Perform entry action for the new state
if (entryActions.containsKey(currentState)) {
entryActions.get(currentState).accept(event);
}
return true;
}
}
return false;
}
public S getCurrentState() {
return currentState;
}
public Set<E> getAvailableEvents() {
Map<E, S> stateTransitions = transitions.get(currentState);
return stateTransitions != null ? stateTransitions.keySet() : Set.of();
}
}
这里面有几个概念:
- 状态转换:或者叫状态流,需要定义那两个状态之间的转换是被允许的。
- 事件:能够触发的事件信息。
- 事件处理器:定义进入状态和退出状态时的钩子函数。
下面是一个使用的例子:
public class SimpleStateMachineExample {
enum OrderState {
NEW, PROCESSING, SHIPPED, DELIVERED, CANCELLED
}
enum OrderEvent {
PROCESS_ORDER, SHIP_ORDER, DELIVER_ORDER, CANCEL_ORDER, RETURN_ORDER
}
public static void main(String[] args) {
StateMachine<OrderState, OrderEvent> orderStateMachine = new StateMachine<>(OrderState.NEW);
// 定义状态转换
orderStateMachine.addTransition(OrderState.NEW, OrderState.PROCESSING, OrderEvent.PROCESS_ORDER);
orderStateMachine.addTransition(OrderState.PROCESSING, OrderState.SHIPPED, OrderEvent.SHIP_ORDER);
orderStateMachine.addTransition(OrderState.SHIPPED, OrderState.DELIVERED, OrderEvent.DELIVER_ORDER);
orderStateMachine.addTransition(OrderState.NEW, OrderState.CANCELLED, OrderEvent.CANCEL_ORDER);
orderStateMachine.addTransition(OrderState.PROCESSING, OrderState.CANCELLED, OrderEvent.CANCEL_ORDER);
orderStateMachine.addTransition(OrderState.SHIPPED, OrderState.CANCELLED, OrderEvent.RETURN_ORDER);
// 定义状态处理器
orderStateMachine.setEntryAction(OrderState.PROCESSING, event -> System.out.println("Order is being processed."));
orderStateMachine.setExitAction(OrderState.PROCESSING, event -> System.out.println("Exiting processing state."));
// 模拟触发一个事件
simulateEvent(orderStateMachine, OrderEvent.PROCESS_ORDER);
simulateEvent(orderStateMachine, OrderEvent.SHIP_ORDER);
simulateEvent(orderStateMachine, OrderEvent.DELIVER_ORDER);
System.out.println("Final State: " + orderStateMachine.getCurrentState());
}
private static void simulateEvent(StateMachine<OrderState, OrderEvent> stateMachine, OrderEvent event) {
System.out.println("Current State: " + stateMachine.getCurrentState());
System.out.println("Handling Event: " + event);
if (!stateMachine.handleEvent(event)) {
System.out.println("Event " + event + " cannot be handled from state " + stateMachine.getCurrentState());
}
System.out.println("New State: " + stateMachine.getCurrentState());
System.out.println("------------------------");
}
}
也可以升级一下状态机,把定义状态转换、定义状态处理器这些逻辑定义成 DSL,放到配置文件中,也可以可视化状态转换的过程、审计日志。
这就是把状态逻辑和业务逻辑分离的价值。
上面这个状态机太简陋了,但是大多数场景下也够用,想实现我们提到的更多功能,用于调试、管理状态变化,可以用一些更加专业的库来实现。
03 使用 Spring Statemachine 项目
Spring Statemachine 就是一个基于 Spring 生态的状态机库。 那么如果是 Spring 这个框架来做的话,有什么更多特性呢?
支持层级状态
Spring Statemachine 支持层级(嵌套)状态,这意味着一个状态机可以包含另一个状态机。这对于需要处理复杂逻辑的应用程序特别有用,可以将状态分层,减少复杂性。
支持并发状态
前面的状态机非常致命的一个地方就是并发,如果状态机是一个单例类就会出现并发问题,如果为每一次请求创建一个状态机实例可以避免并发问题,但是这样会频繁创建和销毁对象。
如果上锁,系统串行化更不划算。
Spring Statemachine 基于一些并发编程的模式,优化了这里,所以更加高效。
其它特性
这些特性一提大家都明白,就不过多展开了。
- 图形化状态图生成
- 动态添加和移除状态
- 支持条件状态转换,这个是指除了固定的状态转换外,可以带上条件。例如,报销审批单总价小于100就不用审批。
- 异步事件处理
04 Spring Statemachine 使用示例
Spring Statemachine 支持使用 YAML 来定义规则,这样定义状态就非常直观。
statemachine:
states:
- READY
- PROCESSING
- COMPLETED
- ERROR
transitions:
- from: READY
to: PROCESSING
event: START_PROCESS
- from: PROCESSING
to: COMPLETED
event: COMPLETE_PROCESS
- from: PROCESSING
to: ERROR
event: ERROR_OCCURRED
@Configuration
public class MyStateMachineConfig extends StateMachineConfigurerAdapter<String, String> {
@Value("${statemachine.states}")
private List<String> states;
@Value("#{${statemachine.transitions}}")
private List<Map<String, String>> transitions;
@Override
public void configure(StateMachineStateConfigurer<String, String> states) throws Exception {
// 配置初始状态和其他状态
states.withStates()
.initial(this.states.get(0)) // 假设第一个状态为初始状态
.states(new HashSet<>(this.states)); // 添加所有状态
}
@Override
public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception {
for (Map<String, String> transition : this.transitions) {
transitions.withExternal()
.source(transition.get("from"))
.target(transition.get("to"))
.event(transition.get("event"));
}
}
}
其实 Spring Statemachine 本身不直接支持 YAML 配置,但通过结合 Spring Boot 和 YAML 文件,我们可以方便地管理状态机的配置。
根据我们这个系列前面关于如何处理业务规则的文章,把状态纳入业务规则也是一种非常有用的技巧,这样业务人员就能很容易看到当前系统如何实现这些状态了。
05 补充:有限状态机和无限状态机
有限状态机 FSM(finite-state machine) 是一种具有有限个状态的抽象机器,它可以从一个状态转换到另一个状态。FSM 可以通过输入的事件或条件来改变其状态。FSM 通常用作控制系统和计算机科学中的逻辑电路、协议分析等。
由于很多场景下,有限状态机模型和业务非常匹配,所以是一个非常好的设计模式。其实我们写在业务代码中,也在使用这个模型,前面的 Order 类中每个方法就是事件处理函数,只不过我们并没有把业务逻辑和状态管理逻辑分离而已。
那么为什么叫有限呢?因为还有无限状态机(Infinite State Machine)。无限状态机拥有无限个状态,通常适用于状态数量是动态的或不可预见的情况。
有限状态机和无限状态机是两种描述系统行为的方法。为了更好地理解它们之间的区别,可以将它们比作现实生活中的不同场景:有限状态机常用于模型中需要管理固定数量的状态的问题,如业务单据流转、简单的游戏关卡、协议解析等。
无限状态机可以比作一个计数器,像一个水表或电表。每次用水或电(输入),计数器的读数(状态)都会增加,理论上这个数可以无限增长。无限状态机适用于那些状态不是事先固定或可以无限变化的问题,如动态内存管理、实时数据流处理、复杂软件系统的调度。
参考资料
- https://spring.io/projects/spring-statemachine
- 项目终于用上了Spring状态机,非常优雅! https://zhuanlan.zhihu.com/p/632003000
- Spring状态机的介绍与使用 https://www.cnblogs.com/yunjie0930/p/17954492
- How to implement a FSM - Finite State Machine in Java https://stackoverflow.com/questions/13221168/how-to-implement-a-fsm-finite-state-machine-in-java