状态模式一般用来实现状态机,状态机的实现方式有多种,除了状态模式,比较常用的还有 分支逻辑法 和 查表法 。
什么是有限状态机?
有限状态机,英文翻译是 Finite State Machine,缩写为 FSM,简称为状态机。状态机有 3 个组成部分:状态(State)、事件(Event)、动作(Action)。其中,事件也称为转移条件(Transition Condition)。事件触发状态的转移及动作的执行。不过,动作不是必须的,也可能只转移状态,不执行任何动作。
举例实现
“超级马里奥” 在游戏中,马里奥可以变身为多种形态,比如小马里奥(Small Mario)、超级马里奥(Super Mario)、火焰马里奥(Fire Mario)、斗篷马里奥(Cape Mario)等等。在不同的游戏情节下,各个形态会互相转化,并相应的增减积分。比如,初始形态是小马里奥,吃了蘑菇之后就会变成超级马里奥,并且增加 100 积分。
其中,马里奥的不同形态就是状态机中的 “状态”,游戏情节(比如吃了蘑菇)就是状态机中的 “事件”,加减积分就是状态机中的 “动作”。比如,吃蘑菇这个事件,会触发状态的转移:从小马里奥转移到超级马里奥,以及触发动作的执行(增加 100 积分)。
具体状态与事件的关系
| State\Event |
Got MushRoom |
Got Cape |
Got Fire Flower |
Met Monster |
| Small |
Super / +100 |
Cape / +200 |
Fire / +300 |
/ |
| Super |
/ |
Cape / +200 |
Fire / +300 |
Small / -100 |
| Cape |
/ |
/ |
/ |
Small / -200 |
| Fire |
/ |
/ |
/ |
Small / -300 |
状态机实现方式一:分支逻辑法
最简单直接的实现方式,参照状态表,将每一个状态转移,原模原样地直译成代码。这样编写的代码会包含大量的 if-else 或 switch-case 分支判断逻辑,甚至是嵌套的分支判断逻辑
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
| public enum State { SMALL(0), SUPER(1), CAPE(2), FIRE(3); private final int value;
State(int value) { this.value = value; }
public int getValue() { return this.value; } }
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; } }
|
代码中充斥着大量的 if-else 或者 switch-case 分支判断逻辑,可读性和可维护性都很差。如果哪天修改了状态机中的某个状态转移,我们要在冗长的分支逻辑中找到对应的代码进行修改,很容易改错,引入 bug。
状态机实现方式二:查表法
上面列出的 具体状态与事件的关系 可以看出这是一个二维表,第一维表示当前状态,第二维表示事件,值表示当前状态经过事件之后,转移到的新状态及其执行的动作。
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
| 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 两个二维数组即可。
这里事件的触发只是简单的积分加减,所以,我们用一个 int 类型的二维数组 actionTable 就能表示,在多一个简单事件的话,可以再多写一个二维表,但是如果要执行的动作并非这么简单,而是一系列复杂的逻辑操作,就没法用如此简单的二维数组来表示了。这也就是说,查表法的实现方式有一定局限性。 这就引出了状态模式。
状态机实现方式三:状态模式
状态模式通过将事件触发的状态转移和动作执行,拆分到不同的状态类中,来避免分支判断逻辑。
现在 MarioStateMachine 只负责 IMario 的调度。
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
| public interface IMario { State getName(); void obtainMushRoom(MarioStateMachine stateMachine); void obtainCape(MarioStateMachine stateMachine); void obtainFireFlower(MarioStateMachine stateMachine); void meetMonster(MarioStateMachine stateMachine); }
public class MarioStateMachine { private int score; private IMario currentState;
public MarioStateMachine() { this.score = 0; this.currentState = SmallMario.getInstance(); }
public void obtainMushRoom() { this.currentState.obtainMushRoom(this); }
public void obtainCape() { this.currentState.obtainCape(this); }
public void obtainFireFlower() { this.currentState.obtainFireFlower(this); }
public void meetMonster() { this.currentState.meetMonster(this); }
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; } }
|
然后是具体的状态代码。
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
| public class SmallMario implements IMario { private static final SmallMario instance = new SmallMario();
private SmallMario() { }
public static SmallMario getInstance() { return instance; }
@Override public State getName() { return State.SMALL; }
@Override public void obtainMushRoom(MarioStateMachine stateMachine) { stateMachine.setCurrentState(SuperMario.getInstance()); stateMachine.setScore(stateMachine.getScore() + 100); }
@Override public void obtainCape(MarioStateMachine stateMachine) { stateMachine.setCurrentState(CapeMario.getInstance()); stateMachine.setScore(stateMachine.getScore() + 200); }
@Override public void obtainFireFlower(MarioStateMachine stateMachine) { stateMachine.setCurrentState(FireMario.getInstance()); stateMachine.setScore(stateMachine.getScore() + 300); }
@Override public void meetMonster(MarioStateMachine stateMachine) { } }
|
状态模式会引入非常多的状态类,如果状态比较多会导致代码比较难维护,但是像电商下单、外卖下单这种类型的状态机,它们的状态不多,状态转移也比较简单,但事件触发执行的动作包含的业务逻辑可能会比较复杂,所以,使用状态模式来实现更合适。
感谢
设计模式之美