延伸學習 Switch Expression 的使用方式
前言
筆者先前文章【使用 Switch Expression 建立 State Machine 控管審核流程】有使用到 C# 8.0 的 Switch Express 特性來建立一個簡單的 state machine 機制,這篇文章再針對 Switch Express 的應用繼續學習。
Switch Expression
以先前文章中的操作來看,主要都是定義好條件,當符合條件時「直接」回傳一個值出來。
public class StateMachine
{
/// <summary>
/// 狀態機目前狀態
/// </summary>
private FormState _currentState;
/// <summary>
/// 建構子
/// </summary>
/// <param name="state">初始狀態</param>
public StateMachine(FormState state)
{
_currentState = state;
}
/// <summary>
/// 定義動作與狀態互動關係
/// </summary>
/// <param name="action">動作</param>
/// <returns>新狀態</returns>
public 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} 動作!"),
};
}
有邏輯操作的空間嗎?
我們是不是可以像在傳統的 switch case 中插入一段邏輯於其中,決定要 return 的資料是什麼? 答案是可以,如下定義一個 GetState 方法,並傳入 Func 實際要取得新狀態的邏輯方法即可;在實際的使用情境上,也許不單只有 _currentState 及 action 就可以決定下個新 state 狀態,也許會有一些特定「外部條件」來影響並決定這組 _currentState 及 action 對於狀態變化,因此就可以利用這種方式來處理。
/// <summary>
/// 取得狀態
/// </summary>
/// <param name="f">取得狀態的方法</param>
/// <returns>狀態</returns>
private FormState GetState(Func<FormState> f) => f();
/// <summary>
/// 定義動作與狀態互動關係
/// </summary>
/// <param name="action">動作</param>
/// <returns>新狀態</returns>
public FormState Manipulate(FormAction action)
=> (_currentState, action) switch
{
// 【草稿】 執行"送審" =>
(FormState.Draft, FormAction.Submit) => GetState(() =>
{
// 判斷系統參數來決定是否已啟用審核機制
var isApproveFlowEnabled = IsUseApproveFlow();
// (已啟用審核機制) ? [待審核] : [審核通過]
return isApproveFlowEnabled ? FormState.Approving : FormState.Approved;
}),
// 【待審核】 執行"核准" => 【審核通過】
(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} 動作!"),
};
有多餘的條件怎樣處理
若以前述方式來處理邏輯,會比較難以一目了然的看出個別情境的條件邏輯,因此可考慮將「外部條件」邏輯整理成簡單的 flag 傳入,使用 when 來加註狀態,讓列表更清晰表達出 switch case 條件式。
public FormState ManipulateYo2(FormAction action)
{
var isApproveFlowEnabled = IsUseApproveFlow();
return (_state, action) switch
{
// [草稿] 送審後 => [待審核] (已啟用審核機制)
(FormState.Draft, FormAction.Submit) when isApproveFlowEnabled => FormState.Approving,
// [草稿] 送審後 => [審核通過] (未啟用審核機制)
(FormState.Draft, FormAction.Submit) when !isApproveFlowEnabled => FormState.Approved,
// [待審核] 審後 => [審核通過]
(FormState.Approving, FormAction.Approve) => FormState.Approved,
// [審核通過] 編輯 => [草稿]
(FormState.Approved, FormAction.Edit) => FormState.Draft,
// [待審核] 退回 => [送審被拒]
(FormState.Approving, FormAction.Reject) => FormState.Reject,
// [送審被拒] 編輯 => [草稿]
(FormState.Reject, FormAction.Edit) => FormState.Draft,
_ => throw new BusinessException(BusinessError.FormFlowInvalid,
_errorCodeLocalizer["BusinessError.FormFlowInvalid", _state, action]),
};
}
但這樣有個缺點,每個陳述無法被檢核為獨一無二的條件,例如以下相同條件無法被 IDE 偵測出來,當在複雜情境下使用時有些不便,會有重複定義的問題產生。
所以我會傾向多定義一個參數,若部分的 switch case 沒使用到這個參數時,可以使用 _ 忽略它。
public FormState ManipulateYo1(FormAction action)
{
var isRequiredApprove = IsUseApproveFlow();
return (_state, action, isRequiredApprove) switch
{
// [草稿] 送審後 => [待審核] (已啟用審核機制)
(FormState.Draft, FormAction.Submit, true) => FormState.Approving,
// [草稿] 送審後 => [審核通過] (未啟用審核機制)
(FormState.Draft, FormAction.Submit, false) => FormState.Approved,
// [待審核] 審後 => [審核通過]
(FormState.Approving, FormAction.Approve, _) => FormState.Approved,
// [審核通過] 編輯 => [草稿]
(FormState.Approved, FormAction.Edit, _) => FormState.Draft,
// [待審核] 退回 => [送審被拒]
(FormState.Approving, FormAction.Reject, _) => FormState.Reject,
// [送審被拒] 編輯 => [草稿]
(FormState.Reject, FormAction.Edit, _) => FormState.Draft,
_ => throw new BusinessException(BusinessError.FormFlowInvalid,
_errorCodeLocalizer["BusinessError.FormFlowInvalid", _state, action]),
};
}
這樣如果有相同條件出現,就可以馬上被檢核出來,避免重複定義的問題產生。
參考資訊
C# 8 Switch Expressions with Pattern Matching
希望此篇文章可以幫助到需要的人
若內容有誤或有其他建議請不吝留言給筆者喔 !