一文搞懂状态模式

2023-10-27

原理

状态机有三个组成部分:状态、事件、动作。遇到不同的事件会触发状态的转移和动作的执行,不过动作不是必须的,可能只有状态的转移,没有动作的执行

状态模式的目的就是实现状态机

案例带入

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

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

为了便于讲解,对游戏背景做了简化,只保留了部分状态和事件。简化之后的状态转移如下图所示
在这里插入图片描述
如何实现这个状态转移图呢?如果把这个状态转移翻译成代码呢?我们有以下三种实现方式:
分支逻辑法
查表法
状态模式

分支逻辑法

最简单直接的实现方式是,参照状态转移图,将每一个状态转移,原模原样地直译成代码。
这样编写的代码会包含大量的 if-else 或 switch-case 分支判断逻辑

const (
	SmallState = iota
	SuperState
	CapeState
	FireState
)

type MarioStateMachine struct {
	state int
	score int
}

func (m *MarioStateMachine) MeetMushRoom() {
	if m.state == SmallState {
		m.state = SuperState
		m.score += 100
	}
}

func (m *MarioStateMachine) MeetCape() {
	if m.state == SmallState || m.state == SuperState {
		m.state = CapeState
		m.score += 200
	}
}

func (m *MarioStateMachine) MeetFire() {
	if m.state == SmallState || m.state == SuperState {
		m.state = FireState
		m.score += 300
	}
}

func (m *MarioStateMachine) MeetMonster() {
	if m.state == SuperState {
		m.score -= 100
	} else if m.state == CapeState {
		m.score -= 200
	} else if m.state == FireState {
		m.score -= 300
	}
	m.state = SmallState
}

对于简单的状态机来说,分支逻辑这种实现方式是可以接受的。但是,对于复杂的状态机来说,这种实现方式极易漏写或者错写
某个状态转移

代码中充斥着大量的 if- else 或者 switch-case 分支判断逻辑,可读性和可维护性都很差。如果哪天修改了状态机中的某个状态转移,我们要在冗长的分支逻辑中找到对应的代码进行修改,很容易改错,引入 bug。

查表法

我们可以维护一个二维表,一维表示当前状态,二维表示事件,值就是当前状态遇到事件后的状态以及执行的动作。
通过二维表表示整个状态转移,代码更加清晰,可读性和可维护性更好。当修改状态机时,我们只需要修改stateTable 和 actionTable 两个二维数组即可。实际上,如果我们把这两个二维数组存储在配置文件中,当需要修改状态机时,我们甚至可以不修改任何代码,只需要修改配置文件就可以了。

const (
	MeetMushRoomEvent = iota
	MeetCapeEvent
	MeetFireEvent
	MeetMonsterEvent
)

type MarioStateMachineV2 struct {
	state       int
	score       int
	stateTable  [][]int //状态二维数组,值是转移后的状态
	actionTable [][]int //动作二维数组,值是执行的动作
}

func NewMarioStateMachineV2() *MarioStateMachineV2 {
	m := new(MarioStateMachineV2)
	m.stateTable = [][]int{
		{SuperState, CapeState, FireState, SmallState},
		{SuperState, CapeState, FireState, SmallState},
		{CapeState, CapeState, CapeState, SmallState},
		{FireState, FireState, FireState, SmallState},
	}
	m.actionTable = [][]int{
		{+100, +200, +300, 0},
		{0, +200, +300, -100},
		{0, 0, 0, -200},
		{0, 0, 0, -300},
	}
	return m
}

func (m *MarioStateMachineV2) MeetMushRoom() {
	m.meetEvent(MeetMushRoomEvent)
}

func (m *MarioStateMachineV2) MeetCape() {
	m.meetEvent(MeetCapeEvent)
}

func (m *MarioStateMachineV2) MeetFire() {
	m.meetEvent(MeetFireEvent)
}

func (m *MarioStateMachineV2) MeetMonster() {
	m.meetEvent(MeetMonsterEvent)
}

func (m *MarioStateMachineV2) meetEvent(event int) {
	m.state = m.stateTable[m.state][event]
	m.score += m.actionTable[m.state][event]
}

状态模式

查表法的局限性在于只能表示简单的动作,例如积分的加减值。但是,如果要执行的动作并非这么简单,而是一系列复杂的逻辑操作(比如加减积分、写数据库,还有可能发送消息通知等等),我们就没法用如此简单的二维数组来表示了。

