Golang實現狀態機
阿新 • • 發佈:2022-03-25
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 通過物件來抽象
這裡我們將會引入一個新的類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重構