.Net輕量狀態機Stateless
很多業務系統開發中,不可避免的會出現狀態變化,通常採用的情形可能是使用工作流去完成,但是對於簡單場景下,用工作流有點大財小用感覺,比如訂單業務中,訂單狀態的變更,涉及到的狀態量不是很多,即使通過簡單的if-else也能足夠使用,甚至是用上switch去減少if-else的使用,都是可以的,儘管這會喪失某些東西。為更好的優化整個流程,此時會考慮到使用狀態模式來解決一些問題。
Stateless狀態機GitHub:https://github.com/dotnet-state-machine/stateless
一、狀態模式與狀態機
1、狀態模式:"允許一個物件在其內部狀態改變時改變它的行為。物件看起來似乎修改了它所屬的類 "。 (State Pattern: "Allow an object to alter its behavior when its internal state changes. The object will appear to change its class ".)
對於這個定義,有點抽象,變通理解一下可以這麼理解:狀態擁有者將變更行為委託給狀態物件,狀態擁有者本身只擁有狀態(當然也可以拋棄狀態物件),狀態物件履行變更職責。
2、狀態機:"依照指定的狀態流程圖,根據當前執行的動作,將當前狀態按照預定的條件變更到新的狀態 "。
狀態機有4個要素,即現態、條件、動作、次態。其中,現態和條件是“因”, 動作和次態是“果”。
- 現態 - 是指當前物件的狀態
- 條件 - 當一個條件滿足時,當前物件會觸發一個動作
- 動作 - 條件滿足之後,執行的動作
- 次態 - 條件滿足之後,當前物件的新狀態。次態是相對現態而言的,次態一旦觸發,就變成了現態
3、狀態遷移圖:"在UML建模中,常常可見,用來描述一個特定的物件所有可能的狀態,以及由於各種事件的發生而引起的狀態之間的轉移和變化,也是配置狀態機按照何種行徑的前提 "。
二、Stateless功能介紹
Stateless是一個基於C#建立狀態機的簡單庫。基於.Net Standard實現,在.Net Framework和.Net Core專案中都可以使用。原始碼地址:https://github.com/dotnet-state-machine/stateless。
以一個打電話的使用案例來講講Stateless的功能:
//初始化狀態機 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.Stat
1、功能特性
狀態機常見功能:
- 支援所有.Net型別的狀態和觸發器(數字、字串、列舉等等)
- 分層狀態
- 狀態的進入和退出事件
- 用衛語句來支援條件轉換
- 內省
提供了一些有用的擴充套件:
- 支援外部的狀態儲存(例如:由ORM跟蹤屬性)
- 引數化觸發器
- 可重入狀態
- 匯出DOT格式圖
2、分層狀態
在以下例子中,OnHold狀態是Connected狀態的子狀態。這意味著電話掛起的時候,還是連線狀態的,通過IsInState()方法,可以判定是否當前狀態處於父狀態下的子狀態,比如IsInState(State.Connected)能夠返回true,說明當前OnHold狀態是處於Connected狀態的。
phoneCall.Configure(State.OnHold) .SubstateOf(State.Connected) .Permit(Trigger.TakenOffHold, State.Connected) .Permit(Trigger.PhoneHurledAgainstWall, State.PhoneDestroyed);
3、狀態的進入和退出事件
在前面的例子中,StartCallTimer()方法會在通話連線時執行,StopCallTimer()方法會在通話結束時執行,對應的便是,進入該狀態與脫離該狀態時候執行的事件。當電話的狀態從已連線(Connected)變為掛起(OnHold)時, 不會觸發StartCallTimer()方法和StopCallTimer()方法, 這是因為OnHold是Connected的子狀態,對於進入和退出事件的處理者,可以傳參提供觸發動作,現狀和次狀資訊。
4、外部狀態儲存
有時候,當前物件的狀態需要來自於一個ORM物件,或者需要將當前物件的狀態儲存到一個ORM物件中,UI框架需要儲存一個狀態到繫結屬性中。為了支援這種外部狀態儲存,StateMachine類的建構函式支援了讀寫狀態值。如程式碼裡,通過使用myState可以去儲存和獲取狀態值。
var stateMachine = new StateMachine<State, Trigger>( () => myState.Value, s => myState.Value = s );
5、內省
該狀態機可以通過StateMachine.PermittedTriggers屬性獲取當前狀態下可以觸發的觸發器列表。並能夠使用StateMachine.GetInfo()獲取狀態相關的配置資訊。
public IEnumerable<TTrigger> PermittedTriggers { get { return GetPermittedTriggers(); } } //返回StateMachineInfo物件,包含狀態及觸發器列表。 _machine.GetInfo();
6、衛語句
狀態機將根據衛語句在多條轉換線路之間進行選擇,衛語句必須是互斥的,多個衛語句不能同時生效。子狀態可以通過重新指定來覆蓋狀態轉換,但是子狀態不能覆蓋父狀態允許的狀態轉換,當觸發器觸發時,衛語句開始評估線路選擇,因此不會帶來其它方面的影響。
phoneCall.Configure(State.OffHook) .PermitIf(Trigger.CallDialled, State.Ringing, () => IsValidNumber) .PermitIf(Trigger.CallDialled, State.Beeping, () => !IsValidNumber)
7、引數化觸發器
支援將強型別引數提供給觸發器,使用方法PermitDynamic()配置狀態機時,能夠通過觸發器引數動態選擇目標狀態。
var assignTrigger = stateMachine.SetTriggerParameters<string>(Trigger.Assign); stateMachine.Configure(State.Assigned) .OnEntryFrom(assignTrigger, email => OnAssigned(email)); stateMachine.Fire(assignTrigger, "[email protected]");
8、忽視轉換和重入狀態
如果觸發了一個沒有配置過的線路,將會丟擲一個異常,通過使用Ignore方法,忽視一些觸發,當觸發了此類觸發器時,不會丟擲異常,而改為忽略該次觸發。
phoneCall.Configure(State.Connected) .Ignore(Trigger.CallDialled);
另外,一個狀態能夠使用PermitReentry方法配置為重複進入(從本狀態到本狀態),entry和exit事件也會被再次觸發。
stateMachine.Configure(State.Assigned) .PermitReentry(Trigger.Assigned) .OnEntry(() => SendEmailToAssignee());
預設情形下,必須明確忽略哪些觸發器. 當未配置的觸發器被觸發時預設是丟擲異常,可以通過使用OnUnhandledTrigger配置狀態機覆寫處理異常情形。
stateMachine.OnUnhandledTrigger((state, trigger) => { });
9、匯出DOT格式圖
執行狀態視覺化狀態機是很有用處的,使用狀態機時,程式碼是命令式的,而狀態圖是副產物。
phoneCall.Configure(State.OffHook) .PermitIf(Trigger.CallDialled, State.Ringing, IsValidNumber); string graph = UmlDotGraph.Format(phoneCall.GetInfo());
UmlDotGraph.Format()方法返回代表狀態機的字串,使用DOT graph語言格式。這個可以被支援DOT graph語言的工具渲染。像graphviz.org和viz.js的dot command line工具。
諸如生成的字串在viz.js中解析的狀態機圖形。
10、非同步觸發
該狀態機支援非同步操作,對於Entry/Exit方法等都有相應的非同步方法,帶Async結尾,並且對於觸發也有非同步方法FireAsync(),需要注意的是,儘管使用了非同步,但仍然是單執行緒操作,不能被多個執行緒同時使用。
stateMachine.Configure(State.Assigned) .OnEntryAsync(async () => await SendEmailToAssignee()); await stateMachine.FireAsync(Trigger.Assigned);
至此,對於狀態機Stateless的功能差不多瞭解完畢了,開始將狀態機融入到專案中實際使用起來,也已經加入到日程中。
2019-09-22,望技術有成後能回來看見自己的腳步