虽然分支逻辑的实现方式不存在这个问题,但它又存在前面讲到的其他问题,比如分支判断逻辑较多,导致代码可读性和可维护性不好等,那么既想要清晰的表达状态转移,又想支持复杂的操作,我们就可以采用状态模式
状态模式通过将事件触发的状态转移和动作执行,拆分到不同的状态类中,来避免分支判断逻辑

type MarioStateMachineV3 struct {
	marioState IMarioState
	state      int
	score      int
}

type IMarioState interface {
	MeetRushRoom()
	MeetCape()
	MeetFire()
	MeetMonster()
}

type ParentMarioState struct {
	stateChine *MarioStateMachineV3
}

func (s *ParentMarioState) MeetRushRoom() {}

func (s *ParentMarioState) MeetCape() {}

func (s *ParentMarioState) MeetFire() {}

func (s *ParentMarioState) MeetMonster() {}

type SmallMarioState struct {
	ParentMarioState
}

func (s *SmallMarioState) MeetRushRoom() {
	s.stateChine.state = SuperState
	s.stateChine.score += 100
}

func (s *SmallMarioState) MeetCape() {
	s.stateChine.state = CapeState
	s.stateChine.score += 200
}

func (s *SmallMarioState) MeetFire() {
	s.stateChine.state = FireState
	s.stateChine.score += 300
}

type SuperMarioState struct {
	ParentMarioState
}

func (s *SuperMarioState) MeetCape() {
	s.stateChine.state = CapeState
	s.stateChine.score += 200
}

func (s *SuperMarioState) MeetFire() {
	s.stateChine.state = FireState
	s.stateChine.score += 300
}

func (s *SuperMarioState) MeetMonster() {
	s.stateChine.state = SmallState
	s.stateChine.score -= 100
}

type CapeMarioState struct {
	ParentMarioState
}

func (s *CapeMarioState) MeetMonster() {
	s.stateChine.state = SmallState
	s.stateChine.score -= 200
}

type FireMarioState struct {
	ParentMarioState
}

func (s *FireMarioState) MeetMonster() {
	s.stateChine.state = SmallState
	s.stateChine.score -= 300
}

状态模式的缺点:需要维护多个状态类,类的个数会比较多,可读性会下降

总结

三种实现方式的选择:

  1. 如果状态转移不复杂,并且不存在扩展的情况,那么可以使用分值逻辑法
  2. 如果事件触发的动作很简单,那么可以使用查表法
  3. 如果状态转移复杂并且事件触发的动作也比较复杂,那么可以使用状态模式
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

