摘要生成中...
AI 摘要
Hunyuan-lite

image.png

状态模式与有限状态机的概念紧密相关。

实现一个有限状态机(FSM)

本小节将用代码实现一个简单的有限状态机。

蜗牛的微笑

一个蜗牛在一条写满 0 和 1 的纸带上爬行。当它爬过的最后两位是 01 时,蜗牛会对着你微笑。请构造一个有限状态机(Moore 型)。状态转换图如下:
image.png

我们先定义一个代码骨架:

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() {
//TODO
}

public void action1() {
//TODO
}
}

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-elseswitch-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;
}
// else if( ){}
// else if( ){}
// ...
}

/* 省略其他方法 */
}

对于复杂的状态机来说,这种实现方式极易漏写或者错写某个状态转移。除此之外,代码中充斥着大量的 if-else 或者 switch-case 分支判断逻辑,存在硬编码,可读性和可维护性都很差。

方法 2:查表

在之前数字逻辑课程的 FSM 实现过程中,有一个关键的中间产物——状态表:

image.png

我们可以用代码编写这个表,实现 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;

/**
* @author uuanqin
*/
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;

/**
* @author uuanqin
*/
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 实现方式的选用

如果状态比较多,且状态迁移遵循一定规律时,优先推荐查表法,因为状态模式会引用非常多的状态类,会导致代码难以维护。

对于电商下单、外卖下单的状态机,状态并不多、状态转移简单但事件触发执行的动作包含的业务逻辑会比较复杂,更适合使用状态模式实现。

如果状态数过多时,尝试使用程序生成代码而不是手写代码。

状态模式

image.png

状态模式的类图如下:

image.png

登场角色:

  • 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 角色。
  • 站内文章策略模式:状态模式可被视为策略模式的扩展。两者都基于组合机制:它们都通过将部分工作委派给「帮手」对象来改变其在不同情景下的行为。策略使得这些对象相互之间完全独立,它们不知道其他对象的存在。但状态模式没有限制具体状态之间的依赖,且允许它们自行改变在不同情景下的状态。

本文参考