1. 程式人生 > 實用技巧 >使用Golang實現狀態機

使用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重構