状态模式实现有限状态机
|总字数:2.7k|阅读时长:10分钟|浏览量:|
|出链:2|入链:3

状态模式与有限状态机的概念紧密相关。
实现一个有限状态机(FSM)
本小节将用代码实现一个简单的有限状态机。
一个蜗牛在一条写满 0 和 1 的纸带上爬行。当它爬过的最后两位是 01 时,蜗牛会对着你微笑。请构造一个有限状态机(Moore 型)。状态转换图如下:

我们先定义一个代码骨架:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| @Getter enum State { STATE0(0), STATE1(1), STATE2(2);
private final int value;
State(int value) { this.value = value; } }
@Getter class FSM { private State currentState;
public FSM() { this.currentState = State.STATE0; } public void action0() { }
public void action1() { } }
|
State 枚举类枚举 FSM 可能出现的状态。
Main 中的使用为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| public static void main(String[] args) { FSM fsm = new FSM(); String actions = "1111010100101"; for (char c : actions.toCharArray()) { if (c == '0') { fsm.action0(); } else { fsm.action1(); } if (fsm.getCurrentState() == State.STATE2) { System.out.print("^_^"); } else { System.out.print("."); } } }
|
fsm 的初始状态为 STATE0。针对状态转移 actionX(),有三种不同的实现形式。
方式 1:分支逻辑
最简单直接的实现方式是,参照状态转移图,将每一个状态转移,原模原样地直译成代码。这种方式为「用方法表示状态」。这样编写的代码会包含大量的 if-else 或 switch-case 分支判断逻辑,甚至是嵌套的分支判断逻辑。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| @Getter class FSM { private int score; private State currentState;
public FSM() { this.currentState = State.STATE0; }
public void action1() { if (currentState.equals(State.STATE0)) { this.currentState = State.STATE1; } }
}
|
对于复杂的状态机来说,这种实现方式极易漏写或者错写某个状态转移。除此之外,代码中充斥着大量的 if-else 或者 switch-case 分支判断逻辑,存在硬编码,可读性和可维护性都很差。
方法 2:查表
在之前数字逻辑课程的 FSM 实现过程中,有一个关键的中间产物——状态表:

我们可以用代码编写这个表,实现 FSM。
相对于分支逻辑的实现方式,查表法的代码实现更加清晰,可读性和可维护性更好。当修改状态机时,我们只需要修改 TRANSITION_TABLE(用于改变状态)二维数组即可。实际上,如果我们把这个二维数组存储在配置文件中,当需要修改状态机时,我们甚至可以不修改任何代码,只需要修改配置文件就可以了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| @Getter enum Event { GOT_0(0), GOT_1(1);
private final int value;
Event(int value) { this.value = value; } }
@Getter class FSM { private State currentState;
private static final State[][] TRANSITION_TABLE = { {State.STATE1, State.STATE0}, {State.STATE1, State.STATE2}, {State.STATE1, State.STATE0},
};
public FSM() { this.currentState = State.STATE0; }
public void action0() { executeEvent(Event.GOT_0); }
public void action1() { executeEvent(Event.GOT_1); }
private void executeEvent(Event event) { int stateValue = currentState.getValue(); int eventValue = event.getValue(); this.currentState = TRANSITION_TABLE[stateValue][eventValue]; } }
|
方式 3:状态模式
现在增加要求:当蜗牛执行动作的时候,根据当前所处的状态不同,积累一个全局的分数。比如 S1 状态下读到 1 时 +2 分,读到 0 时不得分。我们可以在查表法的基础上,在增加一个分数表,上面记录不同状态下进行的不同转移蜗牛应得到的分数。
但是,如果要积累的变量越来越多,或操作越来越复杂(写数据库、发网络请求等)时,查表法就显得一定的局限性了。
状态模式通过将事件触发的状态转移和动作执行,拆分到不同的状态类中,来避免分支判断逻辑。这就是「用类来表示状态」。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120
| package top.uuanqin;
import lombok.Getter; import lombok.Setter;
public class NewStateTest { public static void main(String[] args) { FSM fsm = new FSM(); String actions = "1111010100101"; for (char c : actions.toCharArray()) { if (c == '0') { fsm.action0(); } else { fsm.action1(); } if (fsm.getCurrentState() instanceof State2) { System.out.print("^_^"); } else { System.out.print("."); } } } }
interface IState { void action0(); void action1(); }
class State0 implements IState { FSM fsm;
public State0(FSM fsm) { this.fsm = fsm; }
@Override public void action0() { fsm.setCurrentState(new State1(fsm)); fsm.increaseScore(1); }
@Override public void action1() { fsm.setCurrentState(new State0(fsm)); } }
class State1 implements IState { FSM fsm;
public State1(FSM fsm) { this.fsm = fsm; }
@Override public void action0() { fsm.setCurrentState(new State1(fsm)); }
@Override public void action1() { fsm.setCurrentState(new State2(fsm)); fsm.increaseScore(2); } }
class State2 implements IState { FSM fsm;
public State2(FSM fsm) { this.fsm = fsm; }
@Override public void action0() { fsm.setCurrentState(new State1(fsm)); fsm.decreaseScore(1); }
@Override public void action1() { fsm.setCurrentState(new State0(fsm)); fsm.decreaseScore(5); } }
@Getter @Setter class FSM { private IState currentState; private int score;
public FSM() { this.currentState = new State0(this); this.score = 0; }
public void increaseScore(int score) { this.score += score; }
public void decreaseScore(int score) { this.score -= score; }
public void action0() { this.currentState.action0(); }
public void action1() { this.currentState.action1(); }
}
|
注意 FSM 和每个状态类直接是双向依赖的关系:
- FSM 依赖状态类是理所当然的,因为它需要保存当前状态,并将操作委托给状态类处理。
- 状态类依赖 FSM 的原因,是因为状态类执行行动时,需要调用 FSM 中的函数以改变 FSM 中的状态(比如分数)。
方法 4:单例模式优化状态模式
状态类中不包含任何成员变量,我们可以将状态类设计为 站内文章单例模式。设计为单例模式后,状态类就无法通过构造函数创建,也无法为状态类传入 fsm。解决方法为,通过状态类的方法传入 fsm(方法 3 中是构造函数传入的)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121
| package top.uuanqin;
import lombok.Getter; import lombok.Setter;
public class NewStateTest { public static void main(String[] args) { FSM fsm = new FSM(); String actions = "1111010100101"; for (char c : actions.toCharArray()) { if (c == '0') { fsm.action0(); } else { fsm.action1(); } if (fsm.getCurrentState() instanceof State2) { System.out.print("^_^"); } else { System.out.print("."); } } } }
interface IState { void action0(FSM fsm); void action1(FSM fsm); }
class State0 implements IState {
public static final IState INSTANCE = new State0();
public static IState getInstance() { return INSTANCE; }
@Override public void action0(FSM fsm) { fsm.setCurrentState(State1.getInstance()); fsm.increaseScore(1); }
@Override public void action1(FSM fsm) { fsm.setCurrentState(State0.getInstance()); } }
class State1 implements IState { public static final IState INSTANCE = new State1();
public static IState getInstance() { return INSTANCE; }
@Override public void action0(FSM fsm) { fsm.setCurrentState(State1.getInstance()); }
@Override public void action1(FSM fsm) { fsm.setCurrentState(State2.getInstance()); fsm.increaseScore(2); } }
class State2 implements IState { public static final IState INSTANCE = new State2();
public static IState getInstance() { return INSTANCE; }
@Override public void action0(FSM fsm) { fsm.setCurrentState(State1.getInstance()); fsm.decreaseScore(1); }
@Override public void action1(FSM fsm) { fsm.setCurrentState(State0.getInstance()); fsm.decreaseScore(5); } }
@Getter @Setter class FSM { private IState currentState; private int score;
public FSM() { this.currentState = State0.getInstance(); this.score = 0; }
public void increaseScore(int score) { this.score += score; }
public void decreaseScore(int score) { this.score -= score; }
public void action0() { this.currentState.action0(this); }
public void action1() { this.currentState.action1(this); } }
|
FSM 实现方式的选用
如果状态比较多,且状态迁移遵循一定规律时,优先推荐查表法,因为状态模式会引用非常多的状态类,会导致代码难以维护。
对于电商下单、外卖下单的状态机,状态并不多、状态转移简单但事件触发执行的动作包含的业务逻辑会比较复杂,更适合使用状态模式实现。
如果状态数过多时,尝试使用程序生成代码而不是手写代码。
状态模式

