《设计模式之美》状态模式:游戏、工作流引擎中常用的状态机是如何实现的?

本文介绍了有限状态机的概念,并通过《设计模式之美》中的例子阐述了状态模式在游戏(如超级马里奥)和工作流引擎中的应用。文章详细对比了分支逻辑法、查表法和状态模式三种实现状态机的方式,强调了状态模式在处理复杂逻辑时的优势。同时,提供了代码示例和优化建议,帮助读者更好地理解和应用状态模式。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

王争《设计模式之美》学习笔记

  • 在实际的软件开发中,状态模式并不是很常用,但是在能够用到的场景里,它可以发挥很大的作用。
  • 状态模式一般用来实现状态机,而状态机常用在游戏、工作流引擎等系统开发中。

什么是有限状态机

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

文中举例

  • 超级马里奥
  • 马里奥形态的转变就是一个状态机:
    • 其中,马里奥的不同形态就是状态机中的“状态”
    • 游戏情节(比如吃了蘑菇)就是状态机中的“事件”
    • 加减积分就是状态机中的“动作”
    • 比如,吃蘑菇这个事件,会触发状态的转移:从小马里奥转移到超级马里奥,以及触发动作的执行(增加100积分)

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

  • ,最简单直接的实现方式是,参照状态转移图,将每一个状态转移,原模原样地直译成代码。这样编写的代码会包含大量的 if-else 或 switch-case 分支判断逻辑,甚至是嵌套的分支判断逻辑,所以,我把这种方法暂且命名为分支逻辑法。
  • 对于复杂的状态机来说,这种实现方式极易漏写或者错写某个状态转移。除此之外,代码中充斥着大量的 if-else 或者 switch-case 分支判断逻辑,可读性和可维护性都很差。如果哪天修改了状态机中的某个状态转移,我们要在冗长的分支逻辑中找到对应的代码进行修改,很容易改错,引入 bug。

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

  • 除了用状态转移图来表示之外,状态机还可以用二维表来表示。在这个二维表中,第一维表示当前状态,第二维表示事件,值表示当前状态经过事件之后,转移到的新状态及其执行的动作。
  • 查表法的代码实现更加清晰,可读性和可维护性更好。当修改状态机时,我们只需要修改 transitionTable 和 actionTable 两个二维数组即可。实际上,如果我们把这两个二维数组存储在配置文件中,当需要修改状态机时,我们甚至可以不修改任何代码,只需要修改配置文件就可以了。

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

  • ,如果要执行的动作并非这么简单,而是一系列复杂的逻辑操作(比如加减积分、写数据库,还有可能发送消息通知等等),我们就没法用如此简单的二维数组来表示了。这也就是说,查表法的实现方式有一定局限性。
  • 状态模式通过将事件触发的状态转移和动作执行,拆分到不同的状态类中,来避免分支判断逻辑。

代码示例

public interface IMario { //所有状态类的接口
  State getName();
  //以下是定义的事件
  void obtainMushRoom();
  void obtainCape();
  void obtainFireFlower();
  void meetMonster();
}

public class SmallMario implements IMario {
  private MarioStateMachine stateMachine;
  public SmallMario(MarioStateMachine stateMachine) {
    this.stateMachine = stateMachine;
  }
  @Override
  public State getName() {
    return State.SMALL;
  }
  @Override
  public void obtainMushRoom() {
    stateMachine.setCurrentState(new SuperMario(stateMachine));
    stateMachine.setScore(stateMachine.getScore() + 100);
  }
  @Override
  public void obtainCape() {
    stateMachine.setCurrentState(new CapeMario(stateMachine));
    stateMachine.setScore(stateMachine.getScore() + 200);
  }
  @Override
  public void obtainFireFlower() {
    stateMachine.setCurrentState(new FireMario(stateMachine));
    stateMachine.setScore(stateMachine.getScore() + 300);
  }
  @Override
  public void meetMonster() {
    // do nothing...
  }
}

public class SuperMario implements IMario {
  private MarioStateMachine stateMachine;
  public SuperMario(MarioStateMachine stateMachine) {
    this.stateMachine = stateMachine;
  }
  @Override
  public State getName() {
    return State.SUPER;
  }
  @Override
  public void obtainMushRoom() {
    // do nothing...
  }
  @Override
  public void obtainCape() {
    stateMachine.setCurrentState(new CapeMario(stateMachine));
    stateMachine.setScore(stateMachine.getScore() + 200);
  }
  @Override
  public void obtainFireFlower() {
    stateMachine.setCurrentState(new FireMario(stateMachine));
    stateMachine.setScore(stateMachine.getScore() + 300);
  }
  @Override
  public void meetMonster() {
    stateMachine.setCurrentState(new SmallMario(stateMachine));
    stateMachine.setScore(stateMachine.getScore() - 100);
  }
}

// 省略CapeMario、FireMario类...
public class MarioStateMachine {
  private int score;
  private IMario currentState; //不再使用枚举来表示状态
  public MarioStateMachine() {
    this.score = 0;
    this.currentState = new SmallMario(this);
  }
  public void obtainMushRoom() {
    this.currentState.obtainMushRoom();
  }
  public void obtainCape() {
    this.currentState.obtainCape();
  }
  public void obtainFireFlower() {
    this.currentState.obtainFireFlower();
  }
  public void meetMonster() {
    this.currentState.meetMonster();
  }
  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;
  }
}
  • IMario 是状态的接口,定义了所有的事件。SmallMario、SuperMario、CapeMario、FireMario 是 IMario 接口的实现类,分别对应状态机中的4个状态。
  • 原来所有的状态转移和动作执行的代码逻辑,都集中在 MarioStateMachine 类中,现在,这些代码逻辑被分散到了这4个状态类中。
  • MarioStateMachine 和各个状态类之间是双向依赖关系:
    • MarioStateMachine 依赖各个状态类是理所当然的
    • 各个状态类需要更新 MarioStateMachine 中的两个变量,score 和 currentState。

代码优化

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(IMariocurrentState) {
    this.currentState = currentState;
  }
}
  • 我们可以将状态类设计成单例,毕竟状态类中不包含任何成员变量。
  • 但是,当将状态类设计成单例之后,我们就无法通过构造函数来传递 MarioStateMachine 了,而状态类又要依赖 MarioStateMachine,我们可以通过函数参数将 MarioStateMachine 传递进状态类。

总结

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值