go设计模式-状态模式

有限状态机

概念

状态模式是一种行为设计模式,让你能在一个对象的内部状态变化时改变其行为,使其看上去就像改变了自身所属的类一样。

该模式将与状态相关的行为抽取到独立的状态类中,让原对象将工作委派给这些类的实例,而不是自行进行处理。

状态迁移有四个元素组成,起始状态、触发迁移的事件,终止状态以及要执行的动作,每个具体的状态包含触发状态迁移的执行方法,迁移方法的实现是执行持有状态对象的动作方法,同时设置状态为下一个流转状态;持有状态的业务对象包含有触发状态迁移方法,这些迁移方法将请求委托给当前具体状态对象的迁移方法。

有限状态机,英文翻译是 Finite State Machine,缩写为 FSM,简称为状态机。

状态机有 3 个组成部分:状态(State)、事件(Event)、动作(Action)。

其中,事件也称为转移条件(Transition Condition)。

事件触发状态的转移及动作的执行。

不过,动作不是必须的,也可能只转移状态,不执行任何动作。

栗子

在超级马里奥游戏中,马里奥可以变身为多种形态,比如小马里奥(Small Mario)、超级马里奥(Super Mario)、火焰马里奥(Fire Mario)、斗篷马里奥(Cape Mario)等等。

在不同的游戏情节下,各个形态会互相转化,并相应的增减积分。比如,初始形态是小马里奥,吃了蘑菇之后就会变成超级马里奥,并且增加 100 积分。

实际上,马里奥形态的转变就是一个状态机。其中,马里奥的不同形态就是状态机中的“状态”,游戏情节(比如吃了蘑菇)就是状态机中的“事件”,加减积分就是状态机中的“动作”。

比如,吃蘑菇这个事件,会触发状态的转移:从小马里奥转移到超级马里奥,以及触发动作的执行(增加 100 积分)。为了方便接下来的讲解,我对游戏背景做了简化,只保留了部分状态和事件。简化之后的状态转移如下图所示:

请添加图片描述

E1:ObtainMushroom

E2:ObtainFireFlower

E3:ObtainCape

E4:MeetMonster

状态机实现-状态模式

IMario 是状态的接口,定义了所有的事件。

SmallMario、SuperMario、CapeMario、FireMario 是 IMario 接口的实现类,分别对应状态机中的 4 个状态。

原来所有的状态转移和动作执行的代码逻辑,都集中在 MarioStateMachine 类中,现在,这些代码逻辑被分散到了这 4 个状态类中。

代码目录:

请添加图片描述

package FSM

// IMario 状态接口
// 状态分为:smallMario, superMario, capeMario, fireMario, death
// 定义所有事件
// 把 state 传入事件,可以避免每个状态独自维护保存当前状态,全部都只让 marioStateMachine 保存
// 而不用 smallMario... 分别保存 state 状态
type IMario interface {
	GetState() State                             // 获取当前状态
	ObtainMushroom(machine *MarioStateMachine)   // 获得蘑菇
	ObtainCape(machine *MarioStateMachine)       // 获得斗篷
	ObtainFireFlower(machine *MarioStateMachine) // 获得火焰花
	MeetMonster(machine *MarioStateMachine)      // 遇见怪物
}

实现各种状态:

package FSM

// 使用单例模式生成
var (
	capeMario = &CapeMario{}
)

type CapeMario struct {
	stateMachine MarioStateMachine
}

func NewCapeMario() *CapeMario {
	return capeMario
}

func (s *CapeMario) GetState() State {
	return CAPE
}

func (s *CapeMario) ObtainMushroom(stateMachine *MarioStateMachine) {
	stateMachine.SetScore(stateMachine.GetScore() + 100)
}

func (s *CapeMario) ObtainCape(stateMachine *MarioStateMachine) {
	stateMachine.SetCurrentState(NewCapeMario())
	stateMachine.SetScore(stateMachine.GetScore() + 200)
}

