.NET中的狀態(tài)機(jī)庫(kù)Stateless

標(biāo)題:.NET中的狀態(tài)機(jī)庫(kù)Stateless
作者:Lamond Lu
地址:https://www.cnblogs.com/lwqlun/p/10674018.html

介紹

什么是狀態(tài)機(jī)和狀態(tài)模式

image

狀態(tài)機(jī)是一種用來(lái)進(jìn)行對(duì)象建模的工具翘鸭,它是一個(gè)有向圖形,由一組節(jié)點(diǎn)和一組相應(yīng)的轉(zhuǎn)移函數(shù)組成辕宏。狀態(tài)機(jī)通過(guò)響應(yīng)一系列事件而“運(yùn)行”凡人。每個(gè)事件都在屬于“當(dāng)前” 節(jié)點(diǎn)的轉(zhuǎn)移函數(shù)的控制范圍內(nèi)统诺,其中函數(shù)的范圍是節(jié)點(diǎn)的一個(gè)子集。函數(shù)返回“下一個(gè)”(也許是同一個(gè))節(jié)點(diǎn)。這些節(jié)點(diǎn)中至少有一個(gè)必須是終態(tài)脉执。當(dāng)?shù)竭_(dá)終態(tài)踢代, 狀態(tài)機(jī)停止盲憎。

狀態(tài)模式主要用來(lái)解決對(duì)象狀態(tài)轉(zhuǎn)換比較復(fù)雜的情況。它把狀態(tài)的邏輯判斷轉(zhuǎn)移到不同的類中胳挎,可以把復(fù)雜的邏輯簡(jiǎn)單化饼疙。

狀態(tài)機(jī)的要素

狀態(tài)機(jī)有4個(gè)要素,即現(xiàn)態(tài)慕爬、條件窑眯、動(dòng)作、次態(tài)澡罚。其中伸但,現(xiàn)態(tài)和條件是“因”, 動(dòng)作和次態(tài)是“果”留搔。

  • 現(xiàn)態(tài) - 是指當(dāng)前對(duì)象的狀態(tài)
  • 條件 - 當(dāng)一個(gè)條件滿足時(shí)更胖,當(dāng)前對(duì)象會(huì)觸發(fā)一個(gè)動(dòng)作
  • 動(dòng)作 - 條件滿足之后,執(zhí)行的動(dòng)作
  • 次態(tài) - 條件滿足之后隔显,當(dāng)前對(duì)象的新?tīng)顟B(tài)却妨。次態(tài)是相對(duì)現(xiàn)態(tài)而言的,次態(tài)一旦觸發(fā)括眠,就變成了現(xiàn)態(tài)

Stateless

Stateless是一款基于.NET的開(kāi)源狀態(tài)機(jī)庫(kù)彪标,最新版本4.2.1, 使用它你可以很輕松的在.NET中創(chuàng)建狀態(tài)機(jī)和以狀態(tài)機(jī)為基礎(chǔ)的輕量級(jí)工作流。

由于整個(gè)項(xiàng)目基于.NET Standard的編寫(xiě)的掷豺,所以在.NET Framework和.NET Core項(xiàng)目中都可以使用捞烟。

項(xiàng)目源代碼 https://github.com/dotnet-state-machine/stateless

以下是一個(gè)使用Stateless編寫(xiě)的打電話流程

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

