微信公眾號:[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重構