设计模式-状态模式 State

news/2025/3/12 12:18:48/

状态模式

  • 一、简介概述
  • 二、有限状态机
    • 1、分支法
    • 2、查表法
    • 3、状态模式
  • 三、重点回顾

一、简介概述

状态模式并不是很常用,但是在能够用到的场景里,它可以发挥很大的作用。

状态设计模式是一种行为型设计模式,它允许对象在其内部状态发生变化时改变其行为。这种模式可以消除大量的条件语句,并将每个状态的行为封装到单独的类中。

状态模式的主要组成部分如下:

  1. 上下文(Context):上下文通常包含一个具体状态的引用,用于维护当前状态。上下文委托给当前状态对象处理状态相关行为。
  2. 抽象状态(State):定义一个接口,用于封装与上下文的特定状态相关的行为。
  3. 具体状态(Concrete State):实现抽象状态接口,为具体状态定义行为。每个具体状态类对应一个状态。

现在来看一个简单的 Java 示例。假设要模拟一个简易的电视遥控器,具有开启、关闭和调整音量的功能。

如果不使用设计模式,编写出来的代码可能是这个样子的,我们需要针对电视机当前的状态为每一次操作编写判断逻辑:

public class TV {private boolean isOn;private int volume;public TV() {isOn = false;volume = 0;}public void turnOn() {// 如果是开启状态if (isOn) {System.out.println("TV is already on.");// 否则打开电视} else {isOn = true;System.out.println("Turning on the TV.");}}public void turnOff() {if (isOn) {isOn = false;System.out.println("Turning off the TV.");} else {System.out.println("TV is already off.");}}public void adjustVolume(int volume) {if (isOn) {this.volume = volume;System.out.println("Adjusting volume to: " + volume);} else {System.out.println("Cannot adjust volume, TV is off.");}}
}public class Main {public static void main(String[] args) {TV tv = new TV();tv.turnOn();tv.adjustVolume(10);tv.turnOff();}
}

当然在该例子中状态比较少,所以代码看起来也不是很复杂,但是状态如果变多了呢?比如加入换台,快捷键、静音等功能后呢?你会发现条件分支会急速膨胀,所以此时状态设计模式就要登场了

首先,定义抽象状态接口 TVState,将每一个修改状态的动作抽象成一个接口:

public interface TVState {void turnOn();void turnOff();void adjustVolume(int volume);
}

接下来,为每个具体状态创建类,实现 TVState 接口。例如,创建 TVOnStateTVOffState 类:

// 在on状态下,去执行以下各种操作
public class TVOnState implements TVState {@Overridepublic void turnOn() {System.out.println("TV is already on.");}@Overridepublic void turnOff() {System.out.println("Turning off the TV.");}@Overridepublic void adjustVolume(int volume) {System.out.println("Adjusting volume to: " + volume);}
}// 在关机的状态下执行以下的操作
public class TVOffState implements TVState {@Overridepublic void turnOn() {System.out.println("Turning on the TV.");}@Overridepublic void turnOff() {System.out.println("TV is already off.");}@Overridepublic void adjustVolume(int volume) {System.out.println("Cannot adjust volume, TV is off.");}
}

接下来,定义上下文类 TV

public class TV {// 当前状态private TVState state;public TV() {state = new TVOffState();}public void setState(TVState state) {this.state = state;}public void turnOn() {// 打开state.turnOn();// 设置为开机状态setState(new TVOnState());}public void turnOff() {// 关闭state.turnOff();// 设置为关机状态setState(new TVOffState());}public void adjustVolume(int volume) {state.adjustVolume(volume);}
}

最后,我们可以通过以下方式使用这些类:

public class Main {public static void main(String[] args) {TV tv = new TV();tv.turnOn();tv.adjustVolume(10);tv.turnOff();}
}

这个例子展示了状态模式的基本结构和用法。通过使用状态模式,我们可以更好地组织和管理与特定状态相关的代码。当状态较多时,这种模式的优势就会凸显出来,同时我们在代码时,因为我们会对每个状态进行独立封装,所以也会简化代码编写。

二、有限状态机

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

有限状态机,英文翻译是 Finite State Machine,缩写为 FSM,简称为状态机,比较官方的说法是:有限状态机是描述对象在它的生命周期内所经历的状态序列,以及如何响应来自外界的各种事件。。状态机有 3 个组成部分:状态(State)、事件(Event)、动作(Action)。其中,事件也称为转移条件(Transition Condition)。事件触发状态的转移及动作的执行。不过,动作不是必须的,也可能只转移状态,不执行任何动作。

“超级马里奥”:在游戏中,马里奥可以变身为三种形态,小马里奥(Small Mario)、大马里奥(Big Mario)和火焰马里奥(Fire Mario)。马里奥可以通过吃蘑菇(Mushroom)、火花(Fire Flower)或被敌人攻击(Enemy Attack)来改变形态。我们将用状态图表示这个马里奥的有限状态机。

可以根据这个有限状态机来实现一个马里奥游戏的简化版本。在实际游戏开发中,通常会使用游戏引擎或编程框架来处理状态转换,而不是手动编写状态机代码。不过,这个简化示例可以帮助理解有限状态机在马里奥游戏中的应用。

1、分支法

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

按照这个实现思路,我将上面的骨架代码补全一下。补全之后的代码如下所示:

下面是一个使用if-else 语句实现的马里奥形态变化的代码示例:

public class Mario {private MarioState marioState;public Mario() {this.marioState = MarioState.SMALL;}public void handleEvent(MarioEvent event) {if (marioState == MarioState.DEAD) {return;}// 不同状态下吃蘑菇会有什么反应if (event == MarioEvent.MUSHROOM) {// 只有小马里奥吃蘑菇才会变大if (marioState == MarioState.SMALL) {marioState = MarioState.BIG;}} else if (event == MarioEvent.FIRE_FLOWER) {// 只有大马里奥吃花火会变火if (marioState == MarioState.BIG) {marioState = MarioState.FIRE;}} else if (event == MarioEvent.ENEMY_ATTACK) {if (marioState == MarioState.SMALL) {marioState = MarioState.DEAD;} else if (marioState == MarioState.BIG || marioState == MarioState.FIRE) {marioState = MarioState.SMALL;}} else if (event == MarioEvent.FALL_INTO_PIT) {marioState = MarioState.DEAD;}}public static void main(String[] args) {Mario mario = new Mario();mario.handleEvent(MarioEvent.MUSHROOM);mario.handleEvent(MarioEvent.FIRE_FLOWER);mario.handleEvent(MarioEvent.ENEMY_ATTACK);mario.handleEvent(MarioEvent.FALL_INTO_PIT);System.out.println(mario.marioState);}
}

在这个示例中,我们使用 if-else 语句来处理状态转换。在 handleEvent 方法中,我们根据事件和当前状态的组合来确定新状态,并更新马里奥的状态。这种实现方法相较于查表法和面向对象实现更为简单,但可能在状态和事件更多的情况下变得难以维护。选择合适的实现方法取决于实际需求和场景。

2、查表法

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

我们可以将马里奥的状态转移方式表示为以下表格:

当前状态/事件MUSHROOMFIRE_FLOWERENEMY_ATTACKFALL_INTO_PIT
SMALLBIGSMALLDEADDEAD
BIGBIGFIRESMALLDEAD
FIREFIREFIREBIGDEAD
DEADDEADDEADDEADDEAD

这个表格显示了马里奥在不同状态下遇到不同事件时将转换为的新状态。从左到右分别表示当前状态(SMALL, BIG, FIRE, DEAD),从上到下表示事件(MUSHROOM, FIRE_FLOWER, ENEMY_ATTACK, FALL_INTO_PIT)。表格中的每个单元格表示对应状态和事件的状态转换结果。

查表法是一种使用查找表来处理状态转换的方法,可以简化状态机的实现。以下是使用查表法实现马里奥形态变化的代码示例:

enum MarioState {SMALL, BIG, FIRE, DEAD
}enum Event {MUSHROOM, FIRE_FLOWER, ENEMY_ATTACK, FALL_INTO_PIT
}public class Mario {private MarioState state;// 使用二维数组定义状态转换表private static final MarioState[][] TRANSITION_TABLE = {// SMALL, BIG, FIRE, DEAD{MarioState.BIG, MarioState.BIG, MarioState.FIRE, MarioState.DEAD}, // MUSHROOM{MarioState.SMALL, MarioState.FIRE, MarioState.FIRE, MarioState.DEAD}, // FIRE_FLOWER{MarioState.DEAD, MarioState.SMALL, MarioState.BIG, MarioState.DEAD}, // ENEMY_ATTACK{MarioState.DEAD, MarioState.DEAD, MarioState.DEAD, MarioState.DEAD}  // FALL_INTO_PIT};public Mario() {state = MarioState.SMALL;}public void handleEvent(Event event) {// 使用查表法获取状态转换后的新状态MarioState newState = TRANSITION_TABLE[event.ordinal()][state.ordinal()];// 打印状态转换信息System.out.printf("从 %s 变为 %s%n", state, newState);// 更新状态state = newState;}
}public class MarioDemo {public static void main(String[] args) {Mario mario = new Mario();mario.handleEvent(Event.MUSHROOM); // 变为大马里奥mario.handleEvent(Event.FIRE_FLOWER); // 变为火焰马里奥mario.handleEvent(Event.FALL_INTO_PIT); // 变为死亡马里奥}
}

在这个示例中,使用了一个二维数组 TRANSITION_TABLE 来表示状态转换表。数组的行表示事件,列表示马里奥的当前状态,数组的元素表示新状态。通过查找表,我们可以直接获取状态转换后的新状态,从而简化状态机的实现。

handleEvent 方法中,我们根据事件和当前状态的序数来查找新状态,并更新马里奥的状态。这个查表法实现的有限状态机相比之前的面向对象实现更为简洁,但可能不适用于需要处理复杂事件或动作的场景。根据实际需求选择合适的实现方法是很重要的。

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

3、状态模式

在查表法的代码实现中,事件触发的动作只是简单的状态或者数值,所以,用一个 MarioState类型的二维数组 TRANSITION_TABLE 就能表示,二维数组中的值表示出发事件后的新状态。但是,如果要执行的动作并非这么简单,而是一系列复杂的逻辑操作(比如加减分数、处理位置信息等等),就没法用如此简单的二维数组来表示了。这也就是说,查表法的实现方式有一定局限性

虽然分支逻辑的实现方式不存在这个问题,但它又存在前面讲到的其他问题,比如分支判断逻辑较多,导致代码可读性和可维护性不好等。实际上,针对分支逻辑法存在的问题,我们可以使用状态模式来解决

**状态模式通过将事件触发的状态转移和动作执行,拆分到不同的状态类中,来避免分支判断逻辑。**我们还是结合代码来理解这句话。

利用状态模式,来补全 MarioStateMachine 类,补全后的代码如下所示。

以下是一个使用 Java 实现的简化版马里奥形态变化的案例代码:

/*** 抽象状态*/
public interface MarioStatus {void eatMushroom();void eatFireFlower();void enemyAttack();void fallFit();
}public class MairoSmallState implements MarioStatus {private Mario mario;public MairoSmallState(Mario mario) {this.mario = mario;}@Overridepublic void eatMushroom() {System.out.println("小马里奥吃了蘑菇变大了");// 修改状态mario.setMarioState(new MarioBigState(mario));}@Overridepublic void eatFireFlower() {System.out.println("小马里奥吃了火焰没变化");}@Overridepublic void enemyAttack() {System.out.println("小马里奥遇到敌人,死亡");mario.setMarioState(new MarioDeadState(mario));}@Overridepublic void fallFit() {System.out.println("小马里奥掉坑里,死亡");mario.setMarioState(new MarioDeadState(mario));}
}public class MarioBigState implements MarioStatus{private Mario mario;public MarioBigState(Mario mario) {this.mario = mario;}@Overridepublic void eatMushroom() {System.out.println("大马里奥吃了蘑菇,没变化");}@Overridepublic void eatFireFlower() {System.out.println("小马里奥吃了火焰,变成火焰马里奥");mario.setMarioState(new MarioFireState(mario));}@Overridepublic void enemyAttack() {System.out.println("大马里奥遇到敌人,变成小马里奥");mario.setMarioState(new MairoSmallState(mario));}@Overridepublic void fallFit() {System.out.println("大马里奥掉坑里,死亡");mario.setMarioState(new MarioDeadState(mario));}
}public class MarioDeadState implements MarioStatus{private Mario mario;public MarioDeadState(Mario mario) {this.mario = mario;}@Overridepublic void eatMushroom() {System.out.println("马里奥已死亡");}@Overridepublic void eatFireFlower() {System.out.println("马里奥已死亡");}@Overridepublic void enemyAttack() {System.out.println("马里奥已死亡");}@Overridepublic void fallFit() {System.out.println("马里奥已死亡");}
}public class MarioFireState implements MarioStatus{private Mario mario;public MarioFireState(Mario mario) {this.mario = mario;}@Overridepublic void eatMushroom() {System.out.println("火焰马里奥吃了蘑菇没变化");}@Overridepublic void eatFireFlower() {System.out.println("火焰马里奥吃了火焰没变化");}@Overridepublic void enemyAttack() {System.out.println("火焰马里奥遇到敌人,变成小马里奥");mario.setMarioState(new MairoSmallState(mario));}@Overridepublic void fallFit() {System.out.println("火焰马里奥掉坑里,死亡");mario.setMarioState(new MarioDeadState(mario));}
}@Data
public class Mario {private MarioStatus marioState;public Mario() {this.marioState = new MairoSmallState(this);}public void handEvent(MarioEvent marioEvent) {if (marioEvent == MarioEvent.MUSHROOM) {marioState.eatMushroom();} else if (marioEvent == MarioEvent.FIRE_FLOWER) {marioState.eatFireFlower();} else if (marioEvent == MarioEvent.ENEMY_ATTACK) {marioState.enemyAttack();} else if (marioEvent == MarioEvent.FALL_INTO_PIT) {marioState.fallFit();}}public static void main(String[] args) {Mario mario = new Mario();mario.handEvent(MarioEvent.MUSHROOM);mario.handEvent(MarioEvent.FIRE_FLOWER);mario.handEvent(MarioEvent.ENEMY_ATTACK);mario.handEvent(MarioEvent.FALL_INTO_PIT);}
}

在这个简化示例中,我们定义了 MarioState 接口以及实现了DeadMarioSmallMarioBigMarioFireMario 类,分别表示马里奥的四种形态。每个形态类实现了 handleEvent 方法,用于处理不同的游戏事件并根据有限状态机规则进行状态转换。

Mario 类作为状态的上下文,用于管理和切换马里奥的状态。它有一个 setState 方法,用于更新当前状态。handleEvent 方法将事件传递给当前状态,以便根据事件执行相应的状态转换。

MarioDemo 测试类中,创建了一个 Mario 实例,并通过调用 handleEvent 方法模拟游戏中的事件。通过运行这个测试类,你可以观察到马里奥根据有限状态机的规则在不同形态之间切换。

这个简化示例展示了如何使用有限状态机来实现马里奥角色的形态变化。在实际游戏开发中,可能需要考虑更多的事件和状态,以及与游戏引擎或框架集成的方式。不过,这个示例可以帮助你理解有限状态机在游戏中的应用

三、重点回顾

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

针对状态机,今天我们总结了三种实现方式。

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

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

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

状态模式的代码实现还存在一些问题,比如,状态接口中定义了所有的事件函数,这就导致,即便某个状态类并不需要支持其中的某个或者某些事件,但也要实现所有的事件函数。不仅如此,添加一个事件到状态接口,所有的状态类都要做相应的修改。针对这些问题,你有什么解决方法吗?


http://www.ppmy.cn/news/1349995.html

相关文章

【Kubernetes】kubectl top pod 异常?

目录 前言一、表象二、解决方法1、导入镜像包2、编辑yaml文件3、解决问题 三、优化改造1.修改配置文件2.检查api-server服务是否正常3.测试验证 总结 前言 各位老铁大家好,好久不见,卑微涛目前从事kubernetes相关容器工作,感兴趣的小伙伴相互…

Tdesign 常用知识

Mock数据中的常见随机数: mock 数据中, 开头的是 Mock.js 的语法。Mock.js 是一个用于生成随机数据的库,它提供了一些特殊的语法,可以方便地生成各种类型的随机数据。 在这个 mock 数据中,使用了以下语法&#xff1a…

blender怎么保存窗口布局,怎么设置默认输出文件夹

进行窗口布局大家都会,按照自己喜好来就行了,设置输出文件夹如图 这些其实都简单。关键问题在于,自己调好了窗口布局,或者设置好了输出文件夹之后,怎么能让blender下次启动的时候呈现出自己设置好的窗口布局&#xff…

【doghead】uv_loop_t的创建及线程执行

worker测试程序,类似mediasoup对uv的使用,是one loop per thread 。创建一个UVLoop 就可以创建一个uv_loop_t Transport 创建一个: 试验配置创建一个: UvLoop 封装了libuv的uv_loop_t ,作为共享指针提供 对uv_loop_t 创建并初始化

中科大计网学习记录笔记(九):DNS

前言: 学习视频:中科大郑烇、杨坚全套《计算机网络(自顶向下方法 第7版,James F.Kurose,Keith W.Ross)》课程 该视频是B站非常著名的计网学习视频,但相信很多朋友和我一样在听完前面的部分发现信…

【每日一题】LeetCode——反转链表

📚博客主页:爱敲代码的小杨. ✨专栏:《Java SE语法》 | 《数据结构与算法》 | 《C生万物》 ❤️感谢大家点赞👍🏻收藏⭐评论✍🏻,您的三连就是我持续更新的动力❤️ 🙏小杨水平有…

假如程序员也分等级……

本次考试有10道题,一题10分,做完后可查看段位等级。 1.请问复制粘贴用什么快捷键? A.CtrlV,CtrlC B.CtrlC,CtrlV 2.计算机里用的是( )进制? A.二 B.十 3.对于输出“Hello world”正确的一项是&#…

ChatGPT高效提问—prompt常见用法(续篇八)

ChatGPT高效提问—prompt常见用法(续篇八) 1.1 对抗 ​ 对抗是一个重要主题,深入探讨了大型语言模型(LLM)的安全风险。它不仅反映了人们对LLM可能出现的风险和安全问题的理解,而且能够帮助我们识别这些潜在的风险,并通过切实可行的技术手段来规避。 ​ 截至目前,网络…