代碼解釋

  • 當(dāng)前初始化了一個(gè)狀態(tài)機(jī)來(lái)描述點(diǎn)電話的狀態(tài),這里電話的初始狀態(tài)為掛機(jī)狀態(tài)(OffHook)
  • 當(dāng)電話處于掛機(jī)狀態(tài)時(shí)当船,如果觸發(fā)被呼叫事件题画,電話的狀態(tài)會(huì)變?yōu)轫戔彔顟B(tài)(Ringing)
  • 當(dāng)電話處于響鈴狀態(tài)時(shí),如果觸發(fā)通過(guò)連接事件德频,電話的狀態(tài)會(huì)變?yōu)橐堰B接狀態(tài)(Connected)
  • 當(dāng)電話處于已連接狀態(tài)時(shí)苍息,系統(tǒng)會(huì)開(kāi)始計(jì)時(shí),已連接狀態(tài)變?yōu)槠渌麪顟B(tài)時(shí),系統(tǒng)會(huì)結(jié)束計(jì)時(shí)
  • 當(dāng)電話處于已連接狀態(tài)時(shí)竞思,如果觸發(fā)留言事件表谊,電話的狀態(tài)會(huì)變?yōu)閽鞕C(jī)狀態(tài)(OffHook)
  • 當(dāng)電話處于已連接狀態(tài)時(shí),如果觸發(fā)掛起事件盖喷,電話的狀態(tài)會(huì)變?yōu)閽炱馉顟B(tài)(OnHold)
  • Fire是觸發(fā)事件的函數(shù)爆办,這里觸發(fā)了一個(gè)呼叫事件
  • 觸發(fā)呼叫事件之后,電話的狀態(tài)變更為響鈴狀態(tài)传蹈,所以Assert.AreEqual(State.Ringing, phoneCall.State)的斷言是正確的押逼。

Stateless支持的特性

  • 對(duì)任何.NET類型的狀態(tài)和觸發(fā)器的通用支持
  • 分層狀態(tài)
  • 狀態(tài)的進(jìn)入和退出事件
  • 保護(hù)子句以支持條件轉(zhuǎn)換
  • 內(nèi)省

與此同時(shí),還提供一些有用的擴(kuò)展:

  • 支持外部的狀態(tài)存儲(chǔ)(例如:由ORM跟蹤屬性)
  • 參數(shù)化觸發(fā)器
  • 可重入狀態(tài)
  • 支持DOT格式圖導(dǎo)出

分層狀態(tài)

在以下例子中惦界,OnHold狀態(tài)是Connected狀態(tài)的子狀態(tài)挑格。這意味著電話掛起的時(shí)候,還是連接狀態(tài)的沾歪。

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

狀態(tài)的進(jìn)入和退出事件

在前面的例子中漂彤,StartCallTimer()方法會(huì)在通話連接時(shí)執(zhí)行,StopCallTimer()方法會(huì)在通話結(jié)束時(shí)執(zhí)行(或者電話掛起的時(shí)候灾搏,或者把電話被扔到墻上毀壞的時(shí)候.)挫望。

當(dāng)電話的狀態(tài)從已連接(Connected)變?yōu)閽炱?OnHold)時(shí), 不會(huì)觸發(fā)StartCallTimer()方法和StopCallTimer()方法, 這是因?yàn)?code>OnHold是Connected的子狀態(tài)狂窑。

外部狀態(tài)存儲(chǔ)

有時(shí)候媳板,當(dāng)前對(duì)象的狀態(tài)需要來(lái)自于一個(gè)ORM對(duì)象,或者需要將當(dāng)前對(duì)象的狀態(tài)保存到一個(gè)ORM對(duì)象中泉哈。為了支持這種外部狀態(tài)存儲(chǔ)蛉幸,StateMachine類的構(gòu)造函數(shù)支持了讀寫(xiě)狀態(tài)值。

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

內(nèi)省

狀態(tài)機(jī)可以通過(guò)StateMachine.PermittedTriggers屬性丛晦,提供一個(gè)當(dāng)前對(duì)象狀態(tài)下奕纫,可以觸發(fā)的觸發(fā)器列表。并提供了一個(gè)方法StateMachine.GetInfo()來(lái)獲取有關(guān)狀態(tài)的配置信息烫沙。

保護(hù)子句

狀態(tài)機(jī)將根據(jù)保護(hù)子句在多個(gè)轉(zhuǎn)換之間進(jìn)行選擇匹层。

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

注意:

配置中的保護(hù)子句必須是互斥的,子狀態(tài)可以通過(guò)重新指定來(lái)覆蓋狀態(tài)轉(zhuǎn)換锌蓄,但是子狀態(tài)不能覆蓋父狀態(tài)允許的狀態(tài)轉(zhuǎn)換升筏。

參數(shù)化觸發(fā)器

Stateless中支持將強(qiáng)類型參數(shù)指定給觸發(fā)器。

var assignTrigger = stateMachine.SetTriggerParameters<string>(Trigger.Assign);

stateMachine.Configure(State.Assigned)
    .OnEntryFrom(assignTrigger, email => OnAssigned(email));

