代碼失控與狀態機(上)

来源:https://www.cnblogs.com/Zongsoft/archive/2018/08/06/coding-outcontrol-statemachine-1.html
-Advertisement-
Play Games

不要跟產品經理打架,失控是一種病。我這裡有一劑良藥瞭解下?! ...


前言

前幾天和某某同學吃飯席間,他聊到每當要修改老項目中自己寫的代碼時就痛苦不堪,問我是不是也有同感。我覺得這應該是不少程式猿的心聲,之所以會這樣,大致有兩個主因:

  1. 項目的整體設計很糟糕,只管往上堆砌各種功能、補丁,對於代碼質量和結構關係基本無暇顧及,最終積重難返滑向失控。
  2. 對技術缺乏必要的敬畏心,基礎不夠扎實、知識面較窄,不能(無法)進行合理的規劃,最終導致停留在低水平的代碼堆砌上,只求完成功能就萬事大吉。

程式猿飯桌上總少不了對產品經理的吐槽:“產品經理又對業務流程進行了瘋狂調整,我覺得這會導致狀態機無法支持了。”他的這個槽點讓我一時有些語塞,倒不是懷疑產品經理的腦洞還能大到把狀態機開到失控,只是詫異難道我們還有比狀態機更適合應對業務流程變更的武器嗎?

事實上狀態機對於軟體工程師來說應該是個很基礎的知識點,它原理簡單卻擁有強大的適應力並被廣泛應用 (譬如:游戲開發、工作流、編譯器、正則表達式等解析器中) ,掌握好它的原理和應用,能幫助我們從容應對很多棘手問題,它於程式猿應對複雜流程性問題,就好比醫生使用抗生素應對細菌感染一樣的最佳武器。同時,它還是防止代碼失控的一劑良藥。

基本概念

狀態機一般泛指“有限狀態機(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,
    }
}

上面的代碼雖然看起來沒什麼技術含量,但它已經具備了一個狀態機最基本的三大要素了(狀態狀態驅動遷移判定),針對具體業務場景我們只需完善和優化它的程式結構,底層原理的基本要義其實就是這麼簡單。

Notitle

失控的大腦

人腦是一個很神奇的存在,它很擅長處理抽象思維,對於邏輯推理也有很好的應對能力,但卻有個不擅長處理併發任務的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,以避免因內容陳舊而導致的謬誤,同時亦有更好的閱讀體驗。

掃描關註我們的微信公眾號,可以第一時間看到新文章。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 同源策略(Same origin Policy) 瀏覽器出於安全方面的考慮,只允許與本域下的介面交互。不同源的客戶端腳本在沒有明確授權的情況下,不能讀寫對方的資源。 本域指的是 同協議:如都是http或者https 同功能變數名稱:如都是 http://evenyao.com/a 和 http://even ...
  • 這裡我們以一個簡單的select作為原型來進行說明: 1.獲取/設置當前option的value值 2.獲取/設置當前option的文本:註意:.find("option[text='.......']")適用於input標簽,不適用與select標簽 3.獲取/設置當前option的index: ...
  • 在做圖片上傳的功能時, 使用刪除功能刪除了一張圖片, 然後想重新上傳原來刪除的圖片, 結果預覽不顯示, 也不能上傳成功 解決辦法, 在刪除方法里置空input 拿到input所在的位置, 找到這個input, 然後置空 還有一種方法是來回切換input的屬性 每次刪除圖片後, 改變input的typ ...
  • 1、jQuery選擇器:jQuery選擇器類似於CSS選擇器,用來選取網頁中的元素。 Eg:$("h3").css("background","#09F"); 分析: 獲取並設置網頁中所有<h3>元素的背景 “h3”為選擇器語法,必須放在$()中 $(“h3”)返回jQuery對象 .css()是為 ...
  • 一 數據類型: 基本(值)數據類型: string number undefined null boolean 對象(引用)類型 【 查找對象的屬性時,會查找原型鏈 設置屬性時,一般在構造函數裡面設置,不會查找原型鏈,如果不存在,就添加進這個屬性,並設置值 方法一般在原型中定義 】 【 沒有顯示指定 ...
  • 透切理解面向對象三大基本特性是理解面向對象五大基本原則的基礎. 三大特性是:封裝,繼承,多態 所謂封裝,也就是把客觀事物封裝成抽象的類,並且類可以把自己的數據和方法只讓可信的類或者對象操作,對不可信的進行信息隱藏。封裝是面向對象的特征之一,是對象和類概念的主要特性。 簡單的說,一個類就是一個封裝了數 ...
  • 方法之間調用,可以通過方法名進行調用。但構造方法,無法通過構造方法名來相互調用。 構造方法之間的調用,可以通過this關鍵字來完成。 l 構造方法調用格式: this(參數列表); l 構造方法的調用 l 圖列說明: 1、先執行main方法,main方法壓棧,執行其中的new Person(“張三” ...
  • Set介面,它裡面的集合,所存儲的元素就是不重覆的,通過元素的equals方法,來判斷是否為重覆元素。 HashSet存儲JavaAPI中的類型元素 給HashSet中存儲JavaAPI中提供的類型元素時,不需要重寫元素的hashCode和equals方法,因為這兩個方法,在JavaAPI的每個類中 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...