很多業務系統開發中,不可避免的會出現狀态變化,通常采用的情形可能是使用工作流去完成,但是對于簡單場景下,用工作流有點大财小用感覺,比如訂單業務中,訂單狀态的變更,涉及到的狀态量不是很多,即使通過簡單的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,望技術有成後能回來看見自己的腳步