java中状态机的多种实现
前言
在java后端项目的开发过程中,经常会碰到需要进行状态转换的场景,比如订单的状态、任务的状态等等,通常会使用枚举对状态进行定义,并在需要时更改其枚举值。
但是在一些比较复杂的场景下,例如状态数大于等于5个,或者大状态拥有子状态的场景(如订单【交易中】的状态包含【待发货】【已发货】等子状态),再直接通过修改枚举值的方式已经难以进行维护,且会导致状态变更的代码在项目中散布,不仅在实现上不够优雅,而且在需要添加或者修改状态时牵一发而动全身。因此,我们需要有一种技术手段,来对状态进行统一管理,以及收束变更状态的代码入口。
有限状态机(Finite State Machine, FSM)就是用来干这个事的!
该文是一个系列文章,本章先介绍基本概念及其简单实现
什么是有限状态机
引用参考文档1的说明
A Finite State Machine is a model of computation based on a hypothetical machine made of one or more states. Only one single state of this machine can be active at the same time. It means the machine has to transition from one state to another in to perform different actions.
状态机就是包含多个状态的数学模型,并可以在状态之间进行变换并且触发一些动作。
一个状态机一般包含以下几个元素
State
当前状态Event
触发事件Transition
状态变换,或者说下一个状态(次态)Action
要执行的动作
在一些文章中,会将触发事件和执行动作的名称互换,这里笔者采用开源状态机
Squirrel
的定义
即在一个状态下,某些事件触发后变换成了另一个状态,并执行了一些操作
如何实现一个简单的状态机
这里根据多篇参考文档,总结以下四种实现方式并进行简述,具体的实现代码可以查看参考文档2
1. switch...case
- 保存当前状态
- 状态变更时传入变更事件event, 先通过
if...else
判断是哪个事件,再对当前事件event
进行switch...case
确定要执行的操作 - 进入下一个状态,并执行
action
简单代码如下
1 |
|
该种方法实现是最简单的实现方法,在状态数量较少时,这是一个很高效的实现方案。但是在较复杂的状态下,会导致嵌套大量的 if...else
和 switch...case
语句,维护起来费劲而且实现不够优雅。
2. 状态模式 (State Design Pattern
)
原始定义如下
Allow an object to alter its behavior when its internal state changes.The object will appear to change its class.
采用参考文档4的翻译对其进行解释
外部的调用不用知道其内部如何实现状态和行为变化的
状态模式主要就是对转换规则进行封装,封装状态而暴露行为,状态的改变看起来就是行为发生改变,总结起来,状态模式干了这么几件事
需要定义状态抽象类
AbstractState
,其中需要包含上下文Context
, 以及所有的抽象事件(event
)对象的方法1
2
3
4
5
6
7
8
9
10
11
12
13
14public abstract class AbstractState {
// 上下文信息
protected Context context;
public void setContext(Context context) {
this.context = context;
}
// 事件操作放在具体的状态定义内
abstract void event1();
abstract void event2();
// ......
}具体的状态需要继承并实现抽象状态
1
2
3
4
5
6
7
8
9public class State1 extends AbstractState {
@Override
void event1() {
// set next state
// do something else
}
// ......
}上下文
Context
中需要包含所有所需信息,包括所有的状态实例,并指定一个属性指示当前是哪个状态实例1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20public class Context {
// 当前状态实例
protected AbstractState currState;
protected static final State1 state1 = new State1();
protected static final State2 state2 = new State2();
// ......
public Context(AbstractState state) {
setState(state);
}
// 具体的事件调用当前状态进行执行
public void event1(){
this.currState.event1();
}
// ......
}
由上可见,状态模式将与行为与状态相绑定,避免了直接去写大量的 if...else
和 switch...case
编写时可以方便地增加新的状态,并且只需要改变对象的状态就可以改变对象的行为,
3. Java枚举实现
利用枚举来实现,其主要思想在于可以将状态 State
和 事件 Event
都定义成一个枚举类型,并利用枚举也可以定义抽象方法的特性,使得其定义和动作可以写在一起
简单来说,其定义如下
定义状态枚举
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23public enum StateEnum {
State1 {
@Override
public void doEvent1(StateMachine stateMachine) {
// set next state
// do something else
}
@Override
public void doEvent2(StateMachine stateMachine) {
// set next state
// do something else
}
// ......
},
// .......
;
public abstract void doEvent1(StateMachine stateMachine);
public abstract void doEvent2(StateMachine stateMachine);
// ......
}定义事件枚举
1
2
3
4
5
6
7
8
9
10
11
12
13public enum EventEnum {
Event1 {
@Override
public void trigger(StateMachine stateMachine, StateEnum state) {
return state.doEvent1(stateMachine);
}
},
// ......
;
public abstract void trigger(StateMachine stateMachine, StateEnum state);
}组装状态机
1
2
3
4
5
6
7
8
9
10
11
12public class StateMachine {
private StateEnum state;
public StateMachine(StateEnum state) {
this.state = state;
}
public void execute(EventEnum event) {
event.trigger(this, this.state);
}
该实现根据参考文档2做抽象,具体例子可以查看其文章
使用枚举来定义可以使得我们的代码更加清晰
4. 保存转换映射
一言以蔽之,就是将状态机每一个状态转换所需要的几个要素都保存下来,在触发对应的事件时,遍历所有的状态映射记录,并根据状态以及上下文信息找到对应的记录,并执行转换得到次态
状态机的转换记录定义如下
1 |
|
这种方式实际上就是很多开源状态机的基础实现方式,不过在其实现中,会通过自定义注解的形式来使得代码更加简洁明了和优雅,并进行其他一些优化来优化性能,如记录索引以及懒加载等。
5.srping statemachine
当满足以下条件时,使用状态机是一个不错的选择:
- 你可以将应用程序或其部分结构表示为状态。
- 你想将复杂的逻辑分解为较小的可管理的任务。
- 应用程序已经存在并发问题,例如某些事件以异步方式发生。
当你满足以下条件时,你已经在尝试实现一个状态机:
- 使用布尔标志或枚举来模拟情况。
- 某些变量仅在应用程序生命周期的某个部分具有意义。
- 通过 if-else 结构(或者更糟糕的是,多个这样的结构)进行循环,检查特定标志或枚举是否已设置,然后根据标志和枚举的组合存在与否进一步确定要执行的操作。
https://docs.spring.io/spring-statemachine/docs/3.2.1/reference/#quick-example
1 |
|