func (s *CapeMario) ObtainFireFlower(stateMachine *MarioStateMachine) {
	stateMachine.SetCurrentState(NewFireMario())
	stateMachine.SetScore(stateMachine.GetScore() + 300)
}

func (s *CapeMario) MeetMonster(stateMachine *MarioStateMachine) {
	stateMachine.SetCurrentState(NewSuperMario())
}

package FSM

import "fmt"

var (
	death = &Death{}
)

type Death struct{}

func NewDeath() *Death {
	return death
}

func (s *Death) GetState() State {
	return DEATH
}

func (s *Death) ObtainMushroom(machine *MarioStateMachine) {
	fmt.Println("death ...")
}
func (s *Death) ObtainCape(machine *MarioStateMachine) {
	fmt.Println("death ...")
}
func (s *Death) ObtainFireFlower(machine *MarioStateMachine) {
	fmt.Println("death ...")
}
func (s *Death) MeetMonster(machine *MarioStateMachine) {
	fmt.Println("death ...")
}

package FSM

var (
	fireMario = &FireMario{}
)

type FireMario struct {
	stateMachine MarioStateMachine
}

func NewFireMario() *FireMario {
	return fireMario
}

func (s *FireMario) GetState() State {
	return FIRE
}

func (s *FireMario) ObtainMushroom(stateMachine *MarioStateMachine) {
	stateMachine.SetScore(stateMachine.GetScore() + 100)
}

func (s *FireMario) ObtainCape(stateMachine *MarioStateMachine) {
	stateMachine.SetCurrentState(NewCapeMario())
	stateMachine.SetScore(stateMachine.GetScore() + 200)
}

func (s *FireMario) ObtainFireFlower(stateMachine *MarioStateMachine) {
	stateMachine.SetScore(stateMachine.GetScore() + 300)
}

func (s *FireMario) MeetMonster(stateMachine *MarioStateMachine) {
	stateMachine.SetCurrentState(NewSuperMario())
}

package FSM

var (
	smallMario = &SmallMario{}
)

type SmallMario struct{}

func NewSmallMario() *SmallMario {
	return smallMario
}

func (s *SmallMario) GetState() State {
	return SMALL
}

func (s *SmallMario) ObtainMushroom(stateMachine *MarioStateMachine) {
	stateMachine.SetCurrentState(NewSuperMario())
	stateMachine.SetScore(stateMachine.GetScore() + 100)
}

func (s *SmallMario) ObtainCape(stateMachine *MarioStateMachine) {
	stateMachine.SetCurrentState(NewCapeMario())
	stateMachine.SetScore(stateMachine.GetScore() + 200)
}

func (s *SmallMario) ObtainFireFlower(stateMachine *MarioStateMachine) {
	stateMachine.SetCurrentState(NewFireMario())
	stateMachine.SetScore(stateMachine.GetScore() + 300)
}

func (s *SmallMario) MeetMonster(stateMachine *MarioStateMachine) {
	stateMachine.SetCurrentState(NewDeath())
}

package FSM

var (
	superMario = &SuperMario{}
)

type SuperMario struct {
	stateMachine MarioStateMachine
}

func NewSuperMario() *SuperMario {
	return superMario
}

func (s *SuperMario) GetState() State {
	return SUPER
}

func (s *SuperMario) ObtainMushroom(stateMachine *MarioStateMachine) {
	stateMachine.SetScore(stateMachine.GetScore() + 100)
}

func (s *SuperMario) ObtainCape(stateMachine *MarioStateMachine) {
	stateMachine.SetCurrentState(NewCapeMario())
	stateMachine.SetScore(stateMachine.GetScore() + 200)
}

func (s *SuperMario) ObtainFireFlower(stateMachine *MarioStateMachine) {
	stateMachine.SetCurrentState(NewFireMario())
	stateMachine.SetScore(stateMachine.GetScore() + 300)
}

func (s *SuperMario) MeetMonster(stateMachine *MarioStateMachine) {
	stateMachine.SetCurrentState(NewSmallMario())
}

