Background
生产当中经常会遇到这样的场景:业务具有非常多的状态,不同的状态直接来回跳转,会触发不同的事件,当状态和事件多起来以后,维护起来相当麻烦。
就比如我们玩马里奥游戏,小马里奥吃了蘑菇变大马里奥,吃了火焰蘑菇变火焰马里奥,吃了冰蘑菇又变冰马里奥;冰马里奥挂了会变大马里奥,大马里奥又会变马里奥;通常情况下,我们总是需要记录当前状态,可跳转状态,发生的事件,才能够决定我们的业务逻辑。而使用状态机,则不需要人工去判断这些状态,只需要提供事件,状态机则会帮你选择正确的跳转逻辑。
最近组里的大佬分享了一个状态机的案例,于是打算学习一下状态机模式,并用go做一个简单的状态机生成小工具;
实际使用时只需要维护一份状态调整的json配置,然后通过模版文件自动生成状态跳转的代码;这样我们就只需要关注,状态跳转的业务逻辑,如何进行跳转等一系列繁琐的事情就交由状态机实现。
状态机
一般而言,我们的业务场景会有
- 状态
- 事件
当我们的当前状态确定,事件确定,那么我们能够跳转的状态也确定了;
我们假设这么一种场景,我们目前的业务是一个股票软件的用户管理系统。
那么我们的用户则有下面几种状态
- 游客(sightseer):下载了软件,没有别的操作,是一个初始状态
- 买家(buyer):当我们开通账户后,就变成了买家,可以进行买卖
- 持有者(holder):当我们购买并拥有股票后,就变成了持有者
然后可能发生的事件和状态转换则有
- 开户:由游客变成了买家
- 购买股票:由买家变成了持有者
- 卖出部分股票:持有者仍然为持有者
- 全部卖出: 持有者变成买家
- 注销账户:买家变游客
- 其他情况就暂时不考虑了
根据这个逻辑,我们可以先画一个状态机出来(之前的文章分享过用puml画图,有兴趣的朋友可以看看)
针对这个逻辑,如果我们不用状态机,那么就是在每次事件发生时,先获取当前的状态,判断事件,判断应该跳转到的状态,执行业务逻辑。
对于先获取当前的状态,判断事件,判断应该跳转到的状态
这一部分操作,其实是固定的,可以通过状态机来帮我处理,进而减少一部分冗余代码。
新建一个状态机
状态机使用的是
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) },
},
)
回调函数是我们在进行状态跳转的时候会触发的函数,主要包括下面几种情况
- 在特定事件发生前调用: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())
可以看到,状态机可以帮助我们进行状态的扭转,判断事件是否合理,以及能否扭转。使用状态机后我们只需要关注事件,和对应的逻辑,代码相对而言会更加清晰。
但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)
}
执行完成后就生成了我们的状态机,我们只用另外修改我们的状态转移逻辑即可
完整的代码放到github上了(github.com/EricOo0/gof… 后续有空再来完善一下文章和代码。