如何以面向對象的思想設計有限狀態機

狀態機的概念

有限狀態機又稱有限狀態自動機,簡稱狀態機,是表示有限個狀態以及在這些狀態之間的轉移和動做等行爲的數學計算模型,用英文縮寫也被簡稱爲 FSM。
FSM 會響應「事件」而改變狀態,當事件發生時,就會調用一個函數,並且 FSM 會執行動做產生輸出,所執行的動做會由於當前系統的狀態和輸入的事件不一樣而不一樣。
算法

問題背景

爲了更好地描述狀態機的應用,這裏用一個地鐵站的閘機爲背景,簡單敘述一下閘機的工做流程:
一般閘機默認是關閉的,當閘機檢測到有效的卡片信息後,打開閘機,當乘客經過後,關閉閘機;若是有人非法經過,那麼閘機就會產生報警,若是閘機已經打開,而乘客仍然在刷卡,那麼閘機將會顯示票價和餘額,並在屏幕輸出「請經過,謝謝」。
在瞭解了閘機的工做流程以後,咱們就能夠畫出閘機的狀態圖,狀態圖以下:
閘機狀態圖
在上圖中,線條上面的字表示的是:閘機輸入事件/閘機執行動做,方框內表示的是閘機的狀態。
除了使用狀態圖來表示系統的工做流程外,咱們也能夠採用狀態表的方式來表示系統的工做流程,狀態表以下所示:




編程

起始狀態 事件 結束狀態 動做
Locked card Unlocked unlock
Locked pass Locked alarm
Unlocked card Unlocked thankyou
Unlocked pass Locked lock

經過上述咱們已經知道閘機的工做流程了,接下來咱們來看具體的實現。數據結構

代碼實現

嵌套的 switch 語句

使用嵌套的 switch 語句是最爲直接的辦法,也是最容易想的方法,第一層 switch 用於狀態管理,第二層 switch 用於管理各個狀態下的各個事件。代碼實現能夠用下述僞代碼來實現:架構

switch(當前狀態)
{
case LOCKED 狀態:
	switch(事件):
	{
	case card 事件:
		 切換至 UNLOCKED 狀態;
		 執行 unlock 動做;
		 break;
	case pass 事件:
		執行 alarm 動做;
		break;
	}
	break;
case UNLOCKED 狀態:
	switch(事件):
	{
		case card 事件:
			執行 thankyou 動做;
			break;
		case pass 事件:
			切換至 LOCKED 狀態;
			執行 lock 動做;
			break;
	}
	break;
}

上述代碼雖然很直觀,可是狀態和事件都出如今一個處理函數中,對於一個大型的 FSM 中,可能存在大量的狀態和事件,那麼代碼量將是很是冗長的。爲了解決這個問題,能夠採用狀態轉移表的方法來處理。框架

狀態轉移表

爲了減小代碼的長度,可使用查表法,將各個信息存放於一個表中,根據事件和狀態查找表項,找到須要執行的動做以及即將轉換的狀態。函數

typedef struct _transition_t
{
	狀態;
	事件;
	轉換爲新的狀態;
	執行的動做;
}transition_t;

transition_t transitions[] = {
	{LOCKED 狀態,card 事件,狀態轉換爲UNLOCKED,unlock動做},
	{LOCKED 狀態,pass 事件,狀態保持爲LOCKED,alarm 動做},
	{UNLOCKED 狀態,card 事件,狀態轉換爲 UNLOCKED,thankyou動做},
	{UNLOCKED 狀態,pass 事件,狀態轉換爲 LOCKED,lock 動做}
};

for (int i = 0;i < sizeof(transition)/sizeof(transition[0]);i++)
{
	if (當前狀態 == transition[i].狀態 && 事件 == transition[i].事件)
	{
		切換狀態爲:transition[i].轉換爲新的狀態;
		執行動做:transition[i].執行的動做;
		break;
	}
}

從上述咱們能夠看到若是要往狀態機中添加新的流程,那麼只須要往狀態表中添加東西就能夠了,也就是說整個狀態機的維護及管理只須要把重心放到狀態轉移表的維護中就能夠了,從代碼量也能夠看出來,採用狀態轉移表的方法相比於第一種方法也大大地縮減了代碼量,並且也更容易維護。
可是對於狀態轉移表來講,缺點也是顯而易見的,對於大型的 FSM 來講,遍歷狀態轉移表須要花費大量的時間,從而影響代碼的執行效率。
那要怎樣設計代碼量少,又不須要以遍歷狀態轉移表的形式從而花費大量時間的狀態機呢?這個時候就須要以面向對象的思想來設計有限狀態機。

學習

面向對象法設計狀態機

面向對象基本概念

以面向對象的思想實現的狀態機,大量涉及了對於函數指針的用法,必須對這個概念比較熟悉優化

上述所提到了兩個設計方法都是基於面向過程的一種設計思想,面向過程編程(POP)是一種以過程爲中心的編程思想,以正在發生的事件爲主要目標,指導開發者利用算法做爲基本構建塊構建複雜系統。
即將所要介紹的面向對象編程(OOP)是利用類和對象做爲基本構建塊,所以分解系統時,能夠從算法開始,也能夠從對象開始,而後利用所獲得的結構做爲框架構建系統。
提到面向對象編程,那天然繞不開面向對象的三個基本特徵:

this

  • 封裝:隱藏對象的屬性和實現細節,僅僅對外公開接口
  • 繼承:使用現有類的全部功能,並在無需從新編寫原來的類的狀況下對這些功能進行擴展,C 語言使用 struct 的特性實現繼承
  • 多態性:使用相同的方法,根據對象的類型調用不一樣的處理函數。

上述對於面向對象的三個基本特徵作了一個簡單的介紹,封裝和繼承的概念都都比較清晰,多態性這個特色可能會有所迷惑,在這裏筆者用在書中看到一個例子來解釋多態性,例子是這樣的:
要求畫一個形狀,這個形狀是多是圓形,矩形,星形,不管是什麼圖形,其共性都是須要調用一個畫的方法來進行繪製,繪製的形狀能夠經過函數指針調用各自的繪圖代碼繪製,這就是多態的意義,根據對象的類型調用不一樣的處理函數。
在介紹了上述很基本的概念以後,咱們來看狀態機的設計。

spa

實現細節

咱們由淺入深地來思考這個問題,首先咱們能夠想到把閘機當作一個對象,那麼這個這個對象的職責就是處理 card 事件(刷卡)和 pass 事件(經過閘機),閘機會根據當前的狀態執行不一樣的動做,也就有了以下的代碼:

enum {LOCKED,UNLOCKED};/*枚舉各個狀態*/

/*定義閘機類*/
typedef struct _turnstile
{
    int state;
    void (*card)(struct _turnstile *p_this);
    void (*pass)(struct _turnstile *p_this);
}turnstile_t;

/* 閘機 card 事件 */
void turnstile_card(turnstile_t *p_this)
{
    if (p_this->state == LOCKED)
    {
        /* 切換至解鎖狀態 */
        /* 執行unlock動做,調用 unlock 函數 */
    }
    else
    {
        /* 執行 thank you 動做,調用 thank you 函數 */
    }
}

/* 閘機 pass 事件*/
void turnstile_pass(turnstile_t *p_this)
{
    if (p_this->state == LOCKED)
    {
        /* 執行 alarm 動做,調用 alarm 函數*/
    }
    else
    {
        /* 狀態切換至鎖閉狀態 */
        /* 執行 lock 動做,調用 lock 函數 */
    }
}

上述代碼的思想實現的有限狀態機相比於前兩種不須要進行大量的遍歷,也不會致使代碼量的冗長,看似已經比較完美了,可是咱們再仔細想一想,若是此時狀態更改了,那 turnstile_card 函數和 turnstile_pass 函數都要更改,也就是說事件和狀態存在着耦合,這與「高內聚,低耦合」的思想所違背,也就是說若是咱們要繼續優化代碼,那須要對事件和狀態進行解耦。

狀態和事件解耦

將事件與狀態相分離,從而使得各個狀態的事件處理函數很是的單一,所以在這裏須要定義一個狀態類:

typedef struct _turnstile_state_t
{
    void (*card)(void);  /* card 事件處理函數 */
    void (*pass)(void);  /* pass 事件處理函數 */
}turnstile_state_t;

在定義了狀態類以後,咱們就可使用狀態類建立 lock 和 unlock 的實例並初始化。

turnstile_state_t locked_state = {locked_card,locked_pass};
turnstile_state_t unlocked_state = {unlocked_card,unlocked_pass};

在這裏須要補充一下上述初始化項裏函數裏的具體實現。

void locked_card(void)
{
    /* 狀態切換至解鎖狀態 */
    /* 執行 unlock 動做 ,調用 unlock 函數 */
}

void locked_pass(void)
{
    /* 執行 alarm 動做,調用 alarm 函數 */
}

void unlocked_card(void)
{
    /* 執行 thank you 動做,調用 thank you 函數 */ 
}

void unlocked_pass(void)
{
    /* 狀態切換至鎖閉狀態 */
    /* 執行 lock 動做,調用 lock 函數 */
}

這樣,也就實現了狀態與事件的解耦,閘機再也不須要判斷當前的狀態,而是直接調用不一樣狀態提供的 card() 和 pass() 方法。定義了狀態類以後,因爲閘機是整個系統的中心,咱們還須要定義閘機類,因爲 turnstile_state_t 中只存在方法,並不存在屬性,那麼咱們能夠這樣來定義閘機類:

typedef struct _turnstile_t
{
    turnstile_state_t *p_state;
}turnstile_t;

到這裏,咱們已經定義了閘機類,閘機狀態類,以及閘機狀態類實例,他們之間的關係以下圖所示:
在這裏插入圖片描述
經過圖中咱們也能夠看到閘機類是繼承於閘機狀態類的,locked_state 和 unlocked_state 實例是由閘機狀態類派生而來的,那最底下的那個箭頭是爲何呢?這是在後面須要講到的對於閘機狀態轉換的處理,在獲取輸入事件調用具體的方法進行處理後,咱們須要修改閘機類的p_state,因此也就有了這個箭頭。
相比於最開始定義的閘機類,這個顯得更加簡潔了,同時 p_state 能夠指向相應的狀態對象,從而調用相應的事件處理函數。
在定義了一個閘機類以後,就能夠經過閘機類定義一個閘機實例:



turnstile_t turnstile;

而後經過函數進行初始化:

void turnstile_init(turnstile_t *p_this)
{
    p_this->p_state = &locked_state;
}

整個系統閘機做爲中心,進而須要定義閘機類的事件處理方法,定義方法以下:

/* 閘機 card 事件*/
void turnstile_card(turnstile_t *p_this)
{
    p_this->p_state->card();
}

/* 閘機 pass 事件 */
void turnstile_pass(turnstile_t *p_this)
{
    p_this->p_state->pass();
}

到這裏,咱們回顧前文所述,咱們已經可以對閘機進行初始化並使得閘機根據不一樣的狀態執行不一樣的處理函數了,再回顧整個閘機的工做流程,咱們發現閘機在工做的時候會涉及到從 locked 狀態到 unlocked 狀態的相互變化,也就是狀態的轉移,所以狀態轉移函數能夠這樣實現:

void turnstile_state_set(turnstile_t *p_this,turnstile_state_t *p_new_state)
{
    p_this->p_state = p_new_state;
}

而狀態的轉移是在事件處理以後進行變化的。那麼咱們能夠這樣修改處理函數,這裏用輸出語句替代閘機動做執行函數:

void locked_card(turnstile_t *p_turnstile)
{
    turnstile_state_set(p_turnstile,&unlocked_state);
    printf("unlock\n");   /* 執行 unlock 動做 */
}

void locked_pass(turnstile_t *p_turnstile)
{
    printf("alarm\n");   /* 執行 alarm 動做*/
}

void unlocked_card(turnstile_t *p_turnstile)
{
    printf("thankyou\n"); /* 執行 thank you 動做*/
}

void unlocked_pass(turnstile_t *p_turnstile)
{
    turnstile_state_set(p_turnstile,&locked_state);
    printf("lock\n");     /* 執行 lock 動做 */
}

既然處理函數都發生了變化,那麼閘機狀態類也應該發生更改,更改以下:

typedef struct _turnstile_state_t
{
    void (*card)(turnstile_t *p_turnstile);
    void (*pass)(turnstile_t *p_turnstile);
}turnstile_state_t;

可是回顧以前咱們給出的閘機類和閘機狀態類的關係,閘機類是繼承於閘機狀態類的,也就是說先有的閘機狀態類後有的閘機類,可是這裏卻在閘機狀態類的方法中使用了閘機類的參數,其實這樣也是可行的,須要提早對閘機類進行處理,總的閘機類狀態類定義以下:

#ifndef __TURNSTILE_H__
#define __TURNSTILE_H__

struct _turnstile_t;
typedef struct _turnstile_t turnstile_t;

typedef struct _turnstile_state_t
{
    void (*card)(turnstile_t *p_turnstile);
    void (*pass)(turnstile_t *p_turnstile);
}turnstile_state_t;

typedef struct _turnstile_t
{
    turnstile_state_t *p_state;
}turnstile_t;

void turnstile_init(turnstile_t *p_this);    /* 閘機初始化 */
void turnstile_card(turnstile_t *p_this);    /* 閘機 card 事件處理 */
void turnstile_pass(turnstile_t *p_this);    /* 閘機 pass 事件處理 */

#endif

上述就是全部的關於狀態機的相關定義了,下面經過上述的定義實現狀態機的實現:

#include <stdio.h>
#include <turnstile.h>

int main(void)
{
    int event;
    turnstile_t turnstile;          /* 閘機實例 */
    turnstile_init(&turnstile);     /* 初始化閘機爲鎖閉狀態 */

    while(1)
    {
        scanf("%d",&event);
        switch(event)
        {
        case 0:
            turnstile_card(&turnstile);
            break;
        case 1:
            turnstile_pass(&turnstile);
            break;
        default:
            exit(0);
        }
    }

上述代碼運行結果以下:
在這裏插入圖片描述

結論

以上即是筆者關於狀態機的所有總結,講述了面向過程和麪向對象兩種實現方法,雖然從篇幅上看面向對象的方法要更爲複雜,可是代碼的執行效率以及長度都要優於面向過程的方法,因此瞭解面向對象的程序設計方法是頗有必要的。

這篇文章是在筆者學習了《程序設計與數據結構》周立功版後的本身的理解,該書的PDF版能夠從立功科技官網的周立功專欄中獲取。

下面給出書籍和文章狀態機代碼彙總的連接:
程序設計與數據結構
連接:https://pan.baidu.com/s/17ZH7Si1f_9My7BulLs8AVA
提取碼:x1im
FSM:
連接:https://pan.baidu.com/s/1qO-Dy6bHukBRGxxQ1-KGJA
提取碼:vyn2





最後,若是您以爲個人文章對您有所幫助,歡迎關注筆者的我的公衆號:wenzi嵌入式軟件
在這裏插入圖片描述

相關文章
相關標籤/搜索