状态模式的类图如下:

登场角色:
State(状态):State 角色表示状态,定义了根据不同状态进行不同处理的接口(API)。该接口 (API) 是那些处理内容依赖于状态的方法的集合。
ConcreteState(具体状态):ConcreteState 角色表示各个具体的状态,它实现了 State 接口。
Context(状况、前后关系、上下文):Context 角色持有表示当前状态的 ConcreteState 角色。此外,它还定义了供外部调用者使用 State 模式的接口(API)。上一节中 FSM 就是这个角色。
讨论 Context 上下文与 State 状态类的双向依赖:
Context 提供了一些操作数据的方法,但是在不同的状态下应当进行不同的操作(依赖于状态的操作),所以 Context 委托给 State 进行数据操作。依赖于状态的操作会分散到每个 ConcreteState 中。
- 在状态模式中,我们将「状态迁移的管理」也交给了
ConcreteState,也就是说我们也把它当成了「依赖于状态的操作」。这种方式具有优缺点:
- 优点:想知道某一个状态在什么情况下迁移到其他状态时,只需阅读这个类的代码就行。
- 缺点:每一个
ConcreteState 需要了解其他的 ConcreteState 角色。每个类的依赖关系将会加强。
- 我们可以使用 站内文章中介者模式,将「状态迁移的管理」交给
Context,提高 ConcreteState 的独立性。
在状态模式中增加新的状态十分简单,直接新建一个 ConcreteState 就行。但是修改状态转移的代码时,需要更加仔细,但也不用担心会忘记实现某些方法,因为编译器会告诉你。
| 优点 |
缺点 |
| 单一职责原则。将与特定状态相关的代码放在单独的类中。 |
如果状态机只有很少的几个状态,或者很少发生改变,那么应用该模式可能会显得小题大作。 |
| 开闭原则。无需修改已有状态类和上下文就能引入新状态。 |
|
| 通过消除臃肿的状态机条件语句简化上下文代码。 |
相关的设计模式:
- 站内文章单例模式:单例模式常常会出现在
ConcreteState 角色中。这是因为在表示状态的类中并没有定义任何实例字段(即表示实例的状态的字段)。
- 站内文章享元模式:在表示状态的类中并没有定义任何实例字段。因此,有时我们可以使用享元模式在多个
Context 角色之间共享 ConcreteState 角色。
- 站内文章策略模式:状态模式可被视为策略模式的扩展。两者都基于组合机制:它们都通过将部分工作委派给「帮手」对象来改变其在不同情景下的行为。策略使得这些对象相互之间完全独立,它们不知道其他对象的存在。但状态模式没有限制具体状态之间的依赖,且允许它们自行改变在不同情景下的状态。
本文参考