Golang 有限状态机

373 阅读6分钟

Background

生产当中经常会遇到这样的场景:业务具有非常多的状态,不同的状态直接来回跳转,会触发不同的事件,当状态和事件多起来以后,维护起来相当麻烦。

就比如我们玩马里奥游戏,小马里奥吃了蘑菇变大马里奥,吃了火焰蘑菇变火焰马里奥,吃了冰蘑菇又变冰马里奥;冰马里奥挂了会变大马里奥,大马里奥又会变马里奥;通常情况下,我们总是需要记录当前状态,可跳转状态,发生的事件,才能够决定我们的业务逻辑。而使用状态机,则不需要人工去判断这些状态,只需要提供事件,状态机则会帮你选择正确的跳转逻辑。

最近组里的大佬分享了一个状态机的案例,于是打算学习一下状态机模式,并用go做一个简单的状态机生成小工具;

实际使用时只需要维护一份状态调整的json配置,然后通过模版文件自动生成状态跳转的代码;这样我们就只需要关注,状态跳转的业务逻辑,如何进行跳转等一系列繁琐的事情就交由状态机实现。

image.png

状态机

一般而言,我们的业务场景会有

  • 状态
  • 事件

当我们的当前状态确定,事件确定,那么我们能够跳转的状态也确定了;

我们假设这么一种场景,我们目前的业务是一个股票软件的用户管理系统。

那么我们的用户则有下面几种状态

  • 游客(sightseer):下载了软件,没有别的操作,是一个初始状态
  • 买家(buyer):当我们开通账户后,就变成了买家,可以进行买卖
  • 持有者(holder):当我们购买并拥有股票后,就变成了持有者

然后可能发生的事件和状态转换则有

  • 开户:由游客变成了买家
  • 购买股票:由买家变成了持有者
  • 卖出部分股票:持有者仍然为持有者
  • 全部卖出: 持有者变成买家
  • 注销账户:买家变游客
  • 其他情况就暂时不考虑了

根据这个逻辑,我们可以先画一个状态机出来(之前的文章分享过用puml画图,有兴趣的朋友可以看看)

image.png

针对这个逻辑,如果我们不用状态机,那么就是在每次事件发生时,先获取当前的状态,判断事件,判断应该跳转到的状态,执行业务逻辑。

对于先获取当前的状态,判断事件,判断应该跳转到的状态 这一部分操作,其实是固定的,可以通过状态机来帮我处理,进而减少一部分冗余代码。

新建一个状态机

状态机使用的是

github.com/looplab/fsm

对于一个状态机,我们只需要确定初始条件,并确定状态之间的跳转关系

Ps.Events中是我们前面确定的事件action,不同的action会从一个状态跳到另一个状态

这一部分的跳转,只需要在初始化的时候确定,后续由状态机来扭转。我们需要关注的是跳转到不同状态时的业务逻辑,即这里设置的回调函数。

fsm := fsm.NewFSM(
   "sightseer",
   fsm.Events{
      {Name: "openAccount", Src: []string{"sightseer"}, Dst: "buyer"},
      {Name: "buy", Src: []string{"buyer"}, Dst: "holder"},
      {Name: "sellPartial", Src: []string{"holder"}, Dst: "holder"},
      {Name: "sellAll", Src: []string{"holder"}, Dst: "buyer"},
      {Name: "writeOff", Src: []string{"buyer"}, Dst: "sightseer"},
   },
   fsm.Callbacks{
      "before_event":       func(event *fsm.Event) { fmt.Println("an event is gonna happen") },
      "before_openAccount": func(event *fsm.Event) { fmt.Println("event 'openAccount' is gonna happen") },
      "before_buy":         func(event *fsm.Event) { fmt.Println("event 'buy' is gonna happen") },
      "before_sellPartial": func(event *fsm.Event) { fmt.Println("event 'sellPartial' is gonna happen") },
      "before_sellAll":     func(event *fsm.Event) { fmt.Println("event 'sellAll' is gonna happen") },
      "before_writeOff":    func(event *fsm.Event) { fmt.Println("event 'write off' is gonna happen") },
      "leave_state":        func(event *fsm.Event) { fmt.Printf("now leaving state:%v to %v \n", event.Src, event.Dst) },
   },
)

回调函数是我们在进行状态跳转的时候会触发的函数,主要包括下面几种情况

image.png

  • 在特定事件发生前调用:before_< Event >
  • 在所有事件发生前调用:before_event
  • 离开具体一个状态前调用:leave_< Old_stat >
  • 在所有的状态转移前调用:leave_state
  • 进入某个状态后调用:enter_< new_state >
  • 进入所有状态后都调用:enter_state
  • 特定事件发生后调用
  • 所有事件发生后都调用

进行以下测试

fmt.Println("init state:", fsm.Current())

err := fsm.Event("openAccount")
if err != nil {
   fmt.Println("err happened:", err)
}
fmt.Println("cur state:", fsm.Current())

err = fsm.Event("openAccount")
if err != nil {
   fmt.Println("err happened:", err)
}
fmt.Println("cur state:", fsm.Current())

err = fsm.Event("buy")
if err != nil {
   fmt.Println("err happened:", err)
}
fmt.Println("cur state:", fsm.Current())

err = fsm.Event("sellPartial")
if err != nil {
   fmt.Println("err happened:", err)
}
fmt.Println("cur state:", fsm.Current())

