什么是狀態機?
有限狀態機,英文翻譯是 Finite State Machine,縮寫為 FSM,簡稱為狀態機。狀態機有 3 個組成部分:狀態(State)、事件(Event)、動作(Action)。其中,事件也稱為轉移條件(Transition Condition)。事件觸發狀態的轉移及動作的執行。不過,動作不是必須的,也可能只轉移狀態,不執行任何動作。
實現狀態機的方法有多種,比較常用的有分支邏輯法、查表法、狀態模式。
我們以一個簡單的 CD 播放器為例子。這個例子里面只有狀態、事件,不包含動作
按鍵 | 功能 |
[Play/Pause] | 播放/暫停 |
[Stop] | 停止 |
狀態遷移圖:
狀態機實現方式一:分支邏輯法
它的核心思想是根據狀態遷移圖,要么先確定狀態、要么先確定事件,直譯代碼。
方法分析:對於簡單狀態機,該法是可以接受的。但是,對於復雜的狀態機,這種實現極易漏寫或錯寫某個狀態轉移;代碼中充斥大量if-else或switch-case 分支判斷邏輯,可讀性和可維護性差。
如下就是先確定事件,然后再在事件內根據狀態進行狀態轉移。
1 typedef enum { 2 ST_IDLE, 3 ST_PLAY, 4 ST_PAUSE 5 } State; 6 7 typedef enum { 8 EV_PLAY_PAUSE, 9 EV_STOP 10 } Event; 11 12 State state; 13 14 // 初始化 15 void initialize() { 16 state = ST_IDLE; 17 } 18 19 // play or pause 20 void playOrPause() { 21 if (state == ST_IDLE) { 22 state = ST_PLAY; 23 } else if (state == ST_PLAY) { 24 state = ST_PAUSE; 25 } else if (state == ST_PAUSE) { 26 state = ST_PLAY; 27 } 28 } 29 30 // stop 31 void stop() { 32 if (state == ST_PLAY || state == ST_PAUSE) { 33 state = ST_IDLE; 34 } 35 } 36 37 // 事件響應 38 void onEvent(Event ev) { 39 switch (ev) { 40 case EV_PLAY_PAUSE: 41 playOrPause(); 42 break; 43 case EV_STOP: 44 stop(); 45 break; 46 default: 47 break; 48 } 49 }
狀態機實現方法二:查表法
狀態機除了用狀態轉移圖表示外,還可以用二維表表示。如下第一維表示當前狀態,第二維表示事件,值表示當前狀態經過事件之后,轉移到的新狀態即執行的動作。
狀態/事件 | 停止(EV_STOP) | 播放/暫停(EV_PLAY_PAUSE) |
空閑(ST_IDLE) | 忽略 | 開始播放並轉為播放狀態 |
播放(ST_PLAY) | 停止並轉為空閑狀態 | 停止並轉為暫停狀態 |
暫停(ST_PAUSE) | 停止並轉為空閑狀態 | 播放並轉為播放狀態 |
相比於分支邏輯的實現方式,查表法的代碼實現更加清晰,可讀性和可維護性更好。當修改狀態機時,只需要修改transitionTable和actionTable兩個二維數組即可。
1 typedef enum { 2 ST_IDLE, 3 ST_PLAY, 4 ST_PAUSE 5 } State; 6 7 typedef enum { 8 EV_STOP, 9 EV_PLAY_PAUSE 10 } Event; 11 12 State state; 13 14 // transitionTable[i][j]表示狀態為i時,發生事件j后,狀態遷移到transitionTable[i][j] 15 State transitionTable[][2] = { 16 {ST_IDLE, ST_PLAY}, 17 {ST_IDLE, ST_PAUSE}, 18 {ST_IDLE, ST_PLAY} 19 }; 20 21 // 初始化 22 void initialize() { 23 state = ST_IDLE; 24 } 25 26 State executeEvent(Event ev) { 27 return transitionTable[state][ev]; 28 } 29 30 // play or pause 31 void playOrPause() { 32 state = transitionTable[state][EV_PLAY_PAUSE]; 33 } 34 35 // stop 36 void stop() { 37 state = transitionTable[state][EV_STOP]; 38 } 39 40 // 事件響應 41 void onEvent(Event ev) { 42 switch (ev) { 43 case EV_PLAY_PAUSE: 44 playOrPause(); 45 break; 46 case EV_STOP: 47 stop(); 48 break; 49 default: 50 break; 51 } 52 } 53 54 int main(void) { 55 initialize(); 56 onEvent(EV_PLAY_PAUSE); // 播放 57 printf("%d ", state); 58 onEvent(EV_PLAY_PAUSE); // 暫停 59 printf("%d ", state); 60 onEvent(EV_STOP); // 停止 61 printf("%d\n", state); 62 }
執行結果為:1 2 0
狀態機實現方式三:狀態模式
事件觸發的動作如果非常簡單,可以用二維的actionTable表示,則可以使用查表法。但是,如果執行的動作是一系列復雜的邏輯操作(比如加減積分、寫數據庫、發送消息等),就沒有辦法用簡單的二維數組來表示了。
參考資料
1.64 | 狀態模式:游戲、工作流引擎中常用的狀態機是如何實現的? (geekbang.org)
2.《C現代編程 集成開發環境、設計模式、極限編程、測試驅動開發、重構、持續集成》