实现 marioStateMachine

package FSM

type State = int
type StateName = string

const (
	SMALL = iota
	SUPER
	FIRE
	CAPE
	DEATH
)

var (
	stateMap = map[State]StateName{SMALL: "SMALL", SUPER: "SUPER", FIRE: "FIRE", CAPE: "CAPE", DEATH: "DEATH"}
)

type MarioStateMachine struct {
	score        int
	currentState IMario
}

func NewMarioStateMachine() *MarioStateMachine {
	// 初始状态
	return &MarioStateMachine{
		score:        0,               //当前分数
		currentState: NewSmallMario(), //当前状态
	}
}

func (m *MarioStateMachine) ObtainMushroom() {
	m.currentState.ObtainMushroom(m)
}

func (m *MarioStateMachine) ObtainCape() {
	m.currentState.ObtainCape(m)
}

func (m *MarioStateMachine) ObtainFireFlower() {
	m.currentState.ObtainFireFlower(m)
}

func (m *MarioStateMachine) MeetMonster() {
	m.currentState.MeetMonster(m)
}

func (m *MarioStateMachine) GetScore() int {
	return m.score
}

func (m *MarioStateMachine) SetScore(score int) {
	m.score = score
}

func (m *MarioStateMachine) GetCurrentState() StateName {
	return stateMap[m.currentState.GetState()]
}

func (m *MarioStateMachine) SetCurrentState(currentState IMario) {
	m.currentState = currentState
}

测试

package FSM

import (
	"testing"
)

func TestNewMarioStateMachine(t *testing.T) {
	mario := NewMarioStateMachine()
	mario.ObtainMushroom()
	t.Log(mario.GetCurrentState()) // SUPER
	mario.ObtainFireFlower()
	t.Log(mario.GetCurrentState()) // FIRE
	mario.MeetMonster()
	t.Log(mario.GetCurrentState()) // SUPER
	mario.MeetMonster()
	t.Log(mario.GetCurrentState()) // SMALL
	mario.MeetMonster()
	t.Log(mario.GetCurrentState()) // DEATH
	mario.ObtainCape()
	t.Log(mario.GetScore())        // 400
	t.Log(mario.GetCurrentState()) // DEATH
}

状态机实现-查表法

实际上,除了用状态转移图来表示之外,状态机还可以用二维表来表示,如下所示。

在这个二维表中,第一维表示当前状态,第二维表示事件,值表示当前状态经过事件之后,转移到的新状态及其执行的动作。

E1(obtain mushroom)E2(obtain fire flower)E3(obtain cape)E4(meet monster)
SmallSuper/+100Fire/+200Cape/+300Death
SuperSuper/+100Fire/+200Cape/+300Small
CapeCape/+100Fire/+200Cape/+300Small
FireFire/+100Fire/+200Cape/+300Small
DeathDeathDeathDeathDeath

查表法的代码实现更加清晰,可读性和可维护性更好。当修改状态机时,我们只需要修改 transitionTable 和 actionTable 两个二维数组即可。

实际上,如果我们把这两个二维数组存储在配置文件中,当需要修改状态机时,我们甚至可以不修改任何代码,只需要修改配置文件就可以了。

代码目录:

请添加图片描述

具体的代码如下所示:

package look_up_table

type State = int
type StateName = string

// 定义状态
const (
	Small = iota
	Super
	Cape
	Fire
	Death
)

// 定义事件
const (
	E1 = iota
	E2
	E3
	E4
)

var (
	// 状态表
	transitionTable = [][]State{
		{Super, Fire, Cape, Death},
		{Super, Fire, Cape, Small},
		{Cape, Fire, Cape, Small},
		{Fire, Fire, Cape, Small},
		{Death, Death, Death, Death}}

	// 事件表
	actionTable = [][]int{
		{+100, +200, +300, 0},
		{+100, +200, +300, 0},
		{+100, +200, +300, 0},
		{+100, +200, +300, 0},
		{0, 0, 0, 0},
	}

	stateMap = map[State]StateName{Small: "SMALL", Super: "SUPER", Fire: "FIRE", Cape: "CAPE", Death: "DEATH"}
)

