首页 状态模式
文章
取消

状态模式

定义

状态模式一般用来实现状态机常用在游戏机,而状态、工作流引擎等系统开发中。不过,状态机的实现方式有多种,除了状态模式,比较常用的还有分支逻辑法和查表法。从状态模式的设计来看,它有点像组合模式。

有限状态机

简称为状态机。状态机有 3 个组成部分:状态(State)、事件(Event)、动作(Action)。其中,事件也称为转移条件(Transition Condition)。事件触发状态的转移及动作的执行。不过,动作不是必须的,也可能只转移状态,不执行任何动作。

以超级马里奥为例:马里奥可以变身多种形态,比如比如小马里奥(Small Mario)、超级马里奥(Super Mario)、火焰马里奥(Fire Mario)、斗篷马里奥(Cape Mario)等等。在不同的游戏情节下,各个形态会互相转化,并相应的增减积分。比如,初始形态是小马里奥,吃了蘑菇之后就会变成超级马里奥,并且增加 100 积分。其中游戏情节(比如吃了蘑菇)就是状态机中的“事件”,加减积分就是状态机中的“动作”。比如,吃蘑菇这个事件,会触发状态的转移:从小马里奥转移到超级马里奥,以及触发动作的执行(增加 100 积分)。下面对这个场景的状态转移图:

其中 E1(吃了蘑菇)、E2(获得斗篷) 代表事件,Small 代表状态,+200 代表状态转移后执行的动作。

那么如何将上面的模型翻译成代码呢?这就是下面的重点

有限状态机的实现方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
 * 状态枚举,在下面三种实现方式中都会用到
 */
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 this.value;
    }
}

分支逻辑法

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
public class MarioStateMachine {
  private int score;
  private State currentState;
  public MarioStateMachine() {
    this.score = 0;
    this.currentState = State.SMALL;
  }
  public void obtainMushRoom() {
    if (currentState.equals(State.SMALL)) {
      this.currentState = State.SUPER;
      this.score += 100;
    }
  }
  public void obtainCape() {
    if (currentState.equals(State.SMALL) || currentState.equals(State.SUPER) ) {
      this.currentState = State.CAPE;
      this.score += 200;
    }
  }
  public void obtainFireFlower() {
    if (currentState.equals(State.SMALL) || currentState.equals(State.SUPER) ) {
      this.currentState = State.FIRE;
      this.score += 300;
    }
  }
  public void meetMonster() {
    if (currentState.equals(State.SUPER)) {
      this.currentState = State.SMALL;
      this.score -= 100;
      return;
    }
    if (currentState.equals(State.CAPE)) {
      this.currentState = State.SMALL;
      this.score -= 200;
      return;
    }
    if (currentState.equals(State.FIRE)) {
      this.currentState = State.SMALL;
      this.score -= 300;
      return;
    }
  }
  public int getScore() {
    return this.score;
  }
  public State getCurrentState() {
    return this.currentState;
  }
}

如上每个函数代表一种事件被触发,内部逻辑就是状态转移和动作执行,显然在这种情况下一旦业务复杂 MarioStateMachine 的可读性和可维护性将大大降低、

查表法

这是一种非常巧妙的方法,实际上状态机除了用状态转移图表示还能用二维表表示,如下:

相对于分支逻辑的实现方式,查表法的代码实现更加清晰,可读性和可维护性更好。当修改状态机时,我们只需要修改transitionTable和actionTable两个二维数组即可。实际上,如果我们把这两个二维数组存储在配置文件中,当需要修改状态机时,我们甚至可以不修改任何代码,只需要修改配置文件就可以了。具体的代码如下所示:

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
public enum Event {
  GOT_MUSHROOM(0),
  GOT_CAPE(1),
  GOT_FIRE(2),
  MET_MONSTER(3);
  private int value;
  private Event(int value) {
    this.value = value;
  }
  public int getValue() {
    return this.value;
  }
}


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;
  }
  public void obtainMushRoom() {
    executeEvent(Event.GOT_MUSHROOM);
  }
  public void obtainCape() {
    executeEvent(Event.GOT_CAPE);
  }
  public void obtainFireFlower() {
    executeEvent(Event.GOT_FIRE);
  }
  public void meetMonster() {
    executeEvent(Event.MET_MONSTER);
  }
  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 this.score;
  }
  public State getCurrentState() {
    return this.currentState;
  }
}

