什么是状态机?
有限状态机,英文翻译是 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 ", state); 62 }
执行结果为:1 2 0
状态机实现方式三:状态模式
事件触发的动作如果非常简单,可以用二维的actionTable表示,则可以使用查表法。但是,如果执行的动作是一系列复杂的逻辑操作(比如加减积分、写数据库、发送消息等),就没有办法用简单的二维数组来表示了。
参考资料
1.64 | 状态模式:游戏、工作流引擎中常用的状态机是如何实现的? (geekbang.org)
2.《C现代编程 集成开发环境、设计模式、极限编程、测试驱动开发、重构、持续集成》