一文搞懂状态模式 的相关文章

  • go build 不断抱怨:go.mod 有 post-v0 模块路径

    Go 1 11 发布后 我一直在尝试将我的存储库移动到 Go 模块 方法是添加go mod文件在其根目录下 我的根库之一my host root其版本为17 0 1 所以我在其中写道go mod file module my host ro
  • Goroutine 是如何工作的? (或者:goroutines 和操作系统线程的关系)

    其他 goroutine 如何在调用系统调用时继续执行 当使用 GOMAXPROCS 1 时 据我所知 当调用系统调用时 线程会放弃控制权 直到系统调用返回 Go 如何在不为每个阻塞系统调用 goroutine 创建系统线程的情况下实现这种
  • 命名和未命名类型

    问题 我最近开始阅读Golang规格手册 https golang org ref spec并陷入试图理解的困境有名和无名类型在相关部分 https golang org ref spec Types 我来自动态语言 这让我有点头疼 手册指
  • 模块路径格式错误...第一个路径元素中缺少点

    我有一个包含 2 个不同可执行文件的项目 每个可执行文件都有自己的依赖项以及对根的共享依赖项 如下所示 Root gt server gt main go gt someOtherFiles go gt go mod gt go sum g
  • Go 编程 - 使用指针绕过访问权限

    假设我的项目有以下层次结构 fragment fragment go main go 并且在fragment go我有以下代码 只有一个 getter 没有 setter package fragment type Fragment str
  • 无法封送,(实现encoding.BinaryMarshaler)。具有多个对象的 go-redis Sdd

    我有下面一段代码 我试图将一个数组添加到 redis 集中 但它给了我一个错误 package main import encoding json fmt github com go redis redis type Info struct
  • 如何使用 golang 和 mgo 库在 mongodb 中创建文本索引?

    我正在尝试对集合进行全文搜索 但为了做到这一点 我需要创建一个文本索引 http docs mongodb org manual tutorial create text index on multiple fields http docs
  • go 中的属性更改通知

    如何在 go 中向多个接收器发出 属性 更改信号 类似于在 Qt 中使用通知信号定义属性的方式 例如 如果您想象有一些值需要以多种方式显示 例如进度值可以同时显示为进度条和文本 当基础值发生变化时 两者都需要更新 一种方法可能是利用chan
  • 无法理解 5.6.1。注意事项:捕获迭代变量

    我正在学习 Go 但无法理解 var rmdirs func for dir range tempDirs os MkdirAll dir 0755 rmdirs append rmdirs func os RemoveAll dir NO
  • Golang Appengine 项目无法构建

    我有一个使用 golang 的应用程序引擎项目 我已经大约一年没有碰过了 我现在无法让它在之前构建的机器上构建 我收到以下错误 go app builder 解析输入失败 解析器 src golang org x net internal
  • 如何在 Go 中表示可选字符串?

    我希望建模一个可以有两种可能形式的值 不存在或字符串 执行此操作的自然方法是Maybe String or Optional
  • 取消用户特定的 goroutine [关闭]

    Closed 这个问题需要多问focused help closed questions 目前不接受答案 我有一个应用程序 网络应用程序 允许用户使用 twitter oauth 登录并提供自动推文删除功能 用户登录到 Web 应用程序后
  • 共享来自单独命令/进程的属性

    我提供带有多个命令和子命令的命令行工具 我使用cobra https github com spf13 cobra命令行 我有两个单独的命令首先是前提条件e 给其他人 例如第一个命令是通过创建临时文件夹并验证某些文件来首选环境 第二个命令应
  • 为什么我的 SQL 占位符没有被替换(使用 Go pq)?

    根据文档 我正在这样做 var thingname string asdf var id int err database QueryRow SELECT id from things where thing thingname Scan
  • 无法通过键获取 Gorilla 会话值

    我无法通过这种方式从会话中获取价值 它是nil session initSession r valWithOutType session Values key 完整代码 package main import fmt github com
  • 按引用或按值扫描功能

    我有以下代码 statement SELECT id from source where mgmt 1 var exists string errUnique dr db QueryRow statement mgmt Scan exist
  • nsq 无法通过连接到 nsqlookupd 来消费消息

    我尝试使用 docker compose 来运行 nsq docker compose yml如下 version 3 services nsqlookupd image nsqio nsq command nsqlookupd ports
  • 如何在C#中执行Go函数

    有没有办法从 C 执行 Go 函数 例如 对于 Python 我会使用 Ironpython 我知道我可以生成一个进程来执行 Go 脚本 但如果可能的话 我真的不想回退到这样的解决方案 Google 搜索没有显示任何内容 那么有什么方法可以
  • 在处理程序之后访问 HTTP 请求上下文

    在我的日志记录中间件 链中的第一个 中 我需要访问一些在链下游的某些身份验证中间件中编写的上下文 并且仅在处理程序本身执行之后 旁注 需要首先调用日志记录中间件 因为我需要记录请求的持续时间 包括在中间件中花费的时间 此外 当权限不足时 身
  • 重新插入通道导致死锁

    我有稳定的入站 作业 流 将其输入到无缓冲通道中 我有一个for range循环来迭代项目并处理它们 如果处理该项目失败 我会将项目重新插入通道中 以便稍后重试 问题是当我将项目重新插入通道时 它陷入僵局 我明白为什么会发生这种情况 处理器

