1. 程式人生 > >C#狀態機Stateless

C#狀態機Stateless

最近在折騰一些控制相關的軟體設計,想起來狀態機這個東西,對解決一些控制系統狀態切換還是挺有用的。 狀態機(有限狀態自動機)網上有很多[介紹](https://zhuanlan.zhihu.com/p/47434856)。簡單理解就是定義一系列狀態,通過一系列的事件,可以使得狀態可以相互之間切換。 如果不使用狀態機的思想來程式設計,那麼針對過程的程式設計方法會使得程式拓展性變差,並且不容易除錯。而狀態機只需要定義好了各種狀態和狀態切換之間的事件,你只管觸發事件,剩下的事情它自己就自動完成了(畢竟名稱叫做有限狀態自動機),這對於很多需要定義各種控制階段的系統簡直是完美適配。瞭解到.NET也有很多庫可以實現這些功能,本文主要介紹一下Stateless的應用。 # Stateless介紹 [Stateless](https://github.com/dotnet-state-machine/stateless)可以創建極簡的狀態機與對應的工作流。很多專案(包括VisualStudio Extension、AIlab)都有使用到它。 它支援以下特性: * 支援各種型別作為狀態和觸發事件 * 支援狀態繼承 * 支援狀態進入/離開事件 * 支援條件狀態轉移 * 支援狀態/轉移查詢 也有幾點需要注意的: * 它支援非同步語法,但是它是單執行緒的,不是執行緒安全的。 * 可以匯出DOT graph 安裝起來很簡單,直接在nuget中安裝即可: ```powershell Install-Package Stateless ``` # Stateless使用 用起來也挺簡單的,以打電話這個事情為例,針對打電話的種種動作和狀態做成一個狀態機。 需要先定義一些狀態和事件/觸發器,電話有撥號、接通、留言等事件,有響鈴、掛起、結束通話等事件: ```c# //程式碼來自官方示例,可以在官方github庫上找到,略有修改以完整展示功能。 enum Trigger { CallDialed, CallConnected, LeftMessage, PlacedOnHold, TakenOffHold, PhoneHurledAgainstWall, MuteMicrophone, UnmuteMicrophone, SetVolume } enum State { OffHook, Ringing, Connected, OnHold, PhoneDestroyed } ``` 然後就是建立一個狀態機了: ```c# _machine = new StateMachine(() => _state, s => _state = s); ``` 最後也是最需要詳細解釋的,就是配置狀態機的行為了: ```c# /* 為了解釋儘可能多的功能,以下程式修改了官方的程式碼,可以在官方找可以直接執行的程式碼。 */ //使用Permit指示發生某個事件後,從一個狀態變換到另外一個狀態。 _machine.Configure(State.OffHook) .Permit(Trigger.CallDialed, State.Ringing); //設定一個帶引數的事件,這個事件是CallDialed的型別 var _setCalleeTrigger = _machine.SetTriggerParameters(Trigger.CallDialed); _machine.Configure(State.Ringing) //允許重新進入當前的狀態,這個過程會觸發進入和退出動作 .PermitReentry(Trigger.Ringing) //使用OnEntryFrom指示在觸發這個狀態的時候,執行某個動作,這裡指定的是一個帶引數的事件 .OnEntryFrom(_setCalleeTrigger, callee => OnDialed(callee), "Caller number to call") .Permit(Trigger.CallConnected, State.Connected); _machine.Configure(State.OnHold) //定義子狀態 .SubstateOf(State.Connected) .Permit(Trigger.TakenOffHold, State.Connected) .Permit(Trigger.PhoneHurledAgainstWall, State.PhoneDestroyed); _machine.Configure(State.Connected) //進入狀態的時候執行動作 .OnEntry(t => StartCallTimer()) //離開狀態執行動作 .OnExit(t => StopCallTimer()) //狀態不變化,但是響應某種事件,和PermitReentry不同,它不會觸發進入和退出的動作 .InternalTransition(Trigger.MuteMicrophone, t => OnMute()) .InternalTransition(Trigger.UnmuteMicrophone, t => OnUnmute()) .InternalTransition(_setVolumeTrigger, (volume, t) => OnSetVolume(volume)) .Permit(Trigger.LeftMessage, State.OffHook) .Permit(Trigger.PlacedOnHold, State.OnHold) //指定在發生同一種事件的時候,根據事件的引數不同而決定進入不同的狀態。 .PermitIf(_setCalleeTrigger, State.Connected, callee => string.IsNullOrWhiteSpace(callee)) .PermitIf(_setCalleeTrigger, State.Connected, callee => !string.IsNullOrWhiteSpace(callee)) //如果沒有定義這個事件而發生了這個事件,會彈出異常。通過指定忽略某一類事件,可以避免這個情況。 .Ignore(Trigger.CallDialled); //當然也可以使用這個來避免彈出上面說的異常 _machine.OnUnhandledTrigger((state, trigger) => { }); //可以使用非同步呼叫,但是必須要在觸發事件的時候,使用FireAsync _machine.Configure(State.PhoneDestroyed) .OnEntryAsync(async () => await SendEmailToAssignee()); ``` 配置好了各狀態之間的轉換,下面就是觸發事件了。 ```c# public void Dialed(string callee) { //有引數的觸發 _machine.Fire(_setCalleeTrigger, callee); } public void Connected() { //無引數的觸發 _machine.Fire(Trigger.CallConnected); } public async Task PhoneDestroy() { //非同步觸發 await _machine.FireAsync(Trigger.PhoneDestroyed); } public string ToDotGraph() { //匯出DOT GRAPH return UmlDotGraph.Format(_machine.GetInfo()); } ``` 外部呼叫很簡潔: ```c# phoneCall.Dialed("Prameela"); phoneCall.Connected(); phoneCall.SetVolume(2); phoneCall.Hold(); ``` 只需要呼叫事件即可,別的都會按照我們設定好的動作來進行了,非常自動化。 # 總結 Stateless可以很好地實現狀態機,有點事件驅動的程式設計的感覺,但本質上不同,Stateless核心是各個狀態的遷移。 雖然Stateless很小巧方便,但是還有有很多地方不盡如人意(官方說這就是他們自己的設計目標,維持極簡): * 沒有啟動和停止的說法,一般在建構函式裡面建立就一直有效。 * 不是執行緒安全的 * 拓展性有限 還有一個`Appccelerate.StateMachine`([地址](https://github.com/appccelerate/statemachine)),這個支援四種不同的狀態機實現: - Passive State Machine: 同步單執行緒處理狀態轉換 - Active State Machine: 同步多執行緒處理狀態轉換 - Async Passive State Machine: 非同步單執行緒處理狀態轉換 - Async Active State Machine: 非同步多執行緒處理狀態轉換 其中active的是執行緒安安全的。另外,它還支援狀態、事件的持久化,拓展性強。用法差不多,只是配置的關鍵字上面有一點區別,大家可以自行翻