type MarioStateMachine struct {
	score        int
	currentState State
}

func NewMarioStateMachine() *MarioStateMachine {
	return &MarioStateMachine{score: 0, currentState: Small}
}

func (m *MarioStateMachine) ObtainMushroom() {
	m.executeEvent(E1)
}

func (m *MarioStateMachine) ObtainCape() {
	m.executeEvent(E2)
}

func (m *MarioStateMachine) ObtainFireFlower() {
	m.executeEvent(E3)
}

func (m *MarioStateMachine) MeetMonster() {
	m.executeEvent(E4)
}

// 查表
func (m *MarioStateMachine) executeEvent(event State) {
	stateValue := m.currentState
	eventValue := event
	m.currentState = transitionTable[stateValue][eventValue]
	m.score = actionTable[stateValue][eventValue]
}

func (m *MarioStateMachine) GetScore() int {
	return m.score
}

func (m *MarioStateMachine) GetStateName() string {
	return stateMap[m.currentState]
}

package look_up_table

import "testing"

func TestMarioStateMachine_MeetMonster(t *testing.T) {
	monster := NewMarioStateMachine()
	//monster.MeetMonster()
	t.Log(monster.GetStateName()) // SMALL
	monster.ObtainCape()
	t.Log(monster.GetStateName()) // FiRE
	monster.ObtainFireFlower()
	t.Log(monster.GetStateName()) // CAPE

}

像游戏这种比较复杂的状态机,包含的状态比较多,我优先推荐使用查表法,而状态模式会引入非常多的状态类,会导致代码比较难维护。

相反,像电商下单、外卖下单这种类型的状态机,它们的状态并不多,状态转移也比较简单,但事件触发执行的动作包含的业务逻辑可能会比较复杂,所以,更加推荐使用状态模式来实现。

栗子II

IPhone手机充电就是一个手机电池状态的流转,一开始手机处于有电状态,插入充电插头后,继续充电到满电状态,并进入断电保护,拔出充电插头后使用手机,由满电逐渐变为没电,最终关机;

状态迁移表:

起始状态触发事件终止状态执行动作
有电插入充电线满电充电
有电拔出充电线没电耗电
满电插入充电线满电停止充电
满电拔出充电线有电耗电
没电插入充电线有电充电
没电拔出充电线没电关机
电池状态
package state

import "fmt"

// BatteryState 电池状态接口,支持手机充电线插拔事件
type BatteryState interface {
	ConnectPlug(iPhone *IPhone) string
	DisconnectPlug(iPhone *IPhone) string
}

// fullBatteryState 满电状态
type fullBatteryState struct{}

func (s *fullBatteryState) String() string {
	return "满电状态"
}

func (s *fullBatteryState) ConnectPlug(iPhone *IPhone) string {
	return iPhone.pauseCharge()
}

func (s *fullBatteryState) DisconnectPlug(iPhone *IPhone) string {
	iPhone.SetBatteryState(PartBatteryState)
	return fmt.Sprintf("%s,%s转为%s", iPhone.consume(), s, PartBatteryState)
}

// emptyBatteryState 空电状态
type emptyBatteryState struct{}

func (s *emptyBatteryState) String() string {
	return "没电状态"
}

func (s *emptyBatteryState) ConnectPlug(iPhone *IPhone) string {
	iPhone.SetBatteryState(PartBatteryState)
	return fmt.Sprintf("%s,%s转为%s", iPhone.charge(), s, PartBatteryState)
}

func (s *emptyBatteryState) DisconnectPlug(iPhone *IPhone) string {
	return iPhone.shutdown()
}

// partBatteryState 部分电状态
type partBatteryState struct{}

func (s *partBatteryState) String() string {
	return "有电状态"
}

func (s *partBatteryState) ConnectPlug(iPhone *IPhone) string {
	iPhone.SetBatteryState(FullBatteryState)
	return fmt.Sprintf("%s,%s转为%s", iPhone.charge(), s, FullBatteryState)
}

func (s *partBatteryState) DisconnectPlug(iPhone *IPhone) string {
	iPhone.SetBatteryState(EmptyBatteryState)
	return fmt.Sprintf("%s,%s转为%s", iPhone.consume(), s, EmptyBatteryState)
}
IPhone手机
package state

import "fmt"

// 电池状态单例,全局统一使用三个状态的单例,不需要重复创建
var (
	FullBatteryState  = new(fullBatteryState)  // 满电
	EmptyBatteryState = new(emptyBatteryState) // 空电
	PartBatteryState  = new(partBatteryState)  // 部分电
)

// IPhone 已手机充电为例,实现状态模式
type IPhone struct {
	model        string       // 手机型号
	batteryState BatteryState // 电池状态
}

// NewIPhone 创建指定型号手机
func NewIPhone(model string) *IPhone {
	return &IPhone{
		model:        model,
		batteryState: PartBatteryState,
	}
}

// BatteryState 输出电池当前状态
func (i *IPhone) BatteryState() string {
	return fmt.Sprintf("iPhone %s 当前为%s", i.model, i.batteryState)
}

// ConnectPlug 连接充电线
func (i *IPhone) ConnectPlug() string {
	return fmt.Sprintf("iPhone %s 连接电源线,%s", i.model, i.batteryState.ConnectPlug(i))
}

// DisconnectPlug 断开充电线
func (i *IPhone) DisconnectPlug() string {
	return fmt.Sprintf("iPhone %s 断开电源线,%s", i.model, i.batteryState.DisconnectPlug(i))
}

// SetBatteryState 设置电池状态
func (i *IPhone) SetBatteryState(state BatteryState) {
	i.batteryState = state
}

func (i *IPhone) charge() string {
	return "正在充电"
}

func (i *IPhone) pauseCharge() string {
	return "电已满,暂停充电"
}

func (i *IPhone) shutdown() string {
	return "手机关闭"
}

func (i *IPhone) consume() string {
	return "使用中,消耗电量"
}
测试程序
package state

import (
	"fmt"
	"testing"
)

func TestState(t *testing.T) {
	iPhone13Pro := NewIPhone("13 pro") // 刚创建的手机有部分电

	fmt.Println(iPhone13Pro.BatteryState()) // 打印部分电状态
	fmt.Println(iPhone13Pro.ConnectPlug())  // 插上电源插头,继续充满电
	fmt.Println(iPhone13Pro.ConnectPlug())  // 满电后再充电,会触发满电保护

	fmt.Println(iPhone13Pro.DisconnectPlug()) // 拔掉电源,使用手机消耗电量,变为有部分电
	fmt.Println(iPhone13Pro.DisconnectPlug()) // 一直使用手机,直到没电
	fmt.Println(iPhone13Pro.DisconnectPlug()) // 没电后会关机

	fmt.Println(iPhone13Pro.ConnectPlug()) // 再次插上电源一会,变为有电状态
}
运行结果
=== RUN   TestState
iPhone 13 pro 当前为有电状态
iPhone 13 pro 连接电源线,正在充电,有电状态转为满电状态
iPhone 13 pro 连接电源线,电已满,暂停充电
iPhone 13 pro 断开电源线,使用中,消耗电量,满电状态转为有电状态
iPhone 13 pro 断开电源线,使用中,消耗电量,有电状态转为没电状态
iPhone 13 pro 断开电源线,手机关闭
iPhone 13 pro 连接电源线,正在充电,没电状态转为有电状态
--- PASS: TestState (0.00s)
PASS

资料

https://time.geekbang.org/column/article/218375

https://www.cnblogs.com/amunote/p/15549886.html

https://github.com/mohuishou/go-design-pattern

  • 4
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值