使用Golang实现状态机
微信公众号:[double12gzh]
关注容器技术、关注Kubernetes
。问题或建议,请公众号留言。
1. 背景
在计算机领域中,状态机
是一个比较基础的概念。在我们的日常生活中,我们可以看到许多状态机
的例子,如:交通信息号灯、电梯、自动售货机等。
基于FSM的编程也是一个强大的工具,可以对复杂的状态转换进行建模,它可以大大简化我们的程序。
2. 什么是状态机
有限状态机(FSM)或简称状态机,是一种计算的数学模型。它是一个抽象的机器,在任何时间都可以处于有限的状态之一。FSM可以根据一些输入从一个状
态变为另一个状态;从一个状态到另一个状态的变化称为转换。
一个FSM由三个关键要素组成:初始状态
、所有可能状态的列表
、触发状态转换的输入
。
下面我们以旋转门作为FSM建模的一个简单例子(来自Wikipedia)
和其他FSM一样,转门的状态机有三个元素:
- 它的初始状态是 "锁定"
- 它有两种可能的状态。"锁定 "和 "解锁"
- 两个输入将触发状态变化。"推 "和 "硬币"
3. 实现状态机
接下来,我将建立一个模拟旋转门行为的命令行程序。当程序启动时,它会提示用户输入一些命令,然后它将根据输入的命令改变其状态。
3.1 版本1 简单直接
package main
import (
"bufio"
"fmt"
"log"
"os"
"strings"
)
// 旋转门状态
type State uint32
const (
Locked State = iota
Unlocked
)
// 相关的命令
const (
CmdCoin = "coin"
CmdPush = "push"
)
func main() {
state := Locked
reader := bufio.NewReader(os.Stdin)
prompt(state)
for {
cmd, err := reader.ReadString('\n')
if err != nil {
log.Fatalln(err)
}
cmd = strings.TrimSpace(cmd)
switch state {
case Locked:
if cmd == CmdCoin {
fmt.Println("解锁, 请通行")
state = Unlocked
} else if cmd == CmdPush {
fmt.Println("禁止通行,请先解锁")
} else {
fmt.Println("命令未知,请重新输入")
}
case Unlocked:
if cmd == CmdCoin {
fmt.Println("大兄弟,门开着呢,别浪费钱了")
} else if cmd == CmdPush {
fmt.Println("请通行,通行之后将会关闭")
state = Locked
} else {
fmt.Println("命令未知,请重新输入")
}
}
}
}
func prompt(s State) {
m := map[State]string{
Locked: "Locked",
Unlocked: "Unlocked",
}
fmt.Printf("当前的状态是: [%s], 请输入命令: [coin|push]\n", m[s])
}
说明:
- 首先定义两个状态
Locked
/Unlocked
和两个支持的命令CmdCoin
/CmdPush
- 在main函数中设定了旋转门的初始状态为
Locked
- 后面启动一个无限循环,等待用户输入命令,并根据不同的状态处理不同的命令
问题与优化:
- 我们必须处理每个状态的未知命令,这可以通过小的重构来改进。
- 如果我们把状态转换的逻辑提取到一个函数中,程序的表达能力会更强。
3.2 版本2 重构优化
...
func main() {
...
for {
cmd, err := reader.ReadString('\n')
if err != nil {
log.Fatalln(err)
}
state = step(state, strings.TrimSpace(cmd))
}
}
func step(state State, cmd string) State {
if cmd != CmdCoin && cmd != CmdPush {
fmt.Println("未知命令,请重新输入")
return state
}
switch state {
case Locked:
if cmd == CmdCoin {
fmt.Println("已解锁,请通行")
state = Unlocked
} else {
fmt.Println("禁止通行,请先解锁")
}
case Unlocked:
if cmd == CmdCoin {
fmt.Println("大兄弟,别浪费钱了,现在已经解锁了")
} else {
fmt.Println("请通行,通行之后将会关闭")
state = Locked
}
}
return state
}
...
实现上,一个状态机通常会使用状态转换表
来表示,如下:
3.3 版本3 状态转换表
通过上面的分析下,针对上述实现再次优化,这次引入状态转换表的实现
...
func main() {
...
for {
// 读取用户的输入
cmd, err := reader.ReadString('\n')
if err != nil {
log.Fatalln(err)
}
// 获取状态转换表中的值
tupple := CommandStateTupple{strings.TrimSpace(cmd), state}
if f := StateTransitionTable[tupple]; f == nil {
fmt.Println("未知命令,请重新输入")
} else {
f(&state)
}
}
}
// CommandStateTupple 用于存放状态转换表的结构体
type CommandStateTupple struct {
Command string
State State
}
// TransitionFunc 状态转移方程
type TransitionFunc func(state *State)
// StateTransitionTable 状态转换表
var StateTransitionTable = map[CommandStateTupple]TransitionFunc{
{CmdCoin, Locked}: func(state *State) {
fmt.Println("已解锁,请通行")
*state = Unlocked
},
{CmdPush, Locked}: func(state *State) {
fmt.Println("禁止通行,请先行解锁")
},
{CmdCoin, Unlocked}: func(state *State) {
fmt.Println("大兄弟,已解锁了,别浪费钱了")
},
{CmdPush, Unlocked}: func(state *State) {
fmt.Println("请尽快通行,通行后将自动上锁")
*state = Locked
},
}
...
采用这种方法,所有可能的转换都列在表格中。它易于维护和理解。如果需要一个新的转换,只需增加一个表项。
由于FSM是一个抽象的机器,我们可以更进一步,以面向对象的方式实现它。
3.4 版本4 通过class来抽象
这里我们将会引入一个新的类Turnstile
,这个类有一个属性State
和一个方法ExecuteCmd
。当需要进行状态转换时,就调用ExecuteCmd
,
并且ExecuteCmd
是唯一能触发状态发生转换的途径。
类图如下
完整的代码实现如下:
package main
import (
"bufio"
"fmt"
"log"
"os"
"strings"
)
type State uint32
const (
Locked State = iota
Unlocked
)
const (
CmdCoin = "coin"
CmdPush = "push"
)
type Turnstile struct {
State State
}
// ExecuteCmd 执行命令
func (p *Turnstile) ExecuteCmd(cmd string) {
tupple := CmdStateTupple{strings.TrimSpace(cmd), p.State}
if f := StateTransitionTable[tupple]; f == nil {
fmt.Println("unknown command, try again please")
} else {
f(&p.State)
}
}
func main() {
machine := &Turnstile{State: Locked}
prompt(machine.State)
reader := bufio.NewReader(os.Stdin)
for {
cmd, err := reader.ReadString('\n')
if err != nil {
log.Fatalln(err)
}
machine.ExecuteCmd(cmd)
}
}
type CmdStateTupple struct {
Cmd string
State State
}
type TransitionFunc func(state *State)
var StateTransitionTable = map[CmdStateTupple]TransitionFunc{
{CmdCoin, Locked}: func(state *State) {
fmt.Println("已解锁,请通行")
*state = Unlocked
},
{CmdPush, Locked}: func(state *State) {
fmt.Println("禁止通行,请先解锁")
},
{CmdCoin, Unlocked}: func(state *State) {
fmt.Println("大兄弟,不要浪费钱了")
},
{CmdPush, Unlocked}: func(state *State) {
fmt.Println("请尽快通行,然后将会锁定")
*state = Locked
},
}
func prompt(s State) {
m := map[State]string{
Locked: "Locked",
Unlocked: "Unlocked",
}
fmt.Printf("当前的状态是: [%s], 请输入命令:[coin|push]\n", m[s])
}
运行一下上面的代码,可以看到如下的输出:
F:\hello>go run main.go
当前的状态是: [Locked], 请输入命令:[coin|push]
coin
已解锁,请通行
push
请尽快通行,然后将会锁定
fuck
unknown command, try again please
push
禁止通行,请先解锁
push
禁止通行,请先解锁
coin
已解锁,请通行
push
请尽快通行,然后将会锁定
push
禁止通行,请先解锁
4. 小结
在这个故事中,我们介绍了FSM的概念,并建立了一个基于FSM的程序,同时,我们提供了四个版本的实现方式来实现FSM:
- v1,以直接的形式实现FSM。
- v2,做一些重构以减少代码重复。
- v3、引入状态转换表
- v4,用OOP重构