行为型-State

简介: 在实际的软件开发中,状态模式并不是很常用,但是在能够用到的场景里,它可以发挥很大的作用。从这一点上来看,它有点像我们之前讲到的组合模式。状态模式一般用来实现状态机,而状态机常用在游戏、工作流引擎等系统开发中。不过,状态机的实现方式有多种,除了状态模式,比较常用的还有分支逻辑法和查表法。今天,我们就详细讲讲这几种实现方式,并且对比一下它们的优劣和应用场景。

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


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


什么是有限状态机?



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


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


image.png


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


对于如何实现状态机,我总结了三种方式。其中,最简单直接的实现方式是,参照状态转移图,将每一个状态转移,原模原样地直译成代码。这样编写的代码会包含大量的 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,对于复杂的状态机来说不适用,而状态机的第二种实现方式查表法,就更加合适了。接下来,我们就一块儿来看下,如何利用查表法来补全骨架代码。


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


image.png


相对于分支逻辑的实现方式,查表法的代码实现更加清晰,可读性和可维护性更好。当修改状态机时,我们只需要修改 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



目录
相关文章
|
2月前
|
设计模式
设计模式之 State(状态模式)
设计模式之 State(状态模式)
18 0
|
2月前
|
设计模式 程序员
设计模式-状态模式(State)
设计模式-状态模式(State)
45 0
|
9月前
|
设计模式
设计模式~状态模式(state)-23
在状态模式(State Pattern)中,类的行为是基于它的状态改变的。这种类型的设计模式属于行为型模式。在状态模式中,我们创建表示各种状态的对象和一个行为随着状态对象改变而改变的context对象。 目录 (1)优点: (2)缺点: (3)使用场景: (4)注意事项: (5)应用实例: 代码
38 1
|
9月前
|
设计模式
设计模式19 - 状态模式【State Pattern】
设计模式19 - 状态模式【State Pattern】
24 0
|
12月前
状态模式(State)
状态模式(State)
107 0
|
设计模式 Java
Java设计模式-状态模式(State)
Java设计模式-状态模式(State)
|
设计模式 Java 编译器
设计模式学习(五):State状态模式
在面向对象编程中,是用类表示对象的。也就是说,程序的设计者需要考虑用类来表示什么东西。类对应的东西可能存在于真实世界中,也可能不存在于真实世界中。
124 0
设计模式学习(五):State状态模式
|
设计模式
设计模式实战-状态模式(State Pattern)(中)
设计模式实战-状态模式(State Pattern)(中)
121 0
设计模式实战-状态模式(State Pattern)(中)
|
设计模式 Java
浅谈JAVA设计模式之——状态模式(State)
定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
148 0
浅谈JAVA设计模式之——状态模式(State)