上篇文章:狀態機編程實例-狀態表法
使用狀態表法,實現了炸彈拆除小游戲的狀態機編程,這也是介紹的狀態機編程的第二種方法。
本篇,繼續介紹狀態機編程的第三種方法:面向對象的設計模式。此方法從名字上看,用到了面向對象的思想,所以本篇的代碼,需要以C++為基礎,利用C++中“類”的特性,實現狀態機中狀態的管理。
1 面向對象的狀態設計模式
面向對象的狀態設計模式,其核心思想在于:它是通過不同的類來表示不同的狀態,當狀態機從一個狀態轉換到另一個狀態時,它表現為在運行時改變自己的類。
回顧第一篇時繪制的炸彈拆除小游戲的狀態圖,有2個狀態和4個事件:
使用面向對象的狀態設計模式,此例子中的****兩個工作狀態,就要設計為兩個類,如下圖中的設置狀態(SettingState)和倒計時狀態(TimingState)。
先簡單說明一下下面這個圖,此圖屬于UML類圖,相關介紹可參考: UML簡介與類圖詳解
Bomb3與BombState是組合關系,BombState是一個抽象類,SettingState與TimingState繼承自BombState,屬于繼承關系
可以注意到,此模式引入了一個炸彈狀態的****抽象基類BombState,用于派生具體的工作狀態類。
該抽象類為炸彈的兩個工作狀態聲明了一些公共的接口:onUP、onDOWN、onARM和onTICk,這些接口對應于此例子中的四個事件 。
兩個工作狀態類:SettingState類和TimingState類,通過定義自己的onUP等操作,實現各自狀態類需要處理的功能。
這種設計模式下:
- 如果需要增加新的事件,則需要給抽象類BombState增加新的操作
- 如果需要增加新的狀態,則需要給抽象類BombState增加新的子類
此模式還設計了一個上下文類Bomb3,它通過一個抽象類BombState的指針來實現炸彈狀態的維護。
什么是上下文?
編程中提到的上下文(context),可以理解為環境或語境,每一段程序都有很多的外部變量,一旦寫的一段程序中有了外部變量,這段程序就是不完整的,不能獨立運行,要想讓他運行,就必須把所有的外部變量的值一個一個的全部傳進去,這些值的集合就叫作上下文。
本例中,BombState的運行,就需要一個上下文類作為其參數,這個參數就是Bomb3類。
此外,它還包含需要用到的****擴展狀態變量 :
- timeout(超時時間)
- code(用戶輸入的拆除密碼)
- defuse(默認的拆除密碼)
并通過提供對BombState一樣的接口,即每派生一個事件對應一個操作。
在上下文類Bomb3中的事件處理,是通過state_指針實現的,它代表了對當前狀態對象的全部特定請求,狀態的改變對應于當前工作狀態類對象的改變,通過上下文操作tran()實現。
2 實現
介紹了面向對象的狀態設計模式后,下面來看下如何使用C++語言進行對應的代碼實現。
2.1 類的結構
首先來看下要實現的幾個類的結構定義。
2.1.1 狀態基類與派生類
下面是炸彈狀態基類(BombState)的結構,以及派生的兩個具體狀態類(SettingState和TimingState)的結構。
class Bomb3; //事先聲明炸彈業務類
?
//炸彈狀態基類
class BombState
{
public:
virtual void onUP(Bomb3 *) const {}
virtual void onDOWN(Bomb3 *) const {}
virtual void onARM(Bomb3 *) const {}
virtual void onTICK(Bomb3 *, uint8_t) const {}
};
?
//設置狀態-類,繼承于炸彈狀態基類
class SettingState : public BombState
{
public:
virtual void onUP(Bomb3 *context) const;
virtual void onDOWN(Bomb3 *context) const;
virtual void onARM(Bomb3 *context) const;
};
?
//倒計時狀態-類,繼承于炸彈狀態基類
class TimingState : public BombState
{
public:
virtual void onUP(Bomb3 *context) const;
virtual void onDOWN(Bomb3 *context) const;
virtual void onARM(Bomb3 *context) const;
virtual void onTICK(Bomb3 *context, uint8_t fine_time) const;
};
注意這里用到了C++虛函數的特性。
虛函數,是指被virtual關鍵字修飾的成員函數。
虛函數的作用:
- 實現動態聯編,在函數運行階段動態的選擇合適的成員函數
- 實現多態性(Polymorphism),多態性是將接口與實現進行分離;用形象的語言來解釋就是實現以共同的方法,但因個體差異,而采用不同的策略。
虛函數主要通過V-Table虛函數表來實現,該表主要包含一個類的虛函數的地址表,可解決繼承、覆蓋的問題。當我們使用一個父類的指針去操作一個子類時,虛函數表就像一個地圖一樣,可指明實際所應該調用的函數。
此外,對事件的處理,用到了指向類對象的指針(Bomb3 *context
)
指針也就是內存地址,指針變量是用來存放內存地址的變量,不同類型的指針變量所占用的存儲單元長度是相同的,而存放數據的變量因數據的類型不同,所占用的存儲空間長度也不同。
有了指針以后,不僅可以對數據本身,也可以對存儲數據的變量地址進行操作。
創建對像時,編譯系統會為每一個對像分配一定的存儲空間,以存放其成員,對象空間的起始地址就是對象的指針。可以定義一個指針變量,用來存和對象的指針。
2.1.2 炸彈業務類
炸彈業務類,也就是上面提到的上下文類。
class Bomb3
{
public:
Bomb3(uint8_t defuse) : m_defuse(defuse) {}
?
void init(); //狀態機初始化接口
?
//處理各種事件
void onUP();
void onDOWN();
void onARM();
void onTICK(uint8_t fine_time);
?
private:
//進行狀態轉換
void tran(BombState const *target);
?
private:
BombState const *m_pState; //[狀態變量]
uint8_t m_timeout; // 爆炸前的秒數
uint8_t m_code; // 當前輸入的解除炸彈的密碼
uint8_t m_defuse; // 解除炸彈的拆除密碼
uint8_t m_errcnt; // 當前拆除失敗的次數
?
private:
SettingState const m_settingState; //[設置狀態]
TimingState const m_timingState; //[倒計時狀態]
?
friend class SettingState;
friend class TimingState;
};
注意這里又用到了C++的友元特性。
友元包括友元函數與友元類,這里先介紹下本例使用到的友元類 。
友元類的作用:如果把在A類(如本例中的上下文類Bomb3)中聲明了友元類B(如本例中的SettingState和TimingState),那么A類的所有成員函數,可以被B類的所以成員函數訪問。
友元使用前提:某個類需要實現某種功能,但是這個類自身,因為各種原因,無法自己實現,需要借助于“外力”才能實現。
本例中,SettingState和TimingState,需要借助上下文類Bomb3,實現狀態轉換等功能
2.2 類的具體實現
2.2.1 狀態基類與派生類
體會友元類的用法:Bomb3中聲明了SettingState是友元,SettingState則可以訪問Bomb3的成員變量(如m_timeout變量)和成員函數(如tran函數)。
體會上下文類Bomb3的作用:設置狀態SettingState和倒計時狀態TimingState,都是操作Bomb3這個上下文類,實現對應狀態下的業務功能。
//---------------設置狀態-類,具體實現---------------
void SettingState::onUP(Bomb3 *context) const
{
if (context- >m_timeout < 60)
{
++context- >m_timeout;
bsp_display_set_time(context- >m_timeout);
}
}
void SettingState::onDOWN(Bomb3 *context) const
{
if (context- >m_timeout > 1)
{
--context- >m_timeout;
bsp_display_set_time(context- >m_timeout);
}
}
void SettingState::onARM(Bomb3 *context) const
{
context- >m_code = 0;
context- >tran(&context- >m_timingState); //[轉換到倒計時狀態]
}
?
//---------------倒計時狀態-類,具體實現---------------
void TimingState::onUP(Bomb3 *context) const
{
context- >m_code < <= 1;
context- >m_code |= 1;
bsp_display_user_code(context- >m_code);
}
void TimingState::onDOWN(Bomb3 *context) const
{
context- >m_code < <= 1;
bsp_display_user_code(context- >m_code);
}
void TimingState::onARM(Bomb3 *context) const
{
if (context- >m_code == context- >m_defuse)
{
context- >tran(&context- >m_settingState); //[轉換到設置狀態]
bsp_display_user_success(); //炸彈拆除成功
context- >init();
}
else
{
context- >m_code = 0;
bsp_display_user_code(context- >m_code);
bsp_display_user_err(++context- >m_errcnt);
}
}
void TimingState::onTICK(Bomb3 *context, uint8_t fine_time) const
{
if (fine_time == 0)
{
--context- >m_timeout;
bsp_display_remain_time(context- >m_timeout);
if (context- >m_timeout == 0)
{
bsp_display_bomb(); //顯示爆炸效果
context- >init();
}
}
}