2025年状态设计模式

状态设计模式目录 前言 状态机原理 状态机的实现方式一 分支逻辑法 状态机实现方式二 查表法 状态机实现方式三 状态模式 总结 参考资料 前言 在实际的软件开发中 状态模式并不是很常见 但是在能够用到的场景里

大家好,我是讯享网,很高兴认识大家。

目录

前言:

 状态机原理

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

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

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

总结:

参考资料


前言:

      在实际的软件开发中,状态模式并不是很常见,但是在能够用到的场景里,可以发挥比较大的作用,状态模式一般用来实现状态机,而状态机常用在游戏、工作引擎等系统开发中,不过,状态机的实现方式有很多种,除了状态模式,比较常见的还有分支逻辑法和查表法。


 状态机原理

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

 结合一个具体的案例进行详细解释:

超级马里奥游戏不知道你玩过没有?在游戏中,马里奥可以变身为多种形态,比如小马里奥(Small Mario)、超级马里奥(Super Mario)、火焰马里奥(Fire Mario)、斗篷马里奥(Cape Mario)等等。在不同的游戏情节下,各个形态会互相转化,并相应的增减积分。比如,初始形态是小马里奥,吃了蘑菇之后就会变成超级马里奥,并且增加 100 积分。

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

用代码实现如下:

 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() { //TODO } public void obtainCape() { //TODO } public void obtainFireFlower() { //TODO } public void meetMonster() { //TODO } 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 switch-case 分支判断逻辑,甚至是嵌套的分支判断逻辑,所以,我把这种方法暂且命名为分支逻辑法。

讯享网 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; } }

对于简单的状态机来说,分支逻辑这种实现方式是可以接受的。但是,对于复杂的状态机来说,这种实现方式极易漏写或者错写某个状态转移。除此之外,代码中充斥着大量的 if-else 或者 switch-case 分支判断逻辑,可读性和可维护性都很差。如果哪天修改了状态机中的某个状态转移,我们要在冗长的分支逻辑中找到对应的代码进行修改,很容易改错,引入 bug 


讯享网

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

 实际上,上面这种实现方法有点类似 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; } }

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

     在查表法的代码实现中,事件触发的动作只是简单的积分加减,所以,我们用一个 int 类型的二维数组 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; } }

上面的代码实现不难看懂,我只强调其中的一点,即 MarioStateMachine 和各个状态类之间是双向依赖关系。MarioStateMachine 依赖各个状态类是理所当然的,但是,反过来,各个状态类为什么要依赖 MarioStateMachine 呢?这是因为,各个状态类需要更新 MarioStateMachine 中的两个变量,score currentState。实际上,上面的代码还可以继续优化,我们可以将状态类设计成单例,毕竟状态类中不包含任何成员变量。但是,当将状态类设计成单例之后,我们就无法通过构造函数来传递 MarioStateMachine 了,而状态类又要依赖 MarioStateMachine,那该如何解决这个问题呢?

我们可以通过函数参数将 MarioStateMachine 传递进状态类。

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

总结:

状态模式是状态机的一种实现方式即可。状态机又叫有限状态机,它有 3 个部分组成:状态、事件、动作。其中,事件也称为转移条件。事件触发状态的转移及动作的执行。不过,动作不是必须的,也可能只转移状态,不执行任何动作。

状态机的三种实现方式

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

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

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

参考资料

64 | 状态模式:游戏、工作流引擎中常用的状态机是如何实现的?-极客时间 

小讯
上一篇 2025-02-08 16:51
下一篇 2025-02-11 19:29

相关推荐

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请联系我们,一经查实,本站将立刻删除。
如需转载请保留出处:https://51itzy.com/kjqy/65060.html