重点就在于 transitionTable 这个二维数组,它将当前状态在经过事件触发后的状态转移都记录在了行当中,一目了然。但它也有缺点:actionTable 能够记录的动作是有局限性的,如果动作较为复杂,那么将难以用数组进行记录。

状态模式

实际上状态模式就是对分支逻辑法的优化,在分支逻辑法中,所有的状态转移逻辑和动作都被放在同一个类当中,而状态模式就是将事件触发的状态转移和动作执行,拆分到不同的状态类中,来避免分支判断逻辑。code 如下:

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
/**
 * 状态类接口
 */
public interface IMario {
    State getName();
    //以下是定义的事件
    void obtainMushRoom();
    void obtainCape();
    void obtainFireFlower();
    void meetMonster();
}

/**
 * SmallMario 状态
 */
public class SmallMario implements IMario {
    private MarioStateMachine stateMachine;
    public SmallMario(MarioStateMachine stateMachine) {
        this.stateMachine = stateMachine;
    }
    @Override
    public State getName() {
        return State.SMALL;
    }
    @Override
    public void obtainMushRoom() {
        stateMachine.setCurrentState(new SuperMario(stateMachine));
        stateMachine.setScore(stateMachine.getScore() + 100);
    }
    @Override
    public void obtainCape() {
        stateMachine.setCurrentState(new SuperMario(stateMachine));
        stateMachine.setScore(stateMachine.getScore() + 200);
    }
    @Override
    public void obtainFireFlower() {
        stateMachine.setCurrentState(new SuperMario(stateMachine));
        stateMachine.setScore(stateMachine.getScore() + 300);
    }
    @Override
    public void meetMonster() {
        // do nothing...
    }
}

/**
 * SuperMario 状态
 */
public class SuperMario implements IMario {
    private MarioStateMachine stateMachine;
    public SuperMario(MarioStateMachine stateMachine) {
        this.stateMachine = stateMachine;
    }
    @Override
    public State getName() {
        return State.SUPER;
    }
    @Override
    public void obtainMushRoom() {
        // do nothing...
    }
    @Override
    public void obtainCape() {
        stateMachine.setCurrentState(new SmallMario(stateMachine));
        stateMachine.setScore(stateMachine.getScore() + 200);
    }
    @Override
    public void obtainFireFlower() {
        stateMachine.setCurrentState(new SmallMario(stateMachine));
        stateMachine.setScore(stateMachine.getScore() + 300);
    }
    @Override
    public void meetMonster() {
        stateMachine.setCurrentState(new SmallMario(stateMachine));
        stateMachine.setScore(stateMachine.getScore() - 100);
    }
}

/**
 * 状态机
 */
public class MarioStateMachine {
    private int score;
    private IMario currentState; // 不再使用枚举来表示状态
    public MarioStateMachine() {
        this.score = 0;
        this.currentState = new SmallMario(this);
    }
    public void obtainMushRoom() {
        this.currentState.obtainMushRoom();
    }
    public void obtainCape() {
        this.currentState.obtainCape();
    }
    public void obtainFireFlower() {
        this.currentState.obtainFireFlower();
    }
    public void meetMonster() {
        this.currentState.meetMonster();
    }
    public int getScore() {
        return this.score;
    }
    public State getCurrentState() {
        return this.currentState.getName();
    }
    public void setScore(int score) {
        this.score = score;
    }
    public void setCurrentState(IMario currentState) {
        this.currentState = currentState;
    }
}

在这种实现方式下,MarioStateMachine 的每次事件触发,最后会调用状态类自己的实现方式,但 MarioStateMachine 和 状态类是相互依赖的,之所以相互依赖是因为动作执行的结果最终记录在了 MarioStateMachine 中,而状态类本身代表状态,所以也被记录在了 MarioStateMachine 中。

实际上上面的代码还不是最优,因为状态类并没有可变的成员变量,所以每种状态变量实际上可以设计成单例类,但是这样会导致它无法通过构造器传递 MarioStateMachine,解决办法很简单:在事件触发函数中通过参数进行传递即可。就像下面这样:

本文由作者按照 CC BY 4.0 进行授权

观察者模式

职责链模式