C#狀態機Stateless


最近在折騰一些控制相關的軟件設計,想起來狀態機這個東西,對解決一些控制系統狀態切換還是挺有用的。

狀態機(有限狀態自動機)網上有很多介紹。簡單理解就是定義一系列狀態,通過一系列的事件,可以使得狀態可以相互之間切換。

如果不使用狀態機的思想來編程,那么針對過程的編程方法會使得程序拓展性變差,並且不容易調試。而狀態機只需要定義好了各種狀態和狀態切換之間的事件,你只管觸發事件,剩下的事情它自己就自動完成了(畢竟名稱叫做有限狀態自動機),這對於很多需要定義各種控制階段的系統簡直是完美適配。了解到.NET也有很多庫可以實現這些功能,本文主要介紹一下Stateless的應用。

Stateless介紹

Stateless可以創建極簡的狀態機與對應的工作流。很多項目(包括VisualStudio Extension、AIlab)都有使用到它。

它支持以下特性:

  • 支持各種類型作為狀態和觸發事件
  • 支持狀態繼承
  • 支持狀態進入/離開事件
  • 支持條件狀態轉移
  • 支持狀態/轉移查詢

也有幾點需要注意的:

  • 它支持異步語法,但是它是單線程的,不是線程安全的。
  • 可以導出DOT graph

安裝起來很簡單,直接在nuget中安裝即可:

Install-Package Stateless

Stateless使用

用起來也挺簡單的,以打電話這個事情為例,針對打電話的種種動作和狀態做成一個狀態機。
需要先定義一些狀態和事件/觸發器,電話有撥號、接通、留言等事件,有響鈴、掛起、掛斷等事件:

//代碼來自官方示例,可以在官方github庫上找到,略有修改以完整展示功能。
enum Trigger
{
    CallDialed,
    CallConnected,
    LeftMessage,
    PlacedOnHold,
    TakenOffHold,
    PhoneHurledAgainstWall,
    MuteMicrophone,
    UnmuteMicrophone,
    SetVolume
}

enum State
{
    OffHook,
    Ringing,
    Connected,
    OnHold,
    PhoneDestroyed
}

然后就是創建一個狀態機了:

_machine = new StateMachine<State, Trigger>(() => _state, s => _state = s);

最后也是最需要詳細解釋的,就是配置狀態機的行為了:

/*
為了解釋盡可能多的功能,以下程序修改了官方的代碼,可以在官方找可以直接執行的代碼。
*/

//使用Permit指示發生某個事件后,從一個狀態變換到另外一個狀態。
_machine.Configure(State.OffHook)
    .Permit(Trigger.CallDialed, State.Ringing);

//設置一個帶參數的事件,這個事件是CallDialed的類型
var _setCalleeTrigger = _machine.SetTriggerParameters<string>(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<int>(_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());

配置好了各狀態之間的轉換,下面就是觸發事件了。

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());
}

外部調用很簡潔:

phoneCall.Dialed("Prameela");
phoneCall.Connected();
phoneCall.SetVolume(2);
phoneCall.Hold();

只需要調用事件即可,別的都會按照我們設置好的動作來進行了,非常自動化。

總結

Stateless可以很好地實現狀態機,有點事件驅動的編程的感覺,但本質上不同,Stateless核心是各個狀態的遷移。

雖然Stateless很小巧方便,但是還有有很多地方不盡如人意(官方說這就是他們自己的設計目標,維持極簡):

  • 沒有啟動和停止的說法,一般在構造函數里面創建就一直有效。
  • 不是線程安全的
  • 拓展性有限

還有一個Appccelerate.StateMachine地址),這個支持四種不同的狀態機實現:

  • Passive State Machine: 同步單線程處理狀態轉換
  • Active State Machine: 同步多線程處理狀態轉換
  • Async Passive State Machine: 異步單線程處理狀態轉換
  • Async Active State Machine: 異步多線程處理狀態轉換

其中active的是線程安安全的。另外,它還支持狀態、事件的持久化,拓展性強。用法差不多,只是配置的關鍵字上面有一點區別,大家可以自行翻文檔。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM