什么是有限状态机
Finite state Machine FSM 简称状态机:状态机由三部分组成,状态(State) 事件(Event) 和动作(Action)组成。
其中事件也被称为转移条件,事件触发状态的转移和动作的执行。不过动作不是必须的,也可能只存在状态转移。
状态模式一般用来实现状态机,不过状态机除了使用状态模式实现还可以使用分支判断法和查表法。
状态机案例
实现马里奥状态转换的状态机,可以使用三种方法实现。
- 分支判断法 使用 if-else 硬编码判断
- 使用查表法判断 根据数据库的二维表配置对应状态和事件来确定执行的动作
- 使用状态模式
分支判断法实现状态机
对于简单地状态机,分支判断法也是可以接受的,但是对于复杂的状态机,容易漏写某个状态转移,并且大量的 if-esle 充斥代码是的代码的可读性和可维护性都很差,容易产生 bug。
//定义状态枚举
public enum State{
SMALL(0),
SUPER(1),
FIRE(2),
CAPE(3);
private int value;
private State (int value){
this.value=value;
}
public int getValue(){
return value;
}
}
//实现状态机
public class MarioStateMachine{
private int score;
private State currentState;
//构造函数,定义初始分数和状态
public MarioStateMachine(){
this.score=0;
this.currentState = State.SMALL;
}
//定义四个事件方法,方法中直接包含事件触发后的动作逻辑
//事件E1 吃到蘑菇
public void obtainMushRoom(){
}
//事件E2 获得斗篷
public void obtainCape(){
}
//事件E3 获得火焰
public void obtainFireFlower(){
}
//事件E4 遇到怪物 这里举例,只填充该方法内容
public void ontainMonster(){
if(currentState=State.SMALL){
this.currentState=State.SMALL;
this.score-=100;
}
if(currentState=State.CAPE){
this.currentState=State.SMALL;
this.score-=200;
}
if(currentState=State.FIRE){
this.currentState=State.SMALL;
this.score-=300;
}
}
public int getScore(){
return score;
}
public State getCurrentState(){
return state;
}
}
查表法实现状态机
分支判断法可以处理简单地状态机,对于复杂的状态机可以使用查表法。并且状态机的状态转移图可以表示状态转移的动作和事件外也可以用状态转移表表示。
E1 吃到蘑菇 | E2 获得斗篷 | E3 获得火焰 | E4 遇到怪物 | |
---|---|---|---|---|
Small | Super +100 | Cape +200 | Fire +300 | —— |
super | —— | Cape +200 | Fire +300 | SMALL -100 |
Cape | —— | —— | —— | SMALL -200 |
Fire | —— | —— | —— | SMALL -300 |
对于上方简单的案例,使用二维数组就可以表示状态的转移和对应的动作产生的分数加减了。具体代码如下
//定义事件枚举
public enum Event{
OBTAIN_MUSHROOM(0),
OBTAIN_CAPE(1),
OBTAIN_FIRE(2),
MEET_MONSTER(3);
//其他省略
}
//实现状态机
public class MarioStateMachine{
private int score;
private State currentState;
//定义二维数组,保存状态转移信息
private static final State[][] transitionTable = {
{SUPER,CAPE,FIRE,SMALL},
{SUPER,CAPE,FIRE,SMALL},
{CAPE,CAPE,CAPE,SMALL},
{FIRE,FIRE,FIRE,SMALL}
};
//定义二维数组,保存对应的动作(分数变更)
private static final int[][] actionTable={
{100,200,300,0},
{0,200,300,-100},
{0,0,0,-200},
{0,0,0,-300}
};
//构造函数,定义初始分数和状态
public MarioStateMachine(){
this.score=0;
this.currentState = State.SMALL;
}
//定义四个事件方法,方法中直接包含事件触发后的动作逻辑
//事件E1 吃到蘑菇
public void obtainMushRoom(){
}
//事件E2 获得斗篷
public void obtainCape(){
}
//事件E3 获得火焰
public void obtainFireFlower(){
}
//事件E4 遇到怪物 这里举例,只填充该方法内容
public void ontainMonster(){
}
//根据状态表执行状态转移和动作
private void executeEvent(Event event){
int stateValue=currentState.getvalue;
int eventValue=event.getValue;
//当前状态确定某一行,事件确定某一列,交点为转以后得状态。
this.currentState=transitionTable[stateValue][eventValue];
this.score+=actionTable[stateValue][eventValue];
}
public int getScore(){
return score;
}
public State getCurrentState(){
return state;
}
}
状态模式实现状态机
查表法实现状态机中,事件的动作只是简单地积分加减,所以用一个二维数组就能保存所有动作。但是业务中往往是一系列复杂的操作,比如操作数据库、缓存、远程接口等。此时查表法就有一定的局限性了。此时状态模式就可以排上用场。
状态模式将不同事件出发的状态和动作拆分到不同的状态类中,来避免分支语句。使用状态模式实现的代码如下
//marios所有的状态进行抽象,抽象方法为所有的事件。
public interface IMario{
State getName();
void obtainMushRoom();
void obtainCape();
void obtainFire();
void meetMonster();
}
//为每个状态的IMario单独创建实现类,不在状态机中保存状态转换的逻辑;其他状态省略
public class SmallMario implements IMario{
private MarioStateMachine stateMachine;
@Override
public State getName(){
return State.SMALL;
}
// SMALL状态下的马里奥,对吃到蘑菇事件的动作逻辑方法
@Override
public void obtainMushRoom(){
statemMachine.setCurrentState(new SuperMario(stateMachine));
stateMachine.setScore(stateMachine.getScore()+200);
}
//其他方法省略
...
}
//状态机
public class MarioStateMachine {
private int score;
private IMario currentState;//使用具体的IMario对象保存状态,而不是枚举
//构造函数,定义初始分数和状态
public MarioStateMachine(){
this.score=0;
this.currentState = new SmallMario(this);
}
//状态机事件
public void obtainMushRoom(){
//具体的动作逻辑,交由IMario对象实现
this.currentState.obtainMushRoom();
}
//其他方法省略
.....
}
- 可以看到,状态机和各个状态类是双向依赖关系。原因是状态机依赖状态类,是因为要保存当前状态。而状态类保存状态机对象,则是因为要更新状态机的数据。
- 可优化的点:状态类不保存任何数据,可以优化成单例模式,对于状态机对象直接作为方法参数进行传递。优化成单例模式代码如下。
//状态接口,定义的事件方法
public interface IMario{
State getName();
void obtainMushRoom( MarioStateMachine statemachine);
void obtainCape( MarioStateMachine statemachine);
void obtainFire( MarioStateMachine statemachine);
void meetMonster( MarioStateMachine statemachine);
}
//为每个状态的IMario单独创建实现类,不在状态机中保存状态转换的逻辑;其他状态省略
public class SmallMario implements IMario{
@Override
public State getName(){
return State.SMALL;
}
// SMALL状态下的马里奥,对吃到蘑菇事件的动作逻辑方法
@Override
public void obtainMushRoom(MarioStateMachine statemachine){
statemachine.setCurrentState(new SuperMario(stateMachine));
statemachine.setScore(stateMachine.getScore()+200);
}
//其他方法省略
...
}
Spring 状态机组件
Spring的状态机组件(Spring StateMachine)是Spring框架提供的一种用于实现有限状态机(Finite State Machine, FSM)模式的模块。
三种状态机的总结
对于游戏场景,包含非常多复杂的状态引入状态模式会导致非常多的状态类,所以推荐查表法。
对于电商订单、外卖订单等状态不多,状态转移简单但是动作复杂的场景,更推荐使用状态模式。