狀態機
引言
在業務處理中, 經常需要處理業務對象的狀態轉換, 比如 bug 狀態管理、訂單狀態管理等, 這類問題可依照狀態的復雜度,可以有不同的解決方案。
-
簡單的順序狀態管理
如果狀態數量很少,同時狀態是按照一個方向流轉,就可以歸到這類。 對應的解決方案很簡單, 在業務對象中增加一個表示狀態的枚舉字段即可。 -
復雜的 BPM 狀態管理
這里復雜的狀態管理, 典型特征是包含 BPM 的 split/join, 比如合同的會簽, 對應的解決方案就使用重量級的工作流引擎, 比如 Java 的 flowable、camunda、 activiti, C#的 Elsa Workflows 等。 -
中等復雜的狀態管理
我將不含 BPM 的 split/join 情形的都歸到中等復雜程度,最佳解決方案是使用狀態機,在每個業務對象中內嵌一個狀態機,由狀態機復雜狀態轉換。
對于簡單的狀態管理,往往后期會逐漸會變得不那么簡單,所以, 對于簡單的狀態管理,我也推薦使用狀態機,甚至這么說,只要不涉及到 BPM split/join, 使用狀態機都是最佳方案。
狀態機的幾個概念
-
狀態轉換 transition
目標狀態僅由前置狀態經過一個事件觸發實現狀態轉換。 -
守衛條件 Guard condition
從前置狀態可以無條件地通過一個觸發條件轉換到目標狀態,也可以有條件地通過觸發事件轉換。
這里的條件在狀態機中被叫做 guard condition。
需要說明的是, guard condition 不應該被理解成狀態轉換的 pre check 條件,而是不同的目標狀態的的區分條件。
(A) ----triggerB (condition1) --> (B1)
|
| -----triggerB (condition2) --> (B2)
Java 狀態機
我比較喜歡 C# Stateless 類庫,在 Java 中也有很多狀態機類庫, 比如 Squirrel、Spring Statemachine、EasyFlow、Apache SCXML。 和 C# Stateless 風格最像的是 Squirrel, C# Stateless 有的功能它基本都有,也可以導出 graphviz 流程圖,只是缺少導出 mermaid 流程圖,考慮到 mermaid 流程DSL非常簡單, 自己實現也很簡單。
Squirrel:https://github.com/hekailiang/squirrel
C# Stateless 狀態機開源庫
Stateless github 是 C#中最流行的狀態機實現,功能非常強大:
- 支持守衛 condition
- 支持帶參數的 trigger
- 內建很多事件可供綁定
- 可進行觸發檢查
- 支持父子狀態
Tips: 使用 PermitIf() 啟用守衛 condition時,可以增加一個說明類型參數, 該參數可以在導出流程圖時輸出到trigger的label上,這個說明對于理解狀態轉換非常有幫助。 無條件的狀態轉換沒有辦法傳入這個說明參數, 所以即使是無條件的狀態轉換,推薦增加一個恒成立的守衛 condition,以便能加上這個說明參數。
示例代碼
using System.Runtime.CompilerServices;
using Stateless;
namespace ConsoleApp1
{
internal class Program
{
private static void Main(string[] args)
{
// Console.WriteLine("==================");
// Order goodsOrder = new Order(OrderState.Created);
// goodsOrder.isVirtualOrder = false;
// goodsOrder.pay("OpeatorA");
// Console.WriteLine("==================");
// Order virtualOrder = new Order(OrderState.Created);
// virtualOrder.isVirtualOrder = true;
// virtualOrder.pay("OpeatorB");
// Console.WriteLine("==================");
// Order paidOrder = new Order(OrderState.Paid);
// paidOrder.ship();
// Console.WriteLine("==================");
// Order shippedOrder = new Order(OrderState.Shipped);
// shippedOrder.ship();
//導出 graphviz 流程圖(.dot文件)
//最好使用綁定狀態機最開始的state對象來導出,這樣流程圖看起來更加漂亮, 當然也可以任意狀態的狀態機導出。
// Console.WriteLine("==================");
// Order oneOrder = new Order(OrderState.Created);
// Console.WriteLine(oneOrder.exportToGraphviz());
//導出 mermaid 流程圖
//最好使用綁定狀態機最開始的state對象來導出,這樣流程圖看起來更加漂亮, 當然也可以任意狀態的狀態機導出。
Console.WriteLine("==================");
Order oneOrder2 = new Order(OrderState.Shipped);
Console.WriteLine(oneOrder2.exportToMermaid());
}
}
public class Order
{
public bool isVirtualOrder { get; set; }
private string _operatorName;
/// <summary>
/// 狀態機對象應該從屬于業務對象
/// </summary>
private StateMachine<OrderState, OrderTrigger> _stateMachine;
/// <summary>
/// 聲明一個帶參數的trigger
/// 狀態機只允許為一個trigger 枚舉值綁定一個帶參數的trigger
/// </summary>
private StateMachine<OrderState, OrderTrigger>.TriggerWithParameters<string> _payTrigger;
public Order(OrderState orderState)
{
// 初始化狀態機對象
// 需要指定狀態機的 initialState, 需要注意的是, 它并不是一定是整個流程中最開始的狀態, 而是本業務對象的當前狀態
_stateMachine = new StateMachine<OrderState, OrderTrigger>(orderState);
_stateMachine.OnTransitionCompleted(
(transition) => { Console.WriteLine($"Source:{transition.Source} Destination:{transition.Destination}"); }
);
//非傳參trigger,直接使用 trigger enum 即可
//傳參trigger, 必須聲明為 TriggerWithParameters 類型, 參數需要通過 Fire() 函數傳入, 需要在“目標State”的Configuration下通過 OnEntryFrom() 來接收參數
_payTrigger = _stateMachine.SetTriggerParameters<string>(OrderTrigger.Pay);
//演示使用守衛條件guard condition的狀態轉換,PermitIf()中的條件應該是互斥的.
//最好要為守衛條件設置一個描述信息,這個描述信息將會體現到導出流程圖的transition label上
_stateMachine.Configure(OrderState.Created)
.PermitIf(OrderTrigger.Pay, OrderState.Paid, () => { return isVirtualOrder == false; }, "Non virtual order")
.PermitIf(OrderTrigger.Pay, OrderState.Shipped, () => { return isVirtualOrder == true; }, "Virtual order");
_stateMachine.Configure(OrderState.Paid)
//用來接收Trigger傳參的OnEntryFrom()調用, 要放到“目標State”的Configuration下
.OnEntryFrom(_payTrigger, operatorName => { _operatorName = operatorName; Console.WriteLine($"{_operatorName}"); })
.Permit(OrderTrigger.Ship, OrderState.Shipped);
_stateMachine.Configure(OrderState.Shipped)
.Permit(OrderTrigger.Confirm, OrderState.Closed);
}
public void pay(string operatorName)
{
Console.WriteLine(_stateMachine.State);
_stateMachine.Fire(_payTrigger, operatorName);
Console.WriteLine(_stateMachine.State);
}
public void ship()
{
if (_stateMachine.CanFire(OrderTrigger.Ship))
{
Console.WriteLine(_stateMachine.State);
_stateMachine.Fire(OrderTrigger.Ship);
Console.WriteLine(_stateMachine.State);
}
else
{
Console.WriteLine($"Cannot fire {OrderTrigger.Ship} in state {_stateMachine.State}");
}
}
/// <summary>
///導出 graphviz 流程圖(.dot文件), 可以使用 VS code的 Graphviz Interactive Preview 插件預覽.dot 文件流程圖
///最好使用綁定狀態機最開始的state對象來導出,這樣流程圖看起來更加漂亮, 當然也可以任意狀態的狀態機導出。
/// </summary>
/// <returns></returns>
public string exportToGraphviz()
{
return Stateless.Graph.UmlDotGraph.Format(_stateMachine.GetInfo());
}
/// <summary>
/// 將輸出內容加到 markdown 的 code block 中, 形式為:
/// ```mermaid 流程內容 ```
/// VS code 可以安裝 Markdown Preview Mermaid Support進行預覽, 或者通過網站 https://mermaid.live/ 瀏覽
/// </summary>
/// <returns></returns>
public string exportToMermaid()
{
return Stateless.Graph.MermaidGraph.Format(_stateMachine.GetInfo());
}
}
public enum OrderState
{ Created, Paid, Shipped, Closed }
public enum OrderTrigger
{ Pay, Ship, Confirm }
}
輸出的graphviz流程圖:

輸出的mermaid流程圖:


浙公網安備 33010602011771號