一. 什么是状态模式
状态模式是状态机的一种实现方式. 状态机又叫有限状态机(FSM)
- 状态模式不常用, 有点像组合模式
- 状态机包含3个部分:
- 状态
- 事件
- 动作
二. 描述 FSM 的是那种方法
背景: 在超级马里奥游戏中,马里奥可以变身为多种形态,比如小马里奥, 超级马里奥, 火焰马里奥, 斗篷马里奥等等。在不同的游戏情节下,各个形态会互相转化,并相应的增减积分。比如,初始形态是小马里奥,吃了蘑菇之后就会变成超级马里奥,并且增加 100 积分。
马里奥形态的转变就是一个状态机。其中,马里奥的不同形态就是状态机中的“状态”,游戏情节(比如吃了蘑菇)就是状态机中的“事件”,加减积分就是状态机中的“动作”。比如,吃蘑菇这个事件,会触发状态的转移:从小马里奥转移到超级马里奥,以及触发动作的执行(增加 100 积分)。
如何编程来实现上面的状态机呢?(描述状态转的3个方法)
-
if-else 分支判断, 根据不同状态做出对事件的反应
- 分支描述速马里奥
// 描述状态的枚举 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; } } // 状态机描述 (遇到事件转变不同状态) 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; } } // 调用方 public class ApplicationDemo { public static void main(String[] args) { MarioStateMachine mario = new MarioStateMachine(); mario.obtainMushRoom(); int score = mario.getScore(); State state = mario.getCurrentState(); System.out.println("mario score: " + score + "; state: " + state); } }
- 缺点
对于简单的状态机来说,分支逻辑这种实现方式是可以接受的。但是,对于复杂的状态机来说,这种实现方式极易漏写或者错写某个状态转移。
-
状态模式:
- 状态模式是改进上面使用 if-else 描述状态转移机的做法. 将状态转移和动作执行拆分到不同类中
- (1) 定义状态接口. 方法为対各事件做出的动作
这里将动作设为单例的, 因为状态不包含任何参数
// 状态接口 public interface IMario { State getName(); // 持有 MarioStateMachine 作为参数, 是因为要该表 状态机中的得分和下一状态 void obtainMushRoom(MarioStateMachine stateMachine); // 吃了蘑菇 void obtainCape(MarioStateMachine stateMachine); // 带了斗篷 void obtainFireFlower(MarioStateMachine stateMachine);// 获得火花 void meetMonster(MarioStateMachine stateMachine); // 碰到怪兽 } 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) { // do nothing... } } // 省略SuperMario、CapeMario、FireMario类...
- (2) 定义状态机: 状态机里有得分和当前的状态
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) 定义状态接口. 方法为対各事件做出的动作
- 优点和缺点
- 实际上,像游戏这种比较复杂的状态机,包含的状态比较多,我优先推荐使用查表法,而状态模式会引入非常多的状态类,会导致代码比较难维护。
- 相反,像电商下单、外卖下单这种类型的状态机,它们的状态并不多,状态转移也比较简单,但事件触发执行的动作包含的业务逻辑可能会比较复杂,所以,更加推荐使用状态模式来实现。
- 状态模式是改进上面使用 if-else 描述状态转移机的做法. 将状态转移和动作执行拆分到不同类中
-
查表法
- 生成状态表
状态机还可以用二维表来表示,如下所示。在这个二维表中,第一维表示当前状态,第二维表示事件,值表示当前状态经过事件之后,转移到的新状态及其执行的动作。
E1(get mushroom) E2(get cape) E3(get fireflower) E4(meet monster) 小玛丽奥(small) super/+100 cape/+200 fire/+300 / 超级玛丽奥(super) / cape/+200 fire/+300 small/-100 斗篷马里奥(cape) / / / small/-200 火焰马里奥(fire) / / / small/-300 - 查表法描述马里奥
// 状态枚举 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} }; // action 表(奖励或惩罚) 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; } }
- 生成状态表