一文搞懂状态模式

原理

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

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

案例带入

比如"超级马里奥",在游戏中,马里奥可以变身为多种形态,比如小马里奥(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. 如果状态转移复杂并且事件触发的动作也比较复杂,那么可以使用状态模式
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值