stateMachine.Fire(assignTrigger, "joe@example.com");

導(dǎo)出DOT圖

Stateless還提供了一個(gè)在運(yùn)行時(shí)生成DOT圖代碼的功能瘸爽,使用生成的DOT圖代碼仰冠,我們可以生成可視化的狀態(tài)機(jī)圖。

這里我們可以使用UmlDotGraph.Format()方法來(lái)生成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圖例子

image

一個(gè)BugTracker的例子

看完了這么多介紹,下面我們來(lái)操練一下辆沦, 編寫(xiě)一個(gè)Bug的狀態(tài)機(jī)。

假設(shè)在當(dāng)前的BugTracker系統(tǒng)中,Bug有4個(gè)種狀態(tài)Open, Assigned, Deferred, Closed宜雀。由此我們可以創(chuàng)建一個(gè)枚舉類State闸度。

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

如果想改變Bug的狀態(tài),這里有3種動(dòng)作不翩,Assign, Defer, Close。

    public enum Trigger
    {
        Assign,
        Defer,
        Close
    }

下面我們列舉一下Bug對(duì)象可能的狀態(tài)變化。

  • 每個(gè)Bug的初始狀態(tài)是Open
  • 如果當(dāng)前Bug的狀態(tài)是Open, 觸發(fā)動(dòng)作Assign, Bug的狀態(tài)會(huì)變?yōu)锳ssigned
  • 如果當(dāng)前Bug的狀態(tài)是Assigned, 觸發(fā)動(dòng)作Defer, Bug的狀態(tài)會(huì)變?yōu)镈eferred
  • 如果當(dāng)前Bug的狀態(tài)是Assigned, 觸發(fā)動(dòng)作Close, Bug的狀態(tài)會(huì)變?yōu)镃losed
  • 如果當(dāng)前Bug的狀態(tài)是Assigned, 觸發(fā)動(dòng)作Assign, Bug的狀態(tài)會(huì)保持Assigned(變更Bug修改者的場(chǎng)景)
  • 如果當(dāng)前Bug的狀態(tài)是Deferred, 觸發(fā)動(dòng)作Assign, Bug的狀態(tài)會(huì)變?yōu)锳ssigned

由此我們可以編寫(xiě)B(tài)ug類

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

代碼解釋:

  • 每個(gè)Bug都應(yīng)該有個(gè)指派人和標(biāo)題肛循,所以這里我添加了一個(gè)Assignee和Title屬性
  • 當(dāng)指派Bug時(shí),需要指定一個(gè)指派人银择,所以Assign動(dòng)作的觸發(fā)器我使用的是一個(gè)參數(shù)化的觸發(fā)器
  • 當(dāng)Bug對(duì)象進(jìn)入Assigned狀態(tài)時(shí)多糠,我將當(dāng)前指定的指派人賦值給了_assignee字段。

最終效果

這里我們先展示一個(gè)正常的操作流程浩考。

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

運(yùn)行結(jié)果

image

下面我們修改代碼夹孔,我們?cè)趧?chuàng)建一個(gè)Bug之后,立即嘗試關(guān)閉它

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

重新運(yùn)行程序之后析孽,程序會(huì)拋出以下異常搭伤。

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

當(dāng)Bug處于Open狀態(tài)的時(shí)候,觸發(fā)Close動(dòng)作袜瞬,由于沒(méi)有任何次態(tài)定義怜俐,所以拋出了異常,這與我們前面定義的邏輯相符邓尤,如果希望程序支持Open -> Closed的狀態(tài)變化拍鲤,我們需要修改Open狀態(tài)的配置,允許Open狀態(tài)通過(guò)Close動(dòng)作變?yōu)镃losed狀態(tài)裁赠。

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

由此可見(jiàn)我們完全可以根據(jù)自身項(xiàng)目的需求殿漠,定義一個(gè)簡(jiǎn)單的工作流,Stateless會(huì)自動(dòng)幫我們驗(yàn)證出錯(cuò)誤的流程操作佩捞。

總結(jié)

