不要跟產品經理打架,失控是一種病。我這裡有一劑良藥瞭解下?! ...
前言
前幾天和某某同學吃飯席間,他聊到每當要修改老項目中自己寫的代碼時就痛苦不堪,問我是不是也有同感。我覺得這應該是不少程式猿的心聲,之所以會這樣,大致有兩個主因:
- 項目的整體設計很糟糕,只管往上堆砌各種功能、補丁,對於代碼質量和結構關係基本無暇顧及,最終積重難返滑向失控。
- 對技術缺乏必要的敬畏心,基礎不夠扎實、知識面較窄,不能(無法)進行合理的規劃,最終導致停留在低水平的代碼堆砌上,只求完成功能就萬事大吉。
程式猿飯桌上總少不了對產品經理的吐槽:“產品經理又對業務流程進行了瘋狂調整,我覺得這會導致狀態機無法支持了。”他的這個槽點讓我一時有些語塞,倒不是懷疑產品經理的腦洞還能大到把狀態機開到失控,只是詫異難道我們還有比狀態機更適合應對業務流程變更的武器嗎?
事實上狀態機對於軟體工程師來說應該是個很基礎的知識點,它原理簡單卻擁有強大的適應力並被廣泛應用 (譬如:游戲開發、工作流、編譯器、正則表達式等解析器中) ,掌握好它的原理和應用,能幫助我們從容應對很多棘手問題,它於程式猿應對複雜流程性問題,就好比醫生使用抗生素應對細菌感染一樣的最佳武器。同時,它還是防止代碼失控的一劑良藥。
基本概念
狀態機一般泛指“有限狀態機(Finite State Machine)”,《離散數學》中有關於它的專門章節,以下謹為我對相關概念的形式上的非精準釋義,如有出入請以教科書或相關學術資料為準。
- 狀態:顧名思義表示某個時刻系統處於一個特定的階段。通常我們不考慮中間態,也可以把中間態進行退化處理。當狀態發生變更,就叫狀態轉換(Transfer)或狀態遷移(Transition)。
- 事件:驅動系統進行狀態轉換/遷移的源,提供這種源的也常被稱為“觸發器(Trigger)”。
- 行為:當系統進行狀態轉換時進行的響應處理,提供響應處理的程式也常被稱為“處理器(Handler)”。
有了上面的基本概念,我們來看一個最簡單的狀態圖:
你可能會奇怪這個圖怎麼跟網上那些狀態機圖不一樣,連狀態轉換條件都沒有呢?這是因為,我覺得在瞭解狀態機之前,最好先將確立以下兩種概念:
- 狀態驅動: 狀態機負責根據輸入來驅動狀態流轉。
- 遷移判定: 在狀態流轉過程中確定當前狀態是否需要進行轉換/遷移,以及轉換/遷移到哪個狀態中的判定機制。
所以,在常見的狀態機圖中標註的那些狀態轉換條件只是“遷移判定”的一種具體表現形式,它即可以由狀態機內置,也可以是獨立的判定器來處理,又或者由狀態圖預先定義好,如此等等。
建立“狀態驅動”和“遷移判定”這兩個被抽象化的概念,有助於我們深入理解狀態機的機理,並且對我們設計一個魯棒性和擴展性更好到狀態機有實際指導意義。
狀態機圖
以下是表示一個‘簡陋’的 Email 地址格式的解析器狀態圖,狀態遷移條件採用正則表達式來表達,其中圖二又稱為“狀態遷移圖”。
圖一:節點式
圖二:表格式(紅色格表示拒絕或異常;灰色格表示忽略或無意義;其他表示遷移條件)
代碼實現
有了上面的狀態圖,就像建築工人拿到了詳細的建築設計圖紙;現在我們只需要對著狀態機圖,把它映射成代碼即可完成一個基本狀態機。狀態機圖越詳細,實現起來就越容易,同時代碼的可維護性也越好。
public class Email
{
public string Identifier { get; private set;}
public string Host { get; private set; }
public string Domain { get; private set; }
private Email() {}
public static Email Parse(string text)
{
if(string.IsNullOrEmpty(text))
return null;
var state = State.None;
/* The State-Driven */
for(int i=0; i<text.Length; i++) {
var chr = text[i];
switch(state)
{
case State.None:
//do state transition decision
break;
case State.Identifier:
//do state transition decision
break;
case State.Delimiter:
//do state transition decision
break;
case State.Host:
//do state transition decision
break;
case State.Dot:
//do state transition decision
break;
case State.Domain:
//do state transition decision
break;
}
}
return new Email(...);
}
private enum State
{
None,
Identifier,
Delimiter,
Host,
Dot,
Domain,
}
}
上面的代碼雖然看起來沒什麼技術含量,但它已經具備了一個狀態機最基本的三大要素了(狀態、狀態驅動、遷移判定),針對具體業務場景我們只需完善和優化它的程式結構,底層原理的基本要義其實就是這麼簡單。
失控的大腦
人腦是一個很神奇的存在,它很擅長處理抽象思維,對於邏輯推理也有很好的應對能力,但卻有個不擅長處理併發任務的Bug。比如當面臨很多個邏輯分支,各分支的判定條件彼此關聯,大腦很快就會陷入繁雜的狀態中無法自拔。
表現在解決複雜流程相關的任務時就是,寫著寫著你會發覺腦子好像不夠用了,而程式中的 Bug 卻像打地鼠游戲中的老鼠一樣層出不窮。不難想象,即使腦力過人的你在勉強寫完後的某天,產品經理帶著他的腦洞又來找你了,在他的威逼利誘下你打開了一個月前的代碼,忽然,覺得還是抱著產品經理同歸於盡算了……
這大概是某某同學,面對自己曾經的代碼時痛苦的根源所在,因為普通人面對複雜流程問題時,終歸受人腦算力所限。本質上這是人腦算力有限的一個困境,人類解決這個困境的一個行之有效的辦法就是“分而治之”,即將一個大問題或複雜問題不斷進行分解分化,直至達到人腦能相對輕鬆理解和處理的程度。
為什麼說狀態機是解決此類問題的一劑良藥?
通過狀態機圖可以很容易的看到它天生具有“分解、分化”的特征,一個複雜的流程由多個流程節點組成,這些節點可以理解為對流程的分解,流程節點之間的轉移條件(遷移判定)可以看成是被分化後的邏輯分支,如果大腦直接處理整個流程很容易陷入紛擾的流程分支和各種細節中,但是,當我們把眼光聚焦在某個流程節點和它的轉移條件上的時候,大腦需要處理的信息量就變得非常少了。
所以,當我們直面一個繁雜的流程圖的時候,第一感覺就是複雜、腦闊痛,這其實是大腦的正常反應,當你把眼光聚焦到“Start”節點上,並順著它往下推,每個節點的信息量一定是大腦能輕鬆處理的量級,這種順藤摸瓜的方式反過來也正是流程設計的套路。我有時會被自己剛畫完的狀態機圖給驚訝到,怎麼這麼複雜?因為當我一點點把細節補充上去後,整體性自然會變得複雜了,但是局部依然是簡單的,而簡單就是可靠、魯棒、可維護性的同義詞。
代碼只是狀態機圖的相關元素的一種表現形式,它與“節點式”或“表格式”的狀態機圖並無本質不同。
另外,狀態機圖相對代碼而言,它更專註於流程本身,而代碼畢竟是具體實現層面的東西,除了流程本身還包括程式結構、業務代碼等與流程無關的代碼,這些額外的東西對我們解讀流程造成了干擾,因而相對純粹的狀態機圖就好比是代碼實現的“地圖”。
經過一段時間後,我們可能已經不記得實現細節了,這時看著狀態機圖來進行代碼解讀和修改將會大大提高效率和準確度,這就是提升代碼可維護性的有力手段。
如上,狀態機是防止代碼失控的一劑良藥,製備完善的狀態機圖就是防止代碼失控的一種有效手段。
課後作業
試著脫離狀態機圖擼一個“成員訪問表達式”的解析器去體驗下失控的感受。下次,我們將一起來實現這個東西。
附註:
成員訪問表達式:訪問對象方法、屬性、欄位、索引器(包括字典、列表)這些成員的表達式,其中方法和索引器(包括字典、列表)的參數支持常量和成員表達式(即表達式遞歸)。詳細的文法請參考C#語言手冊。譬如:
PropertyA
.ListProperty[100]
.MethodA(PropertyB, 'StringConstant for Arg2', 200, ['key'])
.Children['arg1', 'arg2']
提示:
本文可能會更新,請閱讀原文:https://zongsoft.github.io/blog/zh-cn/zongsoft/coding-outcontrol-statemachine-1,以避免因內容陳舊而導致的謬誤,同時亦有更好的閱讀體驗。
掃描關註我們的微信公眾號,可以第一時間看到新文章。