err = fsm.Event("sellPartial")
if err != nil {
   fmt.Println("err happened:", err)
}
fmt.Println("cur state:", fsm.Current())

err = fsm.Event("sellAll")
if err != nil {
   fmt.Println("err happened:", err)
}
fmt.Println("cur state:", fsm.Current())

err = fsm.Event("writeOff")
if err != nil {
   fmt.Println("err happened:", err)
}
fmt.Println("final state:", fsm.Current())

image.png

可以看到,状态机可以帮助我们进行状态的扭转,判断事件是否合理,以及能否扭转。使用状态机后我们只需要关注事件,和对应的逻辑,代码相对而言会更加清晰。

但fsm这个库有个小问题,当状态没有扭转,即当前状态还是跳到当前状态时,实际也会有error,需要在代码里对这个进行处理,对应自环issue:github.com/looplab/fsm…

状态机生成工具

首先准备好我们的状态转移配置config.json

{
  "name": "stock_state_machine",
  "init_state": "sightseer",
  "event": [{
    "name": "openAccount",
    "src": ["sightseer"],
    "dst": "buyer"
  },{
    "name": "buy",
    "src": ["buyer"],
    "dst": "holder"
  },{
    "name": "sellPartial",
    "src": ["holder"],
    "dst": "holder"
  },{
    "name": "sellAll",
    "src": ["holder"],
    "dst": "buyer"
  },{
    "name": "writeOff",
    "src": ["buyer"],
    "dst": "sightseer"
  }]
}

利用golang 的模版标准库,一键生成代码

package fsm

import (
   "fmt"
   "github.com/looplab/fsm"
)

func Ref{{.BizName}}StateMachine() *{{.BizName}}StateMachine {
   return stateMachine
}

type {{.BizName}}StateMachine struct {
    Fsm *fsm.FSM
}

var stateMachine = &{{.BizName}}StateMachine{}

func Init() {
   stateMachine.Fsm = fsm.NewFSM(
      "{{.InitState}}",
      fsm.Events{
          {{ range .Events }}
         {Name: "{{.ActionName}}", Src: []string{ {{.SrcState}} }, Dst: "{{.DstState}}" },
         {{end}}
      },
      fsm.Callbacks{
         "leave_state":        func(event *fsm.Event) { fmt.Printf("now leaving state:%v to %v \n", event.Src, event.Dst) },
          {{ range .State }}
          "leave_{{.}}":       Exit{{.}}Func ,
          "enter_{{.}}":       Enter{{.}}Func ,
          {{end}}
      },
   )
}

生成状态机的代码如下

type StateMachineCfg struct {
   Name      string
   InitState string `json:"init_state"`
   Event     []StateEvent
}
type StateEvent struct {
   Name string
   Src  []string
   Dst  string
}

type StateMachineTemplate struct {
   PackageName string
   BizName     string
   InitState   string
   Events      []MachineEvent
   State       []string
}

type MachineEvent struct {
   ActionName string
   SrcState   string
   DstState   string
}

type CallBackFunc struct{}

func buildFsm() {
   // 打开配置文件
   jsonFile, err := os.Open("config.json")
   if err != nil {
      panic(err)
   }

   defer jsonFile.Close()

   byteValue, _ := ioutil.ReadAll(jsonFile)
   // 读取配置
   fsmCfg := &StateMachineCfg{}
   err = json.Unmarshal(byteValue, fsmCfg)
   if err != nil {
      panic(err)
   }

   // 读取模版文件
   tpl, err := template.ParseFiles("./template/state_machine.tpl")
   if err != nil {
      panic(err)
   }
   actionTpl, err := template.ParseFiles("./template/action.tpl")
   if err != nil {
      panic(err)
   }

   eventTpl := &StateMachineTemplate{
      PackageName: "fsm",
      BizName:     "Stock",
      InitState:   fsmCfg.InitState,
   }
   stateMap := make(map[string]bool)
   for _, event := range fsmCfg.Event {
      src, err := json.Marshal(event.Src)
      if err != nil {
         fmt.Println(fmt.Sprintf("add event error,src:%v,dst:%v,err:%v", event.Src, event.Dst, err))
         continue
      }

      for _, src := range event.Src {
         if _, ok := stateMap[src]; !ok {
            eventTpl.State = append(eventTpl.State, src)
            stateMap[src] = true
         }
      }
      if _, ok := stateMap[event.Dst]; !ok {
         eventTpl.State = append(eventTpl.State, event.Dst)
         stateMap[event.Dst] = true
      }
      eventTpl.Events = append(eventTpl.Events, MachineEvent{
         ActionName: event.Name,
         SrcState:   string(src)[1 : len(string(src))-1],
         DstState:   event.Dst,
      })
   }
   //生产状态机
   file, err := os.Create("./fsm/state_machine.go")
   if err != nil {
      panic(err)
   }
   tpl.Execute(file, eventTpl)

   file2, err := os.Create("./fsm/action.go")
   if err != nil {
      panic(err)
   }
   actionTpl.Execute(file2, eventTpl.State)

}
func main() {
   buildFsm()

   fsm.Init()
   err := fsm.RefStockStateMachine().Fsm.Event("openAccount")
   fmt.Println(err)
}

执行完成后就生成了我们的状态机,我们只用另外修改我们的状态转移逻辑即可 image.png

完整的代码放到github上了(github.com/EricOo0/gof… 后续有空再来完善一下文章和代码。