如何實現一個狀態機?

2022-07-20 15:01:07

何為狀態機?

  從字面上簡單粗暴地理解,狀態機是一個跟狀態有關的機器,但其實狀態機並不是一種物理機器,而是一種模型,一種表達事物狀態及狀態變化過程的數學模型。
  狀態機全稱是有限狀態機(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