有限状态机
概念
状态模式是一种行为设计模式,让你能在一个对象的内部状态变化时改变其行为,使其看上去就像改变了自身所属的类一样。
该模式将与状态相关的行为抽取到独立的状态类中,让原对象将工作委派给这些类的实例,而不是自行进行处理。
状态迁移有四个元素组成,起始状态、触发迁移的事件,终止状态以及要执行的动作,每个具体的状态包含触发状态迁移的执行方法,迁移方法的实现是执行持有状态对象的动作方法,同时设置状态为下一个流转状态;持有状态的业务对象包含有触发状态迁移方法,这些迁移方法将请求委托给当前具体状态对象的迁移方法。
有限状态机,英文翻译是 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) | |
---|---|---|---|---|
Small | Super/+100 | Fire/+200 | Cape/+300 | Death |
Super | Super/+100 | Fire/+200 | Cape/+300 | Small |
Cape | Cape/+100 | Fire/+200 | Cape/+300 | Small |
Fire | Fire/+100 | Fire/+200 | Cape/+300 | Small |
Death | Death | Death | Death | Death |
查表法的代码实现更加清晰,可读性和可维护性更好。当修改状态机时,我们只需要修改 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