如何實現一個狀態機?

来源:https://www.cnblogs.com/1992monkey/archive/2022/07/20/16497563.html
-Advertisement-
Play Games

何為狀態機? 從字面上簡單粗暴地理解,狀態機是一個跟狀態有關的機器,但其實狀態機並不是一種物理機器,而是一種模型,一種表達事物狀態及狀態變化過程的數學模型。 狀態機全稱是有限狀態機(finite-state machine,縮寫:FSM)或者有限狀態自動機(finite-state automato ...


何為狀態機?

  從字面上簡單粗暴地理解,狀態機是一個跟狀態有關的機器,但其實狀態機並不是一種物理機器,而是一種模型,一種表達事物狀態及狀態變化過程的數學模型。
  狀態機全稱是有限狀態機(finite-state machine,縮寫:FSM)或者有限狀態自動機(finite-state automaton,縮寫:FSA),是自動機理論的研究對象。狀態機擁有有限數量的狀態,每個狀態可以遷移到零個或多個其它狀態,狀態機的狀態及遷移過程可以用有向圖來表示。

狀態機用來幹啥?

  上面介紹了狀態機的概念,很多同學可能會說:既然狀態機是數學領域中的理論,而我是程式員,這跟我有什麼關係呢?確實,狀態機是屬於數學理論,要深入研究需要掌握離散數學等專業知識,但這並不意味著電腦領域不會用到,畢竟電腦科學中太多東西都是以數學作為基石的。
  在電腦科學中,或者乾脆把範圍直接縮小到我們程式員的日常開發中,我們或多或少都會接觸到狀態機,例如Android的MediaPlayerMediaCodec,其實現框架裡面就包含了大量的狀態管理,iOS的GKState也使用了狀態機來管理多種狀態。
  其實,在軟體開發裡面,我們更多地是結合自動機理論和軟體設計思想來設計編程模式,以此構建出更加優秀的軟體。GoF 23種軟體設計模式中的狀態模式就是一種基於狀態的設計模式。

狀態機的元素

  狀態機中包含哪些元素呢?一般來講,一個狀態機包含如下元素:

  • 狀態

  即狀態機中包含的有限個數的狀態。

  • 行為

  即狀態對應的一系列行為表現。

  • 事件

  即觸髮狀態發生改變的事件。

  • 轉換

  即狀態改變的過程。

  例如:我們每天經歷的白天夜晚可以看做是一個狀態機,早晨太陽從東邊升起,我們迎來了美好的白天,白天我們會吃飯、上班、運動,等到傍晚太陽從西邊落下的時候,我們便進入了靜謐的夜晚,晚上我們會看電視、學習、睡覺,如果用一個有向圖來表示這個過程,大概會是這樣:

狀態機_白天夜晚

  在白天夜晚狀態機裡面,白天和黑夜屬於狀態,白天吃飯上班運動、夜晚看電視學習睡覺是狀態對應的行為表現,日出和日落是觸髮狀態轉換的事件,夜晚經過日出轉換為白天、白天經過日落轉換為夜晚表示狀態轉換的過程。

狀態模式

  狀態設計模式是GoF提出的23種設計模式之一,可以看做是一種基於狀態機的設計模式,在狀態設計模式裡面,包含了與狀態機對應的各項元素,即:狀態、行為、事件、轉換。設計模式和具體的編程語言無關,因此狀態設計模式也可以用多種語言來實現。

狀態模式的適用場景

  我們在什麼情況下需要使用狀態模式呢?一般來講,我們在編碼的時候,如果發現對象在不同場景或不同階段會表現出不同的行為,而且行為控制邏輯比較複雜、容易混亂的時候,我們就可以考慮使用狀態模式。在狀態模式裡面,我們可以根據業務邏輯為對象劃分出有限個數的狀態,每個狀態內部都封裝好對應的行為,要改變對象的行為,只需要簡單地改變對象的狀態即可,我們可以“面向狀態編程”了!這樣原本複雜的糅雜在一起的邏輯,就一下變得清晰明瞭了。

通過狀態模式實現狀態機

  接下來,我們將通過一個完整的示例,來演示如何通過狀態設計模式來實現一個狀態機。在示例裡面,我們會實現上面的白天夜晚狀態機,鑒於面向對象的思想能夠清晰地表達狀態機中的各種元素,因此我們選用當下比較流行的Kotlin作為編碼語言。

定義狀態及行為

  首先,我們來定義狀態。白天夜晚狀態機包含白天和夜晚兩個狀態,兩個狀態都會表現出對應的行為,但是各自的行為是不一樣的,因此,可以通過介面+實現類的方式來定義狀態。這裡我們抽象出了一個狀態介面IState,併在IState中聲明瞭表達狀態行為的run()方法,然後實現了IState的3個子類IdleStateDayStateNightState,分別表示空閑狀態、白天狀態和夜晚狀態,其中,IdleState僅作為狀態機的起始狀態,在示例裡面沒有體現太多實際意義,DayStateNightState在實現run()方法時,通過輸出一段日誌來表示狀態執行的具體行為。
  IState介面:

/**
 * 狀態介面
 */
interface IState {

    /**
     * 狀態要執行的行為
     */
    fun run()
}

  DayState白天狀態類:

/**
 * 白天狀態
 */
class DayState : IState {

    init {
        run()
    }

    override fun run() {
        println("進入白天,吃飯、上班、運動!")
    }
}

  NightState夜晚狀態類:

/**
 * 夜晚狀態
 */
class NightState : IState {

    init {
        run()
    }

    override fun run() {
        println("進入夜晚,看電視、學習、睡覺!")
    }
}
定義事件

  然後我們來定義狀態機的事件。在白天夜晚狀態機中,白天狀態經過日落轉為夜晚狀態,夜晚狀態經過日出轉為白天狀態,因此,狀態機中包含兩個事件,即日出和日落。
事件:

/**
 * 事件-日出
 */
const val STATE_EVENT_SUNRISE = "sunrise"

/**
 * 事件-日落
 */
const val STATE_EVENT_SUNSET = "sunset"

 

狀態轉換

  然後,我們來實現狀態的轉換。為了集中處理狀態的轉換,我們決定封裝一個專門的類StateManager來進行管理。首先,我們抽象出StateManager的父介面IStateManager,用以聲明StateManager中需要實現的各個屬性及方法。
  狀態管理介面IStateManager

/**
 * 狀態管理介面
 */
interface IStateManager {

    /**
     * 當前狀態
     */
    val state: IState

    /**
     * 根據事件轉換狀態
     *
     * @param event 事件
     */
    fun transitionState(event: String)
}

  IStateManager中聲明瞭表示當前狀態的變數state,同時聲明瞭transitionState(event: String)方法用來狀態轉換。
  狀態管理類StateManager

/**
 * 狀態管理類
 */
class StateManager : IStateManager {

    override var state: IState = IdleState()

    override fun transitionState(event: String) {
        state = when (event) {
            STATE_EVENT_SUNRISE -> DayState()
            STATE_EVENT_SUNSET -> NightState()
            else -> IdleState()
        }
    }
}

  至此,白天夜晚狀態機需要的狀態、行為、事件、轉換四個元素就已經備齊了,接下來我們可以運行狀態機了。

運行狀態機

  我們通過模擬白天夜晚變化的情境,來運行狀態機。我們通過定時任務模擬了一天當中從0點到次日0點之間24小時的變化,定時任務中1秒錶示現實中的1個小時,6點日出時將狀態機的當前狀態轉換為白天狀態,18點日落時將狀態機的當前狀態轉換為夜晚狀態。
  模擬情境StatePatternSceneSimulator

/**
 * 狀態模式場景模擬器
 *
 * 通過定時任務模擬一天24小時變化,1秒錶示1小時,6點日出,轉換為白天狀態,18點日落,轉換為夜晚狀態
 */
class StatePatternSceneSimulator : ISceneSimulator {

    /**
     * 狀態管理介面實例
     */
    private val stateManager: IStateManager by lazy { StateManager() }

    /**
     * 當前時間,即幾點
     */
    private var time: Int = 0

    override fun run() {
        val countDownLatch = CountDownLatch(240)
        val timer = Timer()
        timer.scheduleAtFixedRate(object : TimerTask() {
            override fun run() {
                println("現在是 $time 點")
                if (time == 6) {
                    // 6點日出,轉換為白天狀態
                    stateManager.transitionState(STATE_EVENT_SUNRISE)
                } else if (time == 18) {
                    // 18點日落,轉換為夜晚狀態
                    stateManager.transitionState(STATE_EVENT_SUNSET)
                }
                if (time < 23) {
                    time++
                } else {
                    time = 0
                }
                countDownLatch.countDown()
            }
        }, 0, 1000)
        countDownLatch.await()
    }

    companion object {

        /**
         * 運行場景
         */
        fun run() {
            StatePatternSceneSimulator().run()
        }
    }
}

  接下來,我們在測試代碼中,調用StatePatternSceneSimulator來運行模擬情境。

/**
 * 狀態模式測試類
 */
class Main {

    /**
     * 演示狀態模式
     */
    @Test
    fun main() {
        StatePatternSceneSimulator.run()
    }
}

  執行main()函數之後,控制台將會輸出如下日誌:

現在是 0 點
現在是 1 點
現在是 2 點
現在是 3 點
現在是 4 點
現在是 5 點
現在是 6 點
進入白天,吃飯、上班、運動!
現在是 7 點
現在是 8 點
現在是 9 點
現在是 10 點
現在是 11 點
現在是 12 點
現在是 13 點
現在是 14 點
現在是 15 點
現在是 16 點
現在是 17 點
現在是 18 點
進入夜晚,看電視、學習、睡覺!
現在是 19 點
現在是 20 點
現在是 21 點
現在是 22 點
現在是 23 點
現在是 0 點

  通過日誌,我們可以看到隨著時間的變化,狀態機的狀態在白天和夜晚兩個狀態中來迴轉換。至此,我們便通過狀態設計模式實現了白天夜晚狀態機!

狀態機的實際應用

  示例中的白天夜晚狀態機,只是一個最簡單的狀態機,在實際開發中,我們遇到的業務場景會比這個複雜得多,如果要通過狀態機來實現這些複雜業務,狀態機的設計本身也會變得更加複雜,我們可以通過多種形式對簡單的狀態機進行拓展,來解決更加複雜的問題場景。

分層狀態機

  所謂分層狀態機,是指狀態可以像類的繼承那樣,自上而下包含多個層級。例如在白天夜晚狀態機裡面,白天狀態包含吃飯、上班、運動等行為,起初這些行為可通過簡單的代碼進行描述,吃飯就是“吃飯”,上班就是“上班”,運動就是“運動”,但是隨著業務的深入,邏輯會變得越來越複雜,吃飯不再是簡單地描述為“吃飯”,而是需要描述清楚“吃的什麼菜,吃了多少,和誰一起吃的”,上班不再是簡單地描述為“上班”,而是要描述清楚“上班幹了些什麼,有沒有會議,是正常上班還是加班”,運動也不再是簡單地描述為“運動”,而是要描述清楚“做的那種類型的運動,運動時長是多少,消耗了多少熱量”,試想一下,如果把這些邏輯繼續放在白天狀態裡面,那麼白天狀態的邏輯會變得越來越複雜、越來越臃腫,甚至混亂出錯,此時,我們可以考慮將白天狀態進一步拆分,我們可以根據不同的行為,將白天狀態拆分為吃飯狀態、上班狀態、運動狀態等子狀態,每一種子狀態各自管理自己的業務,這樣拆分之後,白天狀態臃腫的邏輯被劃分到了每個子狀態中,一下子就變得清爽乾凈了!

併發狀態機

  所謂併發狀態機,是指不止存在一種狀態機,而是多種狀態機並存。例如代碼裡面既有維護日夜交替的白天夜晚狀態機,又有維護四季變遷的春夏秋冬狀態機,兩種狀態機包含不同的狀態以及狀態轉換邏輯,相互獨立、互不幹涉,但也不排除在某些情況下,狀態機之間會進行交互,例如夏天的夜晚看星星、冬天的白天堆雪人等等。

下推自動機

  所謂下推自動機,是指通過在狀態機內部維護一個存儲狀態的棧來記錄狀態入棧和出棧的順序,狀態完成轉換後,新的狀態被壓入棧中,位於棧頂,前一個狀態並沒有被新的狀態直接覆蓋,而是在棧中位於新狀態的下麵。在某些場景下,如果我們需要將當前狀態恢復為之前的狀態,那麼我們就可以將棧頂的狀態彈出,此時前一個狀態又回到了棧頂的位置,我們拿到棧頂的狀態也就是前一個狀態後,將當前狀態設置為前一個狀態,便完成了狀態的恢復。

  以上便是幾種常見的狀態機拓展應用,當然,對狀態機的拓展遠不止於此,我們可以根據具體業務需求,結合面向對象封裝、繼承、多態的思想以及各種數據結構等,實現相應的拓展。

源碼

  [GitHub項目源碼]

參考資料

  1. https://zh.wikipedia.org/wiki/%E6%9C%89%E9%99%90%E7%8A%B6%E6%80%81%E6%9C%BA
  2. https://zh.wikipedia.org/wiki/%E8%87%AA%E5%8A%A8%E6%9C%BA%E7%BC%96%E7%A8%8B
  3. https://zh.wikipedia.org/wiki/%E8%87%AA%E5%8B%95%E6%A9%9F
  4. https://baike.baidu.com/item/%E6%9C%89%E9%99%90%E7%8A%B6%E6%80%81%E8%87%AA%E5%8A%A8%E6%9C%BA/2850046?fromtitle=%E6%9C%89%E9%99%90%E7%8A%B6%E6%80%81%E6%9C%BA&fromid=2081914&fr=aladdin
  5. https://zhuanlan.zhihu.com/p/74984237

尊重原創,轉載請註明出處:https://yuriyshea.com/archives/%E5%A6%82%E4%BD%95%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E7%8A%B6%E6%80%81%E6%9C%BA


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

-Advertisement-
Play Games
更多相關文章
  • 主要講解運用Webpack 5 中集成 ESLint 的方法與步驟 ...
  • const person = { name: 'Lydia' } Object.defineProperty(person, 'age', { value: 21, }) console.log(person) console.log(Object.keys(person)) 輸出結果 原因: de ...
  • 1. 前言 最近在處理前端直播的業務,根據業務需要,使用 flv.js 的方案播放實時的flv視頻流。不得不承認,flv.js 是一個偉大的庫。 在使用flv.js開發的過程中,遇到了一些問題,也無外乎是視頻延遲,視頻卡頓等問題,經過在github issues里摸爬滾打,加上長時間的試錯,將這些問 ...
  • 介紹: 有時候請求某些第三方api用nginx做反向代理解決跨域不能滿足需求,例如請求百度或者騰訊地圖的ip定位介面,該介面會根據請求來源的ip返回該ip地址對應的位置信息,但是若是用ng做了代理或者是後端做介面轉發的話實際獲取到的ip位置信息是伺服器的ip地址,想要直接解析客戶端的ip位置信息就必 ...
  • # 切換到要覆蓋的分支,這裡我們用develop分支git checkout develop# 執行覆蓋的命令,這裡是將master分支的內容覆蓋到develop分支git reset --hard origin/master# 覆蓋成功之後,將本地分支強行推到遠程分支git push -f 本人試 ...
  • 之前很多項目使用地圖時技術選型不一樣;一會兒使用騰訊地圖、一會兒使用高德地圖,一會兒使用百度地圖;每次使用都去查相關api封裝請求很麻煩,於是自己簡單封裝了一下; 插件介紹:基於h5瀏覽器gps定位、百度地圖、高德地圖、騰訊地圖api封裝的獲取位置信息相關插件,支持高精度定位;選擇使用哪個地圖傳入對 ...
  • 一、什麼是設計原則? 答:如果說設計模式是編寫代碼的一種套路,那麼設計原則就是用來約束我們使用這種套路應該要遵循的規則,只有遵循了這些規則的設計模式編寫出來的應用程式才具有更好的擴展性和維護性。作為一個程式員,不應該只站在客戶的角度去判定某一程式只要實現了了功能、擁有了漂亮的外觀、良好的用戶體驗,就 ...
  • 一、什麼是設計模式? 答:程式都是通過寫代碼來實現的,老前輩們在開發程式的過程中,為瞭解決某一類問題,日積月累總結出了一套套的代碼編寫經驗,通過這些經驗,按照套路出牌,可以讓開發出來的代碼復用性強、可靠性高、閱讀性好。復用性強意味著我們能減少代碼開發,更方便後期代碼維護;這些經驗都是老前輩們經過千錘 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...