使用 Switch Expression 建立 State Machine 控管審核流程
前言
在實作審核流程時,會限制每個「審核狀態」下允許執行的「動作」控制邏輯,這時候可以考慮把這些邏封裝在 State Machine 中,統一由此 State Machine 控制先前所提到的「審核狀態」及「動作」的互動關係,讓邏輯不至於分散四處;本文使用 C# 8.0 的 Switch Expression 特性來建立一個 State Machine 進行說明。
情境
先以一個簡單的情境來進行說明,通常審核流程都會包含以下幾個「審核狀態」及「動作」,而案件在各個「審核狀態」流動會依照狀態圖中的「動作」進行變化,沒有定義到的路線就表示不允許這樣的操作 (e.g. 例如案件狀態為草稿時,是不允許直接透過任何的動作來直接改變狀態為審核通過)。
實作
先把各物件的角色及作用定義一下:
- FormState: 定義所有審核狀態
- FormAction: 定義所有審核動作
- FormStateMachine: 狀態機,管理狀態與操作的互動
- ProductForm: 商品審核表單
- Product: 商品本身
先把所有的「審核狀態」及「動作」整理出來:
審核狀態包含【草稿】、【待審核】、【審核通過】及【審核被拒】。
/// <summary>
/// 表單狀態
/// </summary>
public enum FormState
{
/// <summary>
/// 狀態:草稿
/// </summary>
Draft,
/// <summary>
/// 狀態:審核中
/// </summary>
Approving,
/// <summary>
/// 狀態:審核通過
/// </summary>
Approved,
/// <summary>
/// 狀態:審核被拒
/// </summary>
Reject,
}
動作包含【送審】、【核准】、【拒絕】及【編輯】。
/// <summary>
/// 表單動作
/// </summary>
public enum FormAction
{
/// <summary>
/// 動作:送審
/// </summary>
Submit,
/// <summary>
/// 動作:核准
/// </summary>
Approve,
/// <summary>
/// 動作:退回
/// </summary>
Reject,
/// <summary>
/// 動作:編輯
/// </summary>
Edit,
}
接著利用 Switch Expression 特性建立一個 FormStateMachine 物件,並且依照先前定義的狀態圖解析「狀態」與「動作」的互動於 Manipulate 方法中;描述的方式就是以【目前狀態】執行某個【動作】後會轉變成的【新狀態】進行定義,而沒有定義在上面的情境就表示非法操作,我們直接拋出例外錯誤即可。
後續若需額外求允許其他的操作時,僅需異動 FormStateMachine 的 Manipulate 邏輯即可。
/// <summary>
/// 表單 State Machine
/// </summary>
public class FormStateMachine
{
/// <summary>
/// 目前狀態
/// </summary>
private FormState _currentState;
/// <summary>
/// 建構子
/// </summary>
/// <param name="state">初始狀態</param>
public FormStateMachine(FormState state)
{
_currentState = state;
}
/// <summary>
/// 定義動作與狀態互動關係
/// </summary>
/// <param name="action">動作</param>
/// <returns>新狀態</returns>
private FormState Manipulate(FormAction action)
=> (_currentState, action) switch
{
// 【草稿】 執行"送審" => 【待審核】
(FormState.Draft, FormAction.Submit) => FormState.Approving,
// 【待審核】 執行"核准" => 【審核通過】
(FormState.Approving, FormAction.Approve) => FormState.Approved,
// 【待審核】 執行"退回" => 【送審被拒】
(FormState.Approving, FormAction.Reject) => FormState.Reject,
// 【審核通過】 執行"編輯" => 【草稿】
(FormState.Approved, FormAction.Edit) => FormState.Draft,
// 【送審被拒】 執行"編輯" => 【草稿】
(FormState.Reject, FormAction.Edit) => FormState.Draft,
_ => throw new Exception($"目前狀態 {_currentState} 不允許執行 {action} 動作!"),
};
/// <summary>
/// 執行動作來改變狀態
/// </summary>
/// <param name="action">動作</param>
/// <returns>新狀態</returns>
public FormState Transition(FormAction action)
{
// 執行動作取得新狀態
var newState = Manipulate(action);
// 更新目前狀態
_currentState = newState;
return newState;
}
/// <summary>
/// 提供允許執行動作清單
/// </summary>
/// <returns>允許執行動作清單</returns>
public List<FormAction> AvailableActions()
{
var availableActions = new List<FormAction>();
foreach (FormAction action in Enum.GetValues(typeof(FormAction)))
{
try
{
// 模擬執行動作
Manipulate(action);
// 有定義此狀態可以執行的動作時就加入清單
availableActions.Add(action);
}
catch (Exception)
{
// 非法操作就不列入
}
}
return availableActions;
}
}
建立一個 Product 物件作為 DB 資料代表,其中包含此商品的「審核狀態」資訊於物件中。
/// <summary>
/// Product
/// </summary>
public class Product
{
public int ProductId { get; set; }
public string ProductName { get; set; }
/// <summary>
/// 審核狀態
/// </summary>
public FormState State { get; set; }
}
接著我們就是要在審核的實作中導入 FormStateMachine 來幫我們控制審核流程。
要進行審核流程時,先建立一個 ProductForm 代表商品審核表單物件實體,在建構子中傳入 Product 商品資訊,並以此商品的審核狀態初始 FormStateMachine 物件來產生實體,後續當使用者要對此商品進行審核流程的操作時,可以透過 DoAction 指定動作來對此商品執行審核動作,其中可否執行此動作的邏輯就交由 FormStateMachine 統一決定,若此操作是合法的就會回覆新狀態,若否則拋出錯誤表示此操作尚未定義。
/// <summary>
/// Product 審核表單
/// </summary>
public class ProductForm
{
private readonly Product _product;
private readonly FormStateMachine _stateMachine;
/// <summary>
/// 建構子
/// </summary>
/// <param name="product">Product 資訊</param>
public ProductForm(Product product)
{
// 傳入 Product 物件
_product = product;
// 取得 Product 的目前狀態後,初始 StateMachine 實體
var currentState = _product.State;
_stateMachine = new FormStateMachine(currentState);
}
/// <summary>
/// 執行表單動作
/// </summary>
/// <param name="action">表單動作</param>
public void DoAction(FormAction action)
{
// 取得目前狀態下執行【傳入動作】的下個狀態
var newState = _stateMachine.Transition(action);
// 如果目前狀態不能執行【傳入動作】行為 => 會拋出 Exception
// 如果目前狀態可以執行【傳入動作】行為 => 會取得新狀態 newState
// TODO: 使用新狀態去更新該筆資料(DB)的狀態
Console.WriteLine($"目前狀態 {_product.State} 執行 {action} 動作後,狀態改變成 {newState} ");
_product.State = newState;
}
/// <summary>
/// 提供允許執行動作清單
/// </summary>
/// <returns>允許執行動作清單</returns>
public List<FormAction> AvailableActions()
{
return _stateMachine.AvailableActions();
}
}
實際演練
模擬目前從 DB 取出的商品狀態為「草稿」,建立一個商品審核表單並載入商品資訊,接著執行「送審」、「核准」及「編輯」三個動作,預期會依照我們的規劃(如下圖)去轉換不同的審核狀態。
internal class Program
{
private static void Main(string[] args)
{
// 模擬從資料庫讀出商品資訊
var product = new Product { State = FormState.Draft };
// 建立商品審核表單
var productForm = new ProductForm(product);
try
{
// 透過產品表單將產品執行審核流程動作
Console.Write("目前狀態可執行的 Action 為 ");
productForm.AvailableActions().ForEach(action => Console.Write("{0}, ", action));
productForm.DoAction(FormAction.Submit); // 送審
Console.Write("目前狀態可執行的 Action 為 ");
productForm.AvailableActions().ForEach(action => Console.Write("{0}, ", action));
productForm.DoAction(FormAction.Approve); // 核准
Console.Write("目前狀態可執行的 Action 為 ");
productForm.AvailableActions().ForEach(action => Console.Write("{0}, ", action));
productForm.DoAction(FormAction.Edit); // 編輯
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
}
執行結果正常輸出,狀態與動作的互動正常,轉換的新狀態也如預期變化。
再來調整一下流程,測試一下非法的狀態與動作互動;初始的狀態仍為「草稿」,首先會先進行「送審」這個合法的操作,然後執行「編輯」這個動作,但因為在「待審核」這個狀態下並無定義「編輯」這個動作於 FormStateMachine 中,因此會被判斷為非法行為並拋出錯誤。
internal class Program
{
private static void Main(string[] args)
{
// 模擬從資料庫讀出商品資訊
var product = new Product { State = FormState.Draft };
// 建立商品審核表單
var productForm = new ProductForm(product);
try
{
// 透過產品表單將產品執行審核流程動作
Console.Write("目前狀態可執行的 Action 為 ");
productForm.AvailableActions().ForEach(action => Console.Write("{0}, ", action));
productForm.DoAction(FormAction.Submit); // 送審
Console.Write("目前狀態可執行的 Action 為 ");
productForm.AvailableActions().ForEach(action => Console.Write("{0}, ", action));
productForm.DoAction(FormAction.Edit); // 編輯
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
}
結果也如預期般呈現,審核中的表單是不允許執行編輯動作的,因此顯示錯誤訊息。
後記
透過 State Machine 來管理狀態的變化,讓職責可以切分出來,可避免到處存在 if else 判斷式造成程式的複雜度及錯誤風險升高,如果有類似的需求可以考慮使用這種方式進行實作,讓程式碼乾淨又好維護。
希望此篇文章可以幫助到需要的人
若內容有誤或有其他建議請不吝留言給筆者喔 !