這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
gofsm是一個簡單、小巧而又特色的有限狀態機器(FSM)。
github已經有了很幾個狀態機器的實現,比如下面的幾個,還為什麼要再發明輪子呢?
原因在於這些狀態機器有一個特點,就是一個狀態機器維護一個對象的狀態,這樣一個狀態機器就和一個具體的映像執行個體關聯在一起,在有些情況下,這沒有什麼問題,而且是很好的設計,而且比較符合狀態機器的定義。但是在有些情況下,當我們需要維護成千上百個對象的時候,需要建立成千上百個狀態機器對象,這其實是很大的浪費,因為在大部分情況下,對象本身自己會維護/保持自己當前的狀態,我們只需把對象當前的狀態傳遞給一個共用的狀態機器就可以了,也就是gofsm本身是“stateless”,本身它包維護一個或者多個對象的狀態,所有需要的輸入由調用者輸入,它只負責狀態的轉換的邏輯,所以它的實現非常的簡潔實用,這是建立gofsm的一個目的。
第二個原因它提供了Moore和Mealy兩種狀態機器的統一介面,並且提供了UML狀態機器風格的Action處理,以程式員更熟悉的方式處理狀態的改變。
第三個原因,當我們談論起狀態機器的時候,我們總會畫一個狀態轉換圖,大家可以根據這這張圖進行討論、設計、實現和驗證狀態的遷移。但是對於代碼來說,實現真的和你的設計是一致的嗎,你怎麼保證?gofsm提供了一個簡單的方法,那就是它可以輸出圖片或者pdf檔案,你可以利用輸出的狀態機器圖和你的設計進行比較,看看實現和設計是否一致。
有限狀態機器
有限狀態機器(finite-state machine)常常用於電腦程式和時序邏輯電路的設計數學模型。它被看作是一種抽象的機器,可以有有限個狀態。任意時刻這個機器只有唯一的一個狀態,這個狀態稱為目前狀態。當有外部的事件或者條件被觸發,它可以從一個狀態轉換到另一個狀態,這就是轉換(transition)。一個FSM就是由所有的狀態、初始狀態和每個轉換的觸發條件所定義。有時候,當這個轉換髮生的時候,我們可以要執行一些事情,我們稱之為動作(Action)。
現實情況中,我們實際上遇到了很多的這種狀態機器的情況,只不過我們並沒有把它們抽象出來,比如路口的紅綠燈,總是在紅、黃、綠的狀態之間轉變,比如電梯的狀態,包括開、關、上、下等幾個狀態。
有限狀態機器可以有效清晰的為一大堆的問題建立模型,大量應用於電子設計、通訊協議、語言解析和其它的工程應用中,比如TCP/IP協議棧。
以一個轉門為例,這種專門在一些會展、博物館、公園的門口很常見,顧客可以投幣或者刷卡刷票進入,我們下面以投幣(Coin)統稱這個觸發事件。如果你不投幣,閘門是鎖著的,你推不動它的轉臂,而且投一次幣只能進去一個人,過去之後閘門又是鎖著的,挺智能的 :)。
如果我們抽象出來它的狀態圖,可以用表示:
它有兩個狀態:Locked、Unlocked。有兩個輸入(input)會影響它的狀態,投幣(coin)和推動轉臂(push)。
- 在Locked狀態, push沒有作用。不管比push多少次閘門的狀態還是lock
- 在Locked狀態,投幣會讓閘門開鎖,閘門可以讓一個人通過
- 在Unlocked狀態,投幣不起作用,閘門還是開著
- 在Unlocked狀態,如果有人push通過,人通過後閘門會由Unlocked狀態轉變成Locked狀態。
這是一個簡單的閘門的狀態轉換,卻是一個很好的理解狀態的典型例子。
以表格來表示:
| Current State |
Input |
Next State |
Output |
| Locked |
coin |
Unlocked |
Unlock turnstile so customer can push through |
| push |
Locked |
None |
| Unlocked |
coin |
Unlocked |
None |
| push |
Locked |
When customer has pushed through, lock turnstile |
UML也有狀態圖的改變,它擴充了FSM的概念,提供了層次化的嵌套狀態(Hierarchically nested states)和正交地區(orthogonal regions),當然這和本文沒有太多的關係,有興趣的讀者可以找UML的資料看看。但是它提供了一個很好的概念,也就是動作(Action)。就像Mealy狀態機器所需要的一樣,動作依賴系統的狀態和觸發事件,而它的Entry Action和Exit Action,卻又像Moore 狀態機器一樣,不依賴輸入,只依賴狀態。所以UML的動作有三種,一種是事件被處理的時候,狀態機器會執行特定的動作,比如改變變數、執行I/O、調用方法、觸發另一個事件等。而離開一個狀態,可以執行Exit action,進入一個狀態,則執行Entry action。記住,收到一個事件,對象的狀態不會改變,比如上邊閘門的例子,在Locked狀態下push多少次狀態都沒改變,這這種情況下,不會執行Exit和Entry action。
gofsm提供了這種擴充的模型,當然如果你不想使用這種擴充,你也可以不去實現Entry和Exit。
可以提到了兩種狀態機器,這兩種狀態機器是這樣來區分的:
- Moore machine
Moore狀態機器只使用entry action,輸出只依賴狀態,不依賴輸入。
- Mealy machine
Mealy狀態機器只使用input action,輸出依賴輸入input和狀態state。使用這種狀態機器通常可以減少狀態的數量。
gofsm提供了一個通用的介面,你可以根據需要確定使用哪個狀態機器。從軟體開發的實踐上來看,有時候你並不一定要關注狀態機器的區分,而是清晰的抽象、設計你所關注的對象的狀態、觸發條件以及要執行的動作。
gofsm
gofsm參考了 elimisteve/fsm 的實現,實現了一種單一狀態機器處理多個對象的方法,並且提供了輸出狀態圖的功能。
它除了定義對象的狀態外,還定義了觸發事件以及處理的Action,這些都是通過字串來表示的,在使用的時候很容易的和你的對象、方法對應起來。
使用gofsm也很簡單,當然第一步將庫拉到本地:
1 |
go get -u github.com/smallnest/gofsm |
我們以上面的閘門為例,看看gofsm是如何使用的。
注意下面的單個狀態機器可以處理並行地的處理多個閘門的狀態改變,雖然例子中只產生了一個閘門對象。
首先定義一個閘門對象,它包含一個State,表示它當前的狀態:
12345678 |
type Turnstile struct {ID uint64EventCount uint64 //事件統計CoinCount uint64 //投幣事件統計PassCount uint64 //顧客通過事件統計State string //目前狀態States []string //曆史經過的狀態} |
狀態機器的初始化簡單直接:
123456789101112 |
func initFSM() *StateMachine {delegate := &DefaultDelegate{p: &TurnstileEventProcessor{}}transitions := []Transition{Transition{From: "Locked", Event: "Coin", To: "Unlocked", Action: "check"},Transition{From: "Locked", Event: "Push", To: "Locked", Action: "invalid-push"},Transition{From: "Unlocked", Event: "Push", To: "Locked", Action: "pass"},Transition{From: "Unlocked", Event: "Coin", To: "Unlocked", Action: "repeat-check"},}return NewStateMachine(delegate, transitions...)} |
你定義好轉換對應關係transitions,一個Transition代表一個轉換,從某個狀態到另外一個狀態,觸發的事件名,要執行的Action。
因為Action是字串,所以你需要實現delegate將Action和對應的要處理的方法對應起來。
注意from和to的狀態可以一樣,在這種情況下,狀態沒有發生改變,只是需要處理Action就可以了。
如果Action為空白,也就是不需要處理事件,只是發生狀態的改變而已。
處理Action的類型如下:
12345678910111213 |
type TurnstileEventProcessor struct{}func (p *TurnstileEventProcessor) OnExit(fromState string, args []interface{}) {……}func (p *TurnstileEventProcessor) Action(action string, fromState string, toState string, args []interface{}) {……}func (p *TurnstileEventProcessor) OnEnter(toState string, args []interface{}) { ……} |
然後我們就可以觸發一些事件看看閘門的狀態機器是否正常工作:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263 |
ts := &Turnstile{ID: 1,State: "Locked",States: []string{"Locked"},}fsm := initFSM()//推門//沒刷卡/投幣不可進入err := fsm.Trigger(ts.State, "Push", ts)if err != nil {t.Errorf("trigger err: %v", err)}//推門//沒刷卡/投幣不可進入err = fsm.Trigger(ts.State, "Push", ts)if err != nil {t.Errorf("trigger err: %v", err)}//刷卡或者投幣//不容易啊,終於解鎖了err = fsm.Trigger(ts.State, "Coin", ts)if err != nil {t.Errorf("trigger err: %v", err)}//刷卡或者投幣//這時才解鎖err = fsm.Trigger(ts.State, "Coin", ts)if err != nil {t.Errorf("trigger err: %v", err)}//推門//這時才能進入,進入後閘門被鎖err = fsm.Trigger(ts.State, "Push", ts)if err != nil {t.Errorf("trigger err: %v", err)}//推門//無法進入,閘門已鎖err = fsm.Trigger(ts.State, "Push", ts)if err != nil {t.Errorf("trigger err: %v", err)}lastState := Turnstile{ID: 1,EventCount: 6,CoinCount: 2,PassCount: 1,State: "Locked",States: []string{"Locked", "Unlocked", "Locked"},}if !compareTurnstile(&lastState, ts) {t.Errorf("Expected last state: %+v, but got %+v", lastState, ts)} else {t.Logf("最終的狀態: %+v", ts)} |
如果想將狀態圖輸出圖片,可以調用下面的方法,它實際是調用graphviz產生的,所以請確保你的機器上是否安裝了這個軟體,你可以執行dot -h檢查一下:
1 |
fsm.Export("state.png") |
產生的圖片就是文首的閘門的狀態機器的圖片。
如果你想定製graphviz的參數,你可以調用另外一個方法:
1 |
func (m *StateMachine) ExportWithDetails(outfile string, format string, layout string, scale string, more string) error |
其它Go語言實現的FSM
如果你發現gofsm的功能需要改進,或者有一些想法、或者發現了bug,請不用遲疑,在issue中提交你的意見和建議,我會及時的進行反饋。
如果你覺得本項目有用,或者將來可能會使用,請star這個項目 smallnest/gofsm。
如果你想比較其它的Go語言實現的fsm,可以參考下面的列表:
- elimisteve/fsm
- looplab/fsm
- vaughan0/go-fsm
- WatchBeam/fsm
- DiscoViking/fsm
- autocube/hsm
- theckman/go-fsm
- Zumata/fsm
- syed/go-fsm
- go-rut/fsm
- yandd/fsm
- go-trellis/fsm
- ……
參考資料
- https://en.wikipedia.org/wiki/Finite-state_machine