状态模式

一. 什么是状态模式

状态模式是状态机的一种实现方式. 状态机又叫有限状态机(FSM)

  • 状态模式不常用, 有点像组合模式
  • 状态机包含3个部分:
    • 状态
    • 事件
    • 动作

二. 描述 FSM 的是那种方法

背景: 在超级马里奥游戏中,马里奥可以变身为多种形态,比如小马里奥, 超级马里奥, 火焰马里奥, 斗篷马里奥等等。在不同的游戏情节下,各个形态会互相转化,并相应的增减积分。比如,初始形态是小马里奥,吃了蘑菇之后就会变成超级马里奥,并且增加 100 积分。
马里奥形态的转变就是一个状态机。其中,马里奥的不同形态就是状态机中的“状态”,游戏情节(比如吃了蘑菇)就是状态机中的“事件”,加减积分就是状态机中的“动作”。比如,吃蘑菇这个事件,会触发状态的转移:从小马里奥转移到超级马里奥,以及触发动作的执行(增加 100 积分)。
如何编程来实现上面的状态机呢?(描述状态转的3个方法)

  1. 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);
      }
    }
    
    • 缺点
      对于简单的状态机来说,分支逻辑这种实现方式是可以接受的。但是,对于复杂的状态机来说,这种实现方式极易漏写或者错写某个状态转移。
  2. 状态模式:

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

    • 生成状态表
      状态机还可以用二维表来表示,如下所示。在这个二维表中,第一维表示当前状态,第二维表示事件,值表示当前状态经过事件之后,转移到的新状态及其执行的动作。
    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;
      }
    
    }
    
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 224,535评论 6 522
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 96,106评论 3 402
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 171,668评论 0 366
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 60,863评论 1 300
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 69,874评论 6 399
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 53,362评论 1 314
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 41,748评论 3 428
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 40,717评论 0 279
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 47,249评论 1 324
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 39,280评论 3 345
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 41,408评论 1 354
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 37,020评论 5 350
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 42,727评论 3 337
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 33,191评论 0 25
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 34,320评论 1 275
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 49,946评论 3 381
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 46,473评论 2 365

推荐阅读更多精彩内容