微信公众号:[double12gzh]
关注容器技术、关注
Kubernetes
。问题或建议,请公众号留言。
1. 背景
在计算机领域中,状态机
是一个比较基础的概念。在我们的日常生活中,我们可以看到许多状态机
的例子,如:交通信息号灯、电梯、自动售货机等。
基于FSM的编程也是一个强大的工具,可以对复杂的状态转换进行建模,它可以大大简化我们的程序。
2. 什么是状态机
有限状态机(FSM)或简称状态机,是一种计算的数学模型。它是一个抽象的机器,在任何时间都可以处于有限的状态之一。FSM可以根据一些输入从一个状 态变为另一个状态;从一个状态到另一个状态的变化称为转换。
一个FSM由三个关键要素组成:初始状态
、所有可能状态的列表
、触发状态转换的输入
。
下面我们以旋转门作为FSM建模的一个简单例子(来自Wikipedia)
和其他FSM一样,转门的状态机有三个元素:
•它的初始状态是 "锁定"•它有两种可能的状态。"锁定 "和 "解锁"•两个输入将触发状态变化。"推 "和 "硬币"
3. 实现状态机
接下来,我将建立一个模拟旋转门行为的命令行程序。当程序启动时,它会提示用户输入一些命令,然后它将根据输入的命令改变其状态。
3.1 版本1 简单直接
package mainimport ( "bufio" "fmt" "log" "os" "strings")// 旋转门状态type State uint32const ( 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 mainimport ( "bufio" "fmt" "log" "os" "strings")type State uint32const ( 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请尽快通行,然后将会锁定fuckunknown command, try again pleasepush禁止通行,请先解锁push禁止通行,请先解锁coin已解锁,请通行push请尽快通行,然后将会锁定push禁止通行,请先解锁
4. 小结
在这个故事中,我们介绍了FSM的概念,并建立了一个基于FSM的程序,同时,我们提供了四个版本的实现方式来实现FSM:
•v1,以直接的形式实现FSM。•v2,做一些重构以减少代码重复。•v3、引入状态转换表•v4,用OOP重构