今天我為大家分享了一下.NET中的狀態(tài)機(jī)庫(kù)Stateless, 使用它我們可以很容易的定義出自己業(yè)務(wù)需要的狀態(tài)機(jī)绞幌,或者基于狀態(tài)機(jī)的工作流,本文大部分的內(nèi)容都來(lái)自官方Github一忱,有興趣的同學(xué)可以深入研究一下莲蜘。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市帘营,隨后出現(xiàn)的幾起案子票渠,更是在濱河造成了極大的恐慌,老刑警劉巖芬迄,帶你破解...
    沈念sama閱讀 218,451評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件问顷,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)杜窄,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,172評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門肠骆,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人塞耕,你說(shuō)我怎么就攤上這事蚀腿。” “怎么了扫外?”我有些...
    開(kāi)封第一講書(shū)人閱讀 164,782評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵莉钙,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我筛谚,道長(zhǎng)磁玉,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,709評(píng)論 1 294
  • 正文 為了忘掉前任刻获,我火速辦了婚禮蜀涨,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘蝎毡。我一直安慰自己厚柳,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,733評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布沐兵。 她就那樣靜靜地躺著别垮,像睡著了一般。 火紅的嫁衣襯著肌膚如雪扎谎。 梳的紋絲不亂的頭發(fā)上碳想,一...
    開(kāi)封第一講書(shū)人閱讀 51,578評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音毁靶,去河邊找鬼胧奔。 笑死,一個(gè)胖子當(dāng)著我的面吹牛预吆,可吹牛的內(nèi)容都是我干的龙填。 我是一名探鬼主播,決...
    沈念sama閱讀 40,320評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼拐叉,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼岩遗!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起凤瘦,我...
    開(kāi)封第一講書(shū)人閱讀 39,241評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤宿礁,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后蔬芥,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體梆靖,經(jīng)...
    沈念sama閱讀 45,686評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡控汉,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,878評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了返吻。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片暇番。...
    茶點(diǎn)故事閱讀 39,992評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖思喊,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情次酌,我是刑警寧澤恨课,帶...
    沈念sama閱讀 35,715評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站岳服,受9級(jí)特大地震影響剂公,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜吊宋,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,336評(píng)論 3 330
  • 文/蒙蒙 一纲辽、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧璃搜,春花似錦拖吼、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,912評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至唾糯,卻和暖如春怠硼,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背移怯。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,040評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工香璃, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人舟误。 一個(gè)月前我還...
    沈念sama閱讀 48,173評(píng)論 3 370
  • 正文 我出身青樓葡秒,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親脐帝。 傳聞我的和親對(duì)象是個(gè)殘疾皇子同云,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,947評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容

  • 狀態(tài)機(jī)是無(wú)論科研探索還是科技應(yīng)用方面都非常重要的一種分析工具。幾乎在所有涉及到隨時(shí)間演化的問(wèn)題中堵腹,都可以找到狀態(tài)機(jī)...
    Esmool閱讀 4,421評(píng)論 0 26
  • 國(guó)家電網(wǎng)公司企業(yè)標(biāo)準(zhǔn)(Q/GDW)- 面向?qū)ο蟮挠秒娦畔?shù)據(jù)交換協(xié)議 - 報(bào)批稿:20170802 前言: 排版 ...
    庭說(shuō)閱讀 10,974評(píng)論 6 13
  • 常常會(huì)想起小時(shí)候的事炸站,也時(shí)常會(huì)夢(mèng)中驚醒,很多時(shí)候疚顷,對(duì)一些人來(lái)說(shuō)旱易,童年并不值得回憶禁偎,我想我也是。
    尹馨兒閱讀 438評(píng)論 0 0
  • 抑郁 既現(xiàn)實(shí)又虛幻 掩藏自己的情感假裝堅(jiān)強(qiáng) 能從邊緣拉回來(lái)的或許真的很少吧........ 清晨一陣鈴聲想起阀坏,...
    可以吃兔兔嗎閱讀 432評(píng)論 0 3
  • 水晶月光的新浪博客里有很多簡(jiǎn)單易行的食譜如暖,經(jīng)常在看,就是沒(méi)有去做忌堂。趁現(xiàn)在放假盒至,來(lái)實(shí)踐實(shí)踐。 我從水晶月光博客里挑了...
    巧23閱讀 798評(píng)論 0 0