随机推荐

  • 自制脚本语言(12) 作用域与符号表

    摘要 介绍了自制语言的编译器对符号表的处理 YF语言中 符号表的基本结构是hash表 每个AST 附带了3个hash表 变量表 类型表 函数表 例如
  • python Django项目点击run或debug时出现Type ‘manage.py help <subcommand>‘ for help on a specific subcommand.

    报错 D python3 7 python exe E code dailyfresh test1 test2 manage py Type manage py help
  • python requests cookies怎么转为_如何将requests.RequestsCookieJar转换为字符串

    新答案 好吧 所以我还是不知道你到底想达到什么目的 如果您想从requests RequestCookieJar对象中提取原始url 这样您就可以检查是否与给定的子域匹配 这是 据我所知 不可能的 不过 你也可以做些类似的事情 usr bi
  • Linux-线程的同步与互斥

    线程的同步与互斥 进程 线程间的互斥相关背景概念 互斥量 互斥量接口 互斥量的初始化 互斥量的销毁 加锁和解锁 改善抢票系统 互斥量原理 可重入与线程安全 重入和线程安全的概念 常见线程不安全情况 常见线程安全的情况 常见不可重入情况 常见
  • 【软件工程】-可行性研究报告

    GB8567 88 可行性研究报告 1引言 1 1编写目的 为了提高机房收费管理的灵活性和效率 减轻机房工作人员的工作负担 节约时间 对机房收费业务做到快速准确管理的目的 从而降低人力 经济的更各方面的消耗 本次编写主要是为了分析廊坊师范学
  • 电机速度曲线规划1:梯形速度曲线设计与实现

    电机驱动是很常见的应用 在很多系统中我们都会碰到需要改变电机的速度以实现相应的控制功能 这就涉及到电机速度曲线规划的问题 在这篇中我们就来简单讨论一下电机的梯形曲线规划的问题 1 基本原理 梯形速度曲线控制算法是工业控制领域应用最为广泛的加
  • 在vc下环境变量的设置

    Error spawning cl exe 编译出错 有人说是没有设置 include环境变量 下面介绍在vc下如何设置环境变量 1 Microsoft Visual Studio下面3个子文件夹 Common VC98 My Projec
  • 1.嵌入式控制器EC学习,编译环境搭建

    工欲善其事 必先利其器 在学习EC相关知识之前 首先需要完成EC代码编译环境的搭建 需要如下内容 Keil C51 用于EC中C代码的编译器环境 EC源代码 我们使用从网上可以下载到的 ITE V12 4 Update 版的代码为例进行学习
  • JavaBean,List,Map转成json格式

    普通JavaBean 以User为例 转成json格式 1 转成JSONArray类型 User user new User user setUsername cxl user setPassword 1234 JSONArray json
  • GORM 基础 -- Gen

    https gorm io gen github 1 GEN Guides GEN 友好和更安全的代码生成 1 1 概述 来自动态原始SQL的惯用和可重用API 100 类型安全的DAO API 不使用 interface Database
  • printf(“%d,%d\n“,i--,i++)

    sample cpp include
  • Windows 下创建定时任务执行Python脚本

    文章目录 一 环境 二 脚本 三 创建定时任务 1 打开 任务计划程序 2 打开 创建任务 窗口 3 创建任务一一常规 4 创建任务一一触发器 5 创建任务一一操作 6 创建任务一一条件 7 创建任务一一设置 8 完成任务创建 四 验证定时
  • 记录自己在结构光三维重建领域的学习过程(一)

    仿真数据集与真是数据集之间差异较大 二者的网络均不可完美预测另一种数据 寻找解决办法 首先确定是不是数据的问题 阅读论文 Light field structured light projection data generation wit
  • 关于存储过程中SQL语句IN条件传参注意说

    背景说明 在数据库操作中我们经常会用到查询语句 在一些情况下 需要使用到IN条件 正常的查询中IN需要注意的是最好in中的参数不能超过1000个 超过1000的时候oracle会抛出异常 这个如何处理先不提 这次要说的是 如果在存储过程中使
  • 某单位分配到一个地址块 136.23.12.64/26。现在需要进一步划分为4个一样大的子网。试问:

    1 每个子网的网络前缀有多长 2 每个子网中有多少个地址 3 每个子网的地址块是什么 4 每一个子网可分配给主机使用的最小地址和最大地址是什么 姐
  • JS中的邮箱验证

    通过js在前端对用户输入进行校验 即可以产生较好的交互体验 也可以减轻后台的压力 邮箱的基本格式要求 1 只能以单词字符开头 即a z A Z 0 9 2 只能有一个 3 后面有一个到多个点 并且点不能在最后 4 特殊字符不能开头和结尾 使
  • 数据存储,详细讲解

    数据存储 详细讲解 数据类型的介绍 整形的内存存储 大小端介绍 浮点数的存储 数据类型的介绍 1 内置类型 char 字符数据类型 1 short 短整型 2 int 整形 4 long 长整型 4 8 long long 更长的整形 8
  • matlab之数组反序输出

    a 1 2 3 4 5 a end 1 1 5 4 3 2 1 转载于 https www cnblogs com yibeimingyue p 11201805 html
  • 高阶数据结构之并查集

    文章目录 并查集 并查集的常规实现 并查集的简化实现 算法题 模板 朴素的并查集 维护size的并查集 维护到祖宗节点的并查集 并查集 在某些应用问题中 需要将n个不同的元素划分成一些不想交的集合 开始时 每个元素自成一个单元集合 然后按照
  • 一文搞懂状态模式

    原理 状态机有三个组成部分 状态 事件 动作 遇到不同的事件会触发状态的转移和动作的执行 不过动作不是必须的 可能只有状态的转移 没有动作的执行 状态模式的目的就是实现状态机 案例带入 比如 超级马里奥 在游戏中 马里奥可以变身为多种形态