行为型 - 5. 状态模式

108 阅读5分钟

在实际的软件开发中,状态模式并不是很常用,但是在能够用到的场景里,它可以发挥很大的作用。它有点像组合模式。

状态模式(State Design Pattern)一般用来实现状态机,而状态机常用在游戏、工作流引擎等系统开发中。不过,状态机的实现方式有多种,除了状态模式,比较常用的还有分支逻辑法和查表法。

1. 什么是有限状态机?

有限状态机,英文翻译是 Finite State Machine,缩写为 FSM,简称为状态机。状态机有 3 个组成部分:状态(State)、事件(Event)、动作(Action)。其中,事件也称为转移条件(Transition Condition)。事件触发状态的转移及动作的执行。不过,动作不是必须的,也可能只转移状态,不执行任何动作。

2. 有限状态机的实现方式

2.1 分支逻辑法

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

对于简单的状态机来说,分支逻辑这种实现方式是可以接受的。但是,对于复杂的状态机来说,这种实现方式极易漏写或者错写某个状态转移。除此之外,代码中充斥着大量的 if-else 或者 switch-case 分支判断逻辑,可读性和可维护性都很差。如果哪天修改了状态机中的某个状态转移,要在冗长的分支逻辑中找到对应的代码进行修改,很容易改错,引入 bug。

2.2 查表法

用状态转移图来表示之外,状态机还可以用二维表来表。在二维表中,第一维表示当前状态,第二维表示事件,值表示当前状态经过事件之后,转移到的新状态及其执行的动作。

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

2.3 状态模式

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

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

状态模式通过将事件触发的状态转移和动作执行,拆分到不同的状态类中,来避免分支判断逻辑。

2.4 总结

像游戏这种比较复杂的状态机,包含的状态比较多,优先推荐使用查表法,而状态模式会引入非常多的状态类,会导致代码比较难维护。相反,像电商下单、外卖下单这种类型的状态机,它们的状态并不多,状态转移也比较简单,但事件触发执行的动作包含的业务逻辑可能会比较复杂,所以,更加推荐使用状态模式来实现。

3. 状态模式的代码实现

MarioStateMachine 和各个状态类之间是双向依赖关系。MarioStateMachine 依赖各个状态类是理所当然的,但是,反过来,各个状态类为什么要依赖 MarioStateMachine 呢?这是因为,各个状态类需要更新 MarioStateMachine 中的两个变量,score 和 currentState。

优化思路:可以通过函数参数将 MarioStateMachine 传递进状态类。

type MarioState int64

const (
   Small MarioState = 1 // 小马里奥
   Super MarioState = 2 // 超级马里奥
   Cape  MarioState = 3 // 斗篷马里奥
   Fire  MarioState = 4 // 火焰马里奥

)

type IMario interface {
   GetState() MarioState
   // 事情的定义
   ObtainMushroom()   // 吃蘑菇
   ObtainCape()       // 获得斗篷
   ObtainFireFlower() // 获得火焰
   MeetMonster()      // 遇到怪物
}

type SmallMario struct {
   machine *MarioStateMachine
}

func NewSmallMario(machine *MarioStateMachine) *SmallMario {
   return &SmallMario{machine: machine}
}

func (s *SmallMario) GetState() MarioState {
   return Small
}

func (s *SmallMario) ObtainMushroom() {
   s.machine.SetCurrentState(NewSuperMario(s.machine))
   s.machine.AddScore(100)
}

func (s *SmallMario) ObtainCape() {
   s.machine.SetCurrentState(NewCapMario(s.machine))
   s.machine.AddScore(200)
}

func (s *SmallMario) ObtainFireFlower() {
   s.machine.SetCurrentState(NewFireMario(s.machine))
   s.machine.AddScore(300)
}

func (s *SmallMario) MeetMonster() {
   fmt.Println("Game over, how about try again.")
}

type SuperMario struct {
   machine *MarioStateMachine
}

func NewSuperMario(machine *MarioStateMachine) *SuperMario {
   return &SuperMario{machine: machine}
}

func (s *SuperMario) GetState() MarioState {
   return Super
}

func (s *SuperMario) ObtainMushroom() {
   // do nothing
}

func (s *SuperMario) ObtainCape() {
   s.machine.SetCurrentState(NewCapMario(s.machine))
   s.machine.AddScore(200)
}

func (s *SuperMario) ObtainFireFlower() {
   s.machine.SetCurrentState(NewFireMario(s.machine))
   s.machine.AddScore(300)
}

func (s *SuperMario) MeetMonster() {
   s.machine.SetCurrentState(NewSmallMario(s.machine))
   s.machine.AddScore(-100)
}

type FireMario struct {
   machine *MarioStateMachine
}

func NewFireMario(machine *MarioStateMachine) *FireMario {
   return &FireMario{machine: machine}
}

func (f *FireMario) GetState() MarioState {
   return Fire
}

func (f *FireMario) ObtainMushroom() {
   // do nothing
}

func (f *FireMario) ObtainCape() {
   f.machine.SetCurrentState(NewCapMario(f.machine))
   f.machine.AddScore(200)
}

func (f *FireMario) ObtainFireFlower() {
   // do nothing
}

func (f *FireMario) MeetMonster() {
   f.machine.SetCurrentState(NewSmallMario(f.machine))
   f.machine.AddScore(-200)
}

type CapeMario struct {
   machine *MarioStateMachine
}

func NewCapMario(machine *MarioStateMachine) *CapeMario {
   return &CapeMario{machine: machine}
}

func (c *CapeMario) GetState() MarioState {
   return Cape
}

func (c *CapeMario) ObtainMushroom() {
   // do nothing
}

func (c *CapeMario) ObtainCape() {
   // do nothing
}

func (c *CapeMario) ObtainFireFlower() {
   // do nothing
}

func (c *CapeMario) MeetMonster() {
   c.machine.SetCurrentState(NewSmallMario(c.machine))
   c.machine.AddScore(-300)
}

type MarioStateMachine struct {
   Score        int64
   CurrentState IMario
}

func NewMarioStateMachine() *MarioStateMachine {
   machine := &MarioStateMachine{}
   smallMario := NewSmallMario(machine)
   machine.CurrentState = smallMario
   return machine
}

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

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

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

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

func (m *MarioStateMachine) GetScore() int64 {
   return m.Score
}

func (m *MarioStateMachine) GetState() MarioState {
   return m.CurrentState.GetState()
}

func (m *MarioStateMachine) AddScore(score int64) {
   m.Score += score
}

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

// 客户端使用
func TestMario(t *testing.T) {
   machine := NewMarioStateMachine()
   t.Log(machine.GetScore()) // 0
   t.Log(machine.GetState()) // 1, represent small

   machine.ObtainMushroom()
   t.Log(machine.GetScore()) // 100
   t.Log(machine.GetState()) // 2, represent huge

   machine.ObtainMushroom()  // do nothing
   t.Log(machine.GetScore()) // 100
   t.Log(machine.GetState()) // 2, represent huge

   machine.ObtainFireFlower()
   t.Log(machine.GetScore()) // 400
   t.Log(machine.GetState()) // 4, represent fire

   machine.MeetMonster()
   t.Log(machine.GetScore()) // 300
   t.Log(machine.GetState()) // 1, represent small

   machine.MeetMonster() // game over
}