.NET中的狀態機庫Stateless

標題:.NET中的狀態機庫Stateless
做者:Lamond Lu
地址:http://www.javashuo.com/article/p-kdfjlabc-km.htmlhtml

介紹

什麼是狀態機和狀態模式

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

狀態模式主要用來解決對象狀態轉換比較複雜的狀況。它把狀態的邏輯判斷轉移到不一樣的類中,能夠把複雜的邏輯簡單化。git

狀態機的要素

狀態機有4個要素,即現態、條件、動做、次態。其中,現態和條件是「因」, 動做和次態是「果」。github

  • 現態 - 是指當前對象的狀態
  • 條件 - 當一個條件知足時,當前對象會觸發一個動做
  • 動做 - 條件知足以後,執行的動做
  • 次態 - 條件知足以後,當前對象的新狀態。次態是相對現態而言的,次態一旦觸發,就變成了現態

Stateless

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

因爲整個項目基於.NET Standard的編寫的,因此在.NET Framework和.NET Core項目中均可以使用。數組

項目源代碼 https://github.com/dotnet-state-machine/statelessless

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

var phoneCall = new StateMachine<State, Trigger>(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<State, Trigger>(
    () => 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<string>(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<State, Trigger> _machine;
        StateMachine<State, Trigger>.TriggerWithParameters<string> _assignTrigger;

        string _title;
        string _assignee;

        public Bug(string title)
        {
            _title = title;

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

            _assignTrigger = _machine.SetTriggerParameters<string>(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,有興趣的同窗能夠深刻研究一下。

相關文章
相關標籤/搜索