歡迎光臨
每天分享高質量文章

.NET中的狀態機庫Stateless

標題:.NET中的狀態機庫Stateless

作者:Lamond Lu

地址:https://www.cnblogs.com/lwqlun/p/10674018.html[1]

介紹

什麼是狀態機和狀態樣式

狀態機是一種用來進行物件建模的工具,它是一個有向圖形,由一組節點和一組相應的轉移函式組成。狀態機透過響應一系列事件而“執行”。每個事件都在屬於“當前” 節點的轉移函式的控制範圍內,其中函式的範圍是節點的一個子集。函式傳回“下一個”(也許是同一個)節點。這些節點中至少有一個必須是終態。當到達終態, 狀態機停止。

狀態樣式主要用來解決物件狀態轉換比較複雜的情況。它把狀態的邏輯判斷轉移到不同的類中,可以把複雜的邏輯簡單化。

狀態機的要素

狀態機有4個要素,即現態、條件、動作、次態。其中,現態和條件是“因”, 動作和次態是“果”。

現態 – 是指當前物件的狀態條件 – 當一個條件滿足時,當前物件會觸發一個動作動作 – 條件滿足之後,執行的動作次態 – 條件滿足之後,當前物件的新狀態。次態是相對現態而言的,次態一旦觸發,就變成了現態

Stateless

Stateless是一款基於.NET的開源狀態機庫,最新版本4.2.1, 使用它你可以很輕鬆的在.NET中建立狀態機和以狀態機為基礎的輕量級工作流。

由於整個專案基於.NET Standard的編寫的,所以在.NET Framework和.NET Core專案中都可以使用。

專案原始碼 https://github.com/dotnet-state-machine/stateless[2]

以下是一個使用Stateless編寫的打電話流程

var phoneCall = new StateMachine(State.OffHook);
phoneCall.Configure(State.OffHook)    .Permit(Trigger.CallDialled, State.Ringing);
phoneCall.Configure(State.Ringing)    .Permit(Trigger.CallConnected, State.Connected);
phoneCall.Configure(State.Connected)    .OnEntry(() => StartCallTimer())    .OnExit(() => StopCallTimer())    .Permit(Trigger.LeftMessage, State.OffHook)    .Permit(Trigger.PlacedOnHold, State.OnHold);
// ...
phoneCall.Fire(Trigger.CallDialled);Assert.AreEqual(State.Ringing, phoneCall.State);

程式碼解釋

當前初始化了一個狀態機來描述點電話的狀態,這裡電話的初始狀態為掛機狀態(OffHook)當電話處於掛機狀態時,如果觸發被呼叫事件,電話的狀態會變為響鈴狀態(Ringing)當電話處於響鈴狀態時,如果觸發透過連線事件,電話的狀態會變為已連線狀態(Connected)當電話處於已連線狀態時,系統會開始計時,已連線狀態變為其他狀態時,系統會結束計時當電話處於已連線狀態時,如果觸發留言事件,電話的狀態會變為掛機狀態(OffHook)當電話處於已連線狀態時,如果觸發掛起事件,電話的狀態會變為掛起狀態(OnHold)Fire是觸發事件的函式,這裡觸發了一個呼叫事件觸發呼叫事件之後,電話的狀態變更為響鈴狀態,所以Assert.AreEqual(State.Ringing, phoneCall.State)的斷言是正確的。

Stateless支援的特性

對任何.NET型別的狀態和觸發器的通用支援分層狀態狀態的進入和退出事件保護子句以支援條件轉換內省

與此同時,還提供一些有用的擴充套件:

支援外部的狀態儲存(例如:由ORM跟蹤屬性)引數化觸發器可重入狀態支援DOT格式圖匯出

分層狀態

在以下例子中,OnHold狀態是Connected狀態的子狀態。這意味著電話掛起的時候,還是連線狀態的。

phoneCall.Configure(State.OnHold)    .SubstateOf(State.Connected)    .Permit(Trigger.TakenOffHold, State.Connected)    .Permit(Trigger.PhoneHurledAgainstWall, State.PhoneDestroyed);

狀態的進入和退出事件

在前面的例子中,StartCallTimer()方法會在通話連線時執行,StopCallTimer()方法會在通話結束時執行(或者電話掛起的時候,或者把電話被扔到牆上毀壞的時候^.^)。

當電話的狀態從已連線(Connected)變為掛起(OnHold)時, 不會觸發StartCallTimer()方法和StopCallTimer()方法, 這是因為OnHoldConnected的子狀態。

外部狀態儲存

有時候,當前物件的狀態需要來自於一個ORM物件,或者需要將當前物件的狀態儲存到一個ORM物件中。為了支援這種外部狀態儲存,StateMachine類的建構式支援了讀寫狀態值。

var stateMachine = new StateMachine(    () => myState.Value,    s => myState.Value = s);

內省

狀態機可以透過StateMachine.PermittedTriggers屬性,提供一個當前物件狀態下,可以觸發的觸發器串列。並提供了一個方法StateMachine.GetInfo()來獲取有關狀態的配置資訊。

保護子句

狀態機將根據保護子句在多個轉換之間進行選擇。

phoneCall.Configure(State.OffHook)    .PermitIf(Trigger.CallDialled, State.Ringing, () => IsValidNumber)    .PermitIf(Trigger.CallDialled, State.Beeping, () => !IsValidNumber);

註意:

配置中的保護子句必須是互斥的,子狀態可以透過重新指定來改寫狀態轉換,但是子狀態不能改寫父狀態允許的狀態轉換。

引數化觸發器

Stateless中支援將強型別引數指定給觸發器。

var assignTrigger = stateMachine.SetTriggerParameters(Trigger.Assign);
stateMachine.Configure(State.Assigned)    .OnEntryFrom(assignTrigger, email => OnAssigned(email));
stateMachine.Fire(assignTrigger, "joe@example.com");

匯出DOT圖

Stateless還提供了一個在執行時生成DOT圖程式碼的功能,使用生成的DOT圖程式碼,我們可以生成視覺化的狀態機圖。

這裡我們可以使用UmlDotGraph.Format()方法來生成DOT圖程式碼。

phoneCall.Configure(State.OffHook)    .PermitIf(Trigger.CallDialled, State.Ringing, IsValidNumber);
string graph = UmlDotGraph.Format(phoneCall.GetInfo());

生成的DOT圖程式碼例子

digraph {    compound=true;    node [shape=Mrecord]    rankdir="LR"
    subgraph clusterOpen    {        label = "Open"        Assigned [label="Assigned|exit / Function"];    }    Deferred [label="Deferred|entry / Function"];    Closed [label="Closed"];
    Open -> Assigned [style="solid", label="Assign / Function"];    Assigned -> Assigned [style="solid", label="Assign"];    Assigned -> Closed [style="solid", label="Close"];    Assigned -> Deferred [style="solid", label="Defer"];    Deferred -> Assigned [style="solid", label="Assign / Function"];}

圖形化之後的DOT圖例子

一個BugTracker的例子

看完了這麼多介紹,下麵我們來操練一下, 編寫一個Bug的狀態機。

假設在當前的BugTracker系統中,Bug有4個種狀態Open, Assigned, Deferred, Closed。由此我們可以建立一個列舉類State

    public enum State    {        Open,        Assigned,        Deferred,        Closed    }

如果想改變Bug的狀態,這裡有3種動作,Assign, Defer, Close。

    public enum Trigger    {        Assign,        Defer,        Close    }

下麵我們列舉一下Bug物件可能的狀態變化。

每個Bug的初始狀態是Open如果當前Bug的狀態是Open, 觸發動作Assign, Bug的狀態會變為Assigned如果當前Bug的狀態是Assigned, 觸發動作Defer, Bug的狀態會變為Deferred如果當前Bug的狀態是Assigned, 觸發動作Close, Bug的狀態會變為Closed如果當前Bug的狀態是Assigned, 觸發動作Assign, Bug的狀態會保持Assigned(變更Bug修改者的場景)如果當前Bug的狀態是Deferred, 觸發動作Assign, Bug的狀態會變為Assigned

由此我們可以編寫Bug類

    public class Bug    {        State _state = State.Open;        StateMachine _machine;        StateMachine.TriggerWithParameters _assignTrigger;
        string _title;        string _assignee;
        public Bug(string title)        {            _title = title;
            _machine = new StateMachine(() => _state, s => _state = s);
            _assignTrigger = _machine.SetTriggerParameters(Trigger.Assign);
            _machine.Configure(State.Open).Permit(Trigger.Assign, State.Assigned);            _machine.Configure(State.Assigned)                .OnEntryFrom(_assignTrigger, assignee => _assignee = assignee)                .SubstateOf(State.Open)                .PermitReentry(Trigger.Assign)                .Permit(Trigger.Close, State.Closed)                .Permit(Trigger.Defer, State.Deferred);
            _machine.Configure(State.Deferred)                .OnEntry(() => _assignee = null)                .Permit(Trigger.Assign, State.Assigned);        }
        public string CurrentState        {            get            {                return _machine.State.ToString();            }        }
        public string Title        {            get            {                return _title;            }        }
        public string Assignee        {            get            {                if (string.IsNullOrWhiteSpace(_assignee))                {                    return "Not Assigned";                }
                return _assignee;            }        }
        public void Assign(string assignee)        {            _machine.Fire(_assignTrigger, assignee);        }
        public void Defer()        {            _machine.Fire(Trigger.Defer);        }
        public void Close()        {            _machine.Fire(Trigger.Close);        }    }

程式碼解釋:

每個Bug都應該有個指派人和標題,所以這裡我添加了一個Assignee和Title屬性當指派Bug時,需要指定一個指派人,所以Assign動作的觸發器我使用的是一個引數化的觸發器當Bug物件進入Assigned狀態時,我將當前指定的指派人賦值給了_assignee欄位。

最終效果

這裡我們先展示一個正常的操作流程。

    class Program    {        static void Main(string[] args)        {            Bug bug = new Bug("Hello World!");
            Console.WriteLine($"Current State: {bug.CurrentState}");
            bug.Assign("Lamond Lu");
            Console.WriteLine($"Current State: {bug.CurrentState}");            Console.WriteLine($"Current Assignee: {bug.Assignee}");
            bug.Defer();
            Console.WriteLine($"Current State: {bug.CurrentState}");            Console.WriteLine($"Current Assignee: {bug.Assignee}");
            bug.Assign("Lu Nan");
            Console.WriteLine($"Current State: {bug.CurrentState}");            Console.WriteLine($"Current Assignee: {bug.Assignee}");
            bug.Close();
            Console.WriteLine($"Current State: {bug.CurrentState}");        }    }

執行結果

下麵我們修改程式碼,我們在建立一個Bug之後,立即嘗試關閉它

    class Program    {        static void Main(string[] args)        {            Bug bug = new Bug("Hello World!");            bug.Close();        }    }

重新執行程式之後,程式會丟擲以下異常。

Unhandled Exception: System.InvalidOperationException: No valid leaving transitions are permitted from state 'Open' for trigger 'Close'. Consider ignoring the trigger.

當Bug處於Open狀態的時候,觸發Close動作,由於沒有任何次態定義,所以丟擲了異常,這與我們前面定義的邏輯相符,如果希望程式支援Open -> Closed的狀態變化,我們需要修改Open狀態的配置,允許Open狀態透過Close動作變為Closed狀態。

_machine.Configure(State.Open)    .Permit(Trigger.Assign, State.Assigned)    .Permit(Trigger.Close, State.Closed);

由此可見我們完全可以根據自身專案的需求,定義一個簡單的工作流,Stateless會自動幫我們驗證出錯誤的流程操作。

總結

今天我為大家分享了一下.NET中的狀態機庫Stateless, 使用它我們可以很容易的定義出自己業務需要的狀態機,或者基於狀態機的工作流,本文大部分的內容都來自官方Github,有興趣的同學可以深入研究一下。

References

[1]https://www.cnblogs.com/lwqlun/p/10674018.html
[2]https://github.com/dotnet-state-machine/stateless

 

    已同步到看一看
    贊(0)

    分享創造快樂