美文网首页
行为型-State

行为型-State

作者: acc8226 | 来源:发表于2021-07-03 09:29 被阅读0次

    在实际的软件开发中,状态模式并不是很常用,但是在能够用到的场景里,它可以发挥很大的作用。从这一点上来看,它有点像我们之前讲到的组合模式。

    状态模式一般用来实现状态机,而状态机常用在游戏、工作流引擎等系统开发中。不过,状态机的实现方式有多种,除了状态模式,比较常用的还有分支逻辑法和查表法。今天,我们就详细讲讲这几种实现方式,并且对比一下它们的优劣和应用场景。

    什么是有限状态机?

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

    为了方便接下来的讲解,我对游戏背景做了简化,只保留了部分状态和事件。简化之后的状态转移如下图所示:

    状态机实现方式一:分支逻辑法

    对于如何实现状态机,我总结了三种方式。其中,最简单直接的实现方式是,参照状态转移图,将每一个状态转移,原模原样地直译成代码。这样编写的代码会包含大量的 if-else 或 switch-case 分支判断逻辑,甚至是嵌套的分支判断逻辑,所以,我把这种方法暂且命名为分支逻辑法。

    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 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);
      }
    }
    
    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;
      }
    }
    

    状态机实现方式二:查表法

    实际上,上面这种实现方法有点类似 hard code,对于复杂的状态机来说不适用,而状态机的第二种实现方式查表法,就更加合适了。接下来,我们就一块儿来看下,如何利用查表法来补全骨架代码。

    实际上,除了用状态转移图来表示之外,状态机还可以用二维表来表示,如下所示。在这个二维表中,第一维表示当前状态,第二维表示事件,值表示当前状态经过事件之后,转移到的新状态及其执行的动作。

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

    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;
      }
    
    }
    

    状态机实现方式三:状态模式

    其中,IMario 是状态的接口,定义了所有的事件。SmallMario、SuperMario、CapeMario、FireMario 是 IMario 接口的实现类,分别对应状态机中的 4 个状态。原来所有的状态转移和动作执行的代码逻辑,都集中在 MarioStateMachine 类中,现在,这些代码逻辑被分散到了这 4 个状态类中。

    public interface IMario {
      State getName();
      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类...
    
    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;
      }
    }
    

    调用 main 方法

    MarioStateMachine stateMachine = new MarioStateMachine();
    stateMachine.obtainCape()
    
    这里就可以打印 stateMachine.getScore() 和 stateMachine.getCurrentState() 的值
    

    实际上,像游戏这种比较复杂的状态机,包含的状态比较多,我优先推荐使用查表法,而状态模式会引入非常多的状态类,会导致代码比较难维护。相反,像电商下单、外卖下单这种类型的状态机,它们的状态并不多,状态转移也比较简单,但事件触发执行的动作包含的业务逻辑可能会比较复杂,所以,更加推荐使用状态模式来实现。

    我们总结了三种实现方式。

    第一种实现方式叫分支逻辑法。利用 if-else 或者 switch-case 分支逻辑,参照状态转移图,将每一个状态转移原模原样地直译成代码。对于简单的状态机来说,这种实现方式最简单、最直接,是首选。

    第二种实现方式叫查表法。对于状态很多、状态转移比较复杂的状态机来说,查表法比较合适。通过二维数组来表示状态转移图,能极大地提高代码的可读性和可维护性。

    第三种实现方式叫状态模式。对于状态并不多、状态转移也比较简单,但事件触发执行的动作包含的业务逻辑可能比较复杂的状态机来说,我们首选这种实现方式。

    参考

    设计模式之美设计模式代码重构-极客时间
    https://time.geekbang.org/column/intro/250

    相关文章

      网友评论

          本文标题:行为型-State

          本文链接:https://www.haomeiwen.com/subject/ksbfyltx.html