軟體教練說 | 從狀態機到狀態模式
臨近春節,正好趕上和團隊小夥伴review設計,看到大家對狀態機情有獨鍾,有感而發……
狀態機本身是一個數字電路領域的詞彙,因為嵌入式領域與數位電路設計在早期結合緊密,所以在嵌入式開發領域,尤其是監控領域,也把狀態機的設計設計模式引入到軟體領域,採用狀態機的方式來設計程式,我們經常會看到整個應用在不同的狀態之間進行跳轉,在不同狀態下系統(或子系統、模組)表現的行為不一樣。採用狀態機的方式解決問題自然而然的成為了嵌入式軟體工程師的幾乎必備技能,它能幫助設計者把一個複雜的系統在執行時分解為不同的狀態模式,進而驅動整個系統迴圈反覆的一直執行下去。
而隨著面向物件設計思想的普及,從OO的角度來講,用狀態模式來替換部分情形的狀態機實現,能更好的簡化整個程式實現以及維護難度,本文試圖談談個人的理解,與各位讀者探討。
狀態機
狀態機是一個行為模型,在建模過程中會分析出有限數目狀態,因此,有些開發者也會稱之為有限狀態機(FSM,finite state machine)。
在軟體設計領域,因為處理的是非實體輸入,通常會有一個一個虛擬的執行環境,也會被稱之為虛擬狀態機(VFSM)。
對比來看,在硬體設計領域,狀態機的輸入輸出通常為布林型(二值輸入),邏輯條件比較簡單。但軟體的輸入會更加複雜的多,面臨的是多值的輸入以及複雜的邏輯條件。
狀態機是有正式的數學定義的,它包含六個維度:
(S,s_0, I, O, F, G)(S,s0,I,O,F,G), 分別代表有限的狀態集S,初始狀態s_0s0, 有限的輸入集I, 有限的輸出集O,狀態轉移方程F,輸出方程G。
從這個嚴格的數字定義上來看,如果我們採用狀態機來設計我們的嵌入式應用的時候,缺少了部分維度,或者維度集合識別不全面,就會導致設計存在隱患。
這裡面的s_0s0非常重要,它告訴我們一個狀態機必須有一個初始狀態,之後才能根據輸入在不同狀態之間跳轉。
舉個例子,如果狀態集合S沒有識別完全,就會導致狀態跳轉到未知狀態,應用無法完成後續狀態轉換,如果輸入沒有識別完全,就會導致我們的設計對未知輸入考慮不充分,導致F無法按照實際期望完成我們的需求,如果狀態轉移方程設計的不正確,也會導致狀態紊亂……
另外,可以理解為狀態變換就像角色扮演一樣:一個普通的不能再普通的彼得·本傑明·帕克,平時正常工作、上下班、談戀愛,處理的日常輸入和你我沒什麼不同。
但如果突然來了一個輸入i_1i1,他突然變身(FF)成為蜘蛛俠,處理輸入的方法GG就和我們完全不一樣了,狀態機其實就是在做這樣的事情,讓一個人切換不同的狀態來表現出不一樣的行為模式。
作為一個機器(machine),狀態機能夠根據當前處於的狀態完成不同的任務:
- 邏輯資料處理:處理輸入,產生輸出。
- 狀態轉換處理:根據輸入,跳轉到新的狀態。
狀態機的正式定義
狀態機從簡單到複雜可以分為三個階段:
- 輸出僅和狀態有關係,一般稱之為摩爾有限狀態機(Moore FSM)
- 初始化為狀態s_0s0
- 無論輸入如何,僅根據當前的狀態產生輸出,函式形式為:G:S->OG:S−>O,
- 根據不同的輸入和當前狀態轉移到下一個狀態,函式形式為:F:(S, I)->SF:(S,I)−>S
- 輸出僅和狀態以及輸入有關係,一般稱之為米利有限狀態機(Mealy FSM)
- 初始化為狀態s_0s0
- 根據當前的狀態以及輸入產生輸出,函式形式為:G:(S,I)->OG:(S,I)−>O,
- 根據不同的輸入和當前狀態轉移到下一個狀態,函式形式為:F:(S, I)->SF:(S,I)−>S
- 引入新的維度CC代表上下文,這個時候狀態機就變成比較複雜的有限狀態機:
- 初始化狀態s_0s0,上下文c_0c0
- 根據當前的狀態、狀態下上文、輸入產生輸出、及上線文變化:G:(S,C,I)->(C,O)G:(S,C,I)−>(C,O)
- 根據當前的狀態、狀態上下文、輸入轉移到下一個狀態及上下文,並傳遞上下文:F:(S,C,I)->(S,C)F:(S,C,I)−>(S,C)
簡單示例
一般來講做狀態機分析可以採用一個表格方式或者狀態圖來實現。我們舉個簡單的控制燈光的案例來便於理解。
- 一個簡單的例子:在我很小的時候,家裡採用的燈光開關是拉線的,就是拉一下開燈,再拉一下關燈。這是一個非常簡單的摩爾狀態機,只要有輸入就進行狀態轉換。
- 一個略微複雜的燈光系統,在酒店的時候經常碰到這樣的開關,只要按一下開關按鈕,燈就會切換開關狀態,並且開關按鈕沒有狀態顯示。
- 當按下對應開關的時候,對應的燈會調整狀態。
- 當按下總開關的時候,所有的燈會進入關閉狀態。
- 接受手機app傳送的簡單指令:開燈或者關燈
表格分析
當前狀態 | 輸入 | 輸出 | 下一狀態 |
---|---|---|---|
ON | 按下訊號 | 關燈 | OFF |
ON | APP命令ON | 無動作 | ON |
ON | APP命令OFF | 關燈 | OFF |
ON | 全關 | 關燈 | OFF |
OFF | 按下訊號 | 開燈 | ON |
OFF | APP命令ON | 開燈 | ON |
OFF | APP命令OFF | 無動作 | OFF |
OFF | 全關 | 無動作 | OFF |
狀態圖分析
有時候採用狀態圖來描述狀態轉換會更為直觀,分析步驟如下:
- 識別出所有的狀態集合
- 識別出所有的輸入集合
- 識別兩個狀態之間的轉換函式
- 識別出每個狀態下不同輸入的輸出函式
可以畫出和下面的示意圖,使得其更加直觀:
經過這樣的分析和設計,我們會吧整個狀態轉換的過程識別清楚,確保狀態轉化你的路徑不會遺漏,
程式碼範例
這樣我們嘗試些個簡單的狀態機,根據輸入的不同命令,來進行狀態切換,示例程式碼如下,限於篇幅的原因,忽略掉一些具體的細節實現(這部分邏輯不復雜,去掉後更容易看清全貌,避免大腦棧溢位:-D),實際上很多程式碼幾乎都是按照這個思路來操作的:
狀態模式從上面的程式碼中我們可以看到,分別針對F,G進行了獨立實現,這反映了我們分析和設計過程。但在實際嵌入式開發過程中,F,G關係緊密,為了效能等,需要進行F,G的融合,狀態輸入處理和狀態轉移合併為一個介面來實現。
在面向物件設計領域,我比較喜歡用狀態模式來取代狀態機進行設計。這樣有兩個好處:
- 簡化狀態處理過程:在狀態機實現中,我們需要維護F,GF,G兩個狀態,對輸入輸出集合也要進行維護,因為它是一個多維度的資訊變化,在實際的專案中會非常複雜,耦合性比較高。
- 降低響應變化難度:在設計之初不可能識別出所有的狀態,隨著專案需求的不斷變化,識別出來的狀態越多,使得維護整個設計以及實現變得越來越困難,並且違背了開閉原則。這也是狀態模式出現的原因。
狀態設計模式(State Pattern)的定義
按照設計模式的分類, 狀態模式屬於一個行為型的設計模式。它支援一個物件隨著內部狀態的變化改變它的行為,使得看起來就像例項物件本身對應的class改變了一樣。
從這個定義來看,雖然物件本身的行為發生了改變,但實際上對外提供的介面契約並沒有改變。狀態模式試圖把物件上下文維護的屬性與對應的操作分離開來,把不同狀態情況下要做的操作委託(delegate)出去。
UML 圖
為了便於理解,我畫出一個簡單的UML圖來描述狀態模式。
- 在Context的例項物件context中儲存一個到不同狀態的引用物件並且把所有具體的狀態相關工作委託給它,context通過State介面與具體的狀態物件,同時提供一個改變狀態的介面用以切換到新的狀態。
- State 介面聲明瞭狀態相關的方法,這些方法應該仔細分析設計,保證在不同狀態下都有意義,避免介面臃腫和存在無意義的方法。
- context和concretestate都應該有能力通過替換狀態物件改變系統狀態。
模式解析
從狀態模式的典型UML來看,呼叫者Client與上下文模型Context互動,上下文模型一般作為業務領域對外唯一的服務介面隔離了內部的狀態變化。Context並未與具體的State類耦合,而是通過聚合識別出的State抽象介面來隔離了這種變化。這樣使得Context類不再關心具體的State變化,State的變化通過當前的狀態根據具體輸入來決定跳轉到新的狀態,並通知Context來更改state指向的具體例項。
試著想一下,隨著系統的變化,有一個新的狀態被識別出來的時候,我們只需要實現一個ConcreteStateC,並更改現在State實現類的程式碼來相應如何跳轉到新的狀態介面,無需更改Context以及Client的任何程式碼,非常好的支援了OCP(開閉原則),同時,每個類都僅有一個原因來進行修改(單一職責)。
程式碼範例
我嘗試把上文中我們用狀態機來實現的功能重新用狀態模式來實現一次。通過構造上下文類,狀態介面以及具體的狀態類來一步一步的而言是如何用狀態類取代狀態機的實現。
上下文類的宣告
先來看一下上下文標頭檔案的宣告,提供了兩個介面滿足基本的業務需要,同時提供了更改當前狀態的介面便於狀態的切換。
狀態介面以及狀態類宣告忽略掉建構函式等,我們看到這個類提供的基本的業務處理介面以及通過一個指標來儲存當前狀態的處理例項。
再來看一下State的相關定義
接下來抽取部分程式碼,來演示狀態類的具體實現,為了減少不必要的篇幅,我僅展示一個狀態的實現。狀態類的部分實現
實現了兩個基本的狀態類,我們再放出上下文類Context的實現例子LightingController。可以看到這個類的實現非常簡單,所有的狀態輸入處理都委託給了具體的狀態類,上下文類的實現
從上面的整體來回顧,還有一個明顯的特徵是在採用狀態機設計的時候,我們定義了所有的狀態(列舉型),但通過狀態模式來設計的時候,我們把這部分的內容轉移到了具體的狀態類中,使得上下文類以及呼叫者不再對這個內容有感知,把底層實現與上層應用完全的隔離開來,這也是大多數設計模式秉持的設計原則(SOLID)。通過這個實現,如果呼叫者傳送指令到controller,都會轉到當前狀態的實現中。把可能預見的頻發變化隔離開了,使得controller相對穩定,同時相應的跳轉到不同狀態的職責也從controller中分離出來並轉移給具體的狀態類。
本文程式碼採用TDD方式進行實現,附上部分gtest/gmock程式碼供參考。
Z3:通過這個簡單的案例與大家分享了我對狀態機以及狀態模式的認識,希望與大家共同探討。限於本文篇幅,我們這裡介紹了簡單的狀態機以及狀態模式的實現,在下一篇中,我會展示狀態模式如何響應需求的變化。系列文章:設計原則101:從外門弟子到化神
標籤:設計原則設計模式面向物件修仙狀在本文寫作過程中參考了公開的相關文章和著作,在此不再一一列出。
感謝樊偉老師幫忙審閱本文。