如何實現一個狀態機?

来源: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
  • 示例項目結構 在 Visual Studio 中創建一個 WinForms 應用程式後,項目結構如下所示: MyWinFormsApp/ │ ├───Properties/ │ └───Settings.settings │ ├───bin/ │ ├───Debug/ │ └───Release/ ...
  • [STAThread] 特性用於需要與 COM 組件交互的應用程式,尤其是依賴單線程模型(如 Windows Forms 應用程式)的組件。在 STA 模式下,線程擁有自己的消息迴圈,這對於處理用戶界面和某些 COM 組件是必要的。 [STAThread] static void Main(stri ...
  • 在WinForm中使用全局異常捕獲處理 在WinForm應用程式中,全局異常捕獲是確保程式穩定性的關鍵。通過在Program類的Main方法中設置全局異常處理,可以有效地捕獲並處理未預見的異常,從而避免程式崩潰。 註冊全局異常事件 [STAThread] static void Main() { / ...
  • 前言 給大家推薦一款開源的 Winform 控制項庫,可以幫助我們開發更加美觀、漂亮的 WinForm 界面。 項目介紹 SunnyUI.NET 是一個基於 .NET Framework 4.0+、.NET 6、.NET 7 和 .NET 8 的 WinForm 開源控制項庫,同時也提供了工具類庫、擴展 ...
  • 說明 該文章是屬於OverallAuth2.0系列文章,每周更新一篇該系列文章(從0到1完成系統開發)。 該系統文章,我會儘量說的非常詳細,做到不管新手、老手都能看懂。 說明:OverallAuth2.0 是一個簡單、易懂、功能強大的許可權+可視化流程管理系統。 有興趣的朋友,請關註我吧(*^▽^*) ...
  • 一、下載安裝 1.下載git 必須先下載並安裝git,再TortoiseGit下載安裝 git安裝參考教程:https://blog.csdn.net/mukes/article/details/115693833 2.TortoiseGit下載與安裝 TortoiseGit,Git客戶端,32/6 ...
  • 前言 在項目開發過程中,理解數據結構和演算法如同掌握蓋房子的秘訣。演算法不僅能幫助我們編寫高效、優質的代碼,還能解決項目中遇到的各種難題。 給大家推薦一個支持C#的開源免費、新手友好的數據結構與演算法入門教程:Hello演算法。 項目介紹 《Hello Algo》是一本開源免費、新手友好的數據結構與演算法入門 ...
  • 1.生成單個Proto.bat內容 @rem Copyright 2016, Google Inc. @rem All rights reserved. @rem @rem Redistribution and use in source and binary forms, with or with ...
  • 一:背景 1. 講故事 前段時間有位朋友找到我,說他的窗體程式在客戶這邊出現了卡死,讓我幫忙看下怎麼回事?dump也生成了,既然有dump了那就上 windbg 分析吧。 二:WinDbg 分析 1. 為什麼會卡死 窗體程式的卡死,入口門檻很低,後續往下分析就不一定了,不管怎麼說先用 !clrsta ...
  • 前言 人工智慧時代,人臉識別技術已成為安全驗證、身份識別和用戶交互的關鍵工具。 給大家推薦一款.NET 開源提供了強大的人臉識別 API,工具不僅易於集成,還具備高效處理能力。 本文將介紹一款如何利用這些API,為我們的項目添加智能識別的亮點。 項目介紹 GitHub 上擁有 1.2k 星標的 C# ...