在實際軟體開發中,尤其是工業軟體,每一款設備都有複雜的狀態以及狀態之間的切換功能需求,在這種情況下,如何管理狀態以及狀態之間切換,和對應狀態下的功能控制,成為一個非常重要的問題。如果處理不好,那麼這種繁複的狀態將成為「像麵條一樣」纏繞耦合,一團亂麻,真的就是「剪不斷,理還亂」。那如何解決這個問題呢?今天我們以一篇簡單的小例子,簡述如何透過 Stateless 元件,完成狀態的管理和觸發,僅供學習分享使用,如有不足之處,還請指正。
什麼是狀態模式?
關於狀態模式和狀態機,如下所示:
狀態模式:「允許一個物件在其內部狀態改變時改變它的行為。物件看起來似乎修改了它所屬的類別。」(State Pattern: "Allow an object to alter its behavior when its internal state changes. The object will appear to change its class.")。對於這個定義,有點抽象,變通理解一下可以這麼理解:狀態擁有者將變更行為委託給狀態物件,狀態擁有者本身只擁有狀態(當然也可以拋棄狀態物件),狀態物件履行變更職責。
狀態機:「依照指定的狀態流程圖,根據當前執行的動作,將當前狀態按照預定的條件變更到新的狀態」。狀態機有 4 個要素,即現態、條件、動作、次態。其中,現態和條件是「因」,動作和次態是「果」。
- 現態 - 是指當前物件的狀態
- 條件 - 當一個條件滿足時,當前物件會觸發一個動作
- 動作 - 條件滿足之後,執行的動作
- 次態 - 條件滿足之後,當前物件的新狀態。次態是相對現態而言的,次態一旦觸發,就變成了現態
狀態遷移圖:「在 UML 建模中,常常可見,用來描述一個特定的物件所有可能的狀態,以及由於各種事件的發生而引起的狀態之間的轉移和變化,也是配置狀態機按照何種行徑的前提」。
什麼是 Stateless?
Stateless 是一個輕量級、高效能的狀態機庫,它基於 .Net Standard 實現,在 .NET Framework 和 .NET Core 專案中都可以使用,能夠輕鬆地幫助我們實現狀態轉換的邏輯。
- github:https://github.com/dotnet-state-machine/stateless

實例場景說明
本文主要透過 Stateless 元件實現如下一個狀態的管理和觸發,當前有一個設備軟體,它擁有如下幾個狀態 (State):
- Idle 狀態,表示當前沒有執行任務,處於空閒狀態。
- Running 狀態,表示當前正在工作,處於執行狀態。
- Malfunction 狀態,表示當前有異常,處於故障狀態。
- Recovery 狀態,表示經過維修,從故障中恢復了,處於修復狀態。
它擁有觸發動件 (Trigger):
- Work,表示當前開始工作,它可以由 Idle 狀態進入,也可以從 Recovery 狀態進入,程式會進入 Running 狀態。
- Fatal,表示遇到異常,它只可以由 Running 狀態進入,程式會進入 Malfunction 狀態。
- Repair,表示修復異常,它只可以由 Malfunction 狀態進入,程式會進入 Recovery 狀態。
- Finish,表示完成,它可以由 Running 狀態進入,也可以由 Recovery 狀態進入,程式會進入 Idle 狀態。
上述狀態 (State) 和觸發動件 (Trigger) 以及之間的流轉,如下圖所示:

建立專案
為了示範狀態變化,我們建立一個 WinForm 應用程式「Okcoder.Stateless.WinTool」和一個公共庫「Okcoder.Stateless.Common」,如下所示:

安裝 Stateless 元件
在 Visual Studio 2022 開發工具中,可以透過 NuGet 套件管理員進行安裝。在解決方案右鍵,開啟右鍵選單,然後點選「管理解決方案的 NuGet 程式套件」在開啟的 NuGet 套件解決方案,搜尋 Stateless,然後安裝即可。目前最新版本為 v5.18.0,如下所示:

定義狀態和觸發器
狀態機管理狀態的變化和觸發事件,所以定義狀態和觸發器,它們是列舉類型,根據實例場景說明進行定義 ToolState 如下所示:
namespace Okcoder.Stateless.Common
{
/// <summary>
/// 工具狀態
/// </summary>
public enum ToolState
{
Idle=0, //空閒狀態
Running=1, //執行狀態
Malfunction = 2, // 故障狀態
Recovery=3, //已恢復狀態
}
}
ToolTrigger 定義如下所示:
namespace Okcoder.Stateless.Common
{
public enum ToolTrigger
{
Work = 0, // 開始運轉
Fatal = 1, // 出異常
Finish = 2, // 完成
Repair=3, //維修
}
}
定義介面
定義 IToolStateService 介面,它表示狀態機所具備的能力,如觸發動件、狀態判斷、狀態機匯出 DotGraph 等,如下所示:
namespace Okcoder.Stateless.Common
{
/// <summary>
/// 狀態機介面
/// </summary>
public interface IToolStateService
{
/// <summary>
/// 初始化狀態
/// </summary>
void Init();
/// <summary>
/// 開始工作
/// </summary>
/// <returns></returns>
ToolResult Work();
/// <summary>
/// 遇到嚴重錯誤
/// </summary>
/// <returns></returns>
ToolResult Fatal();
/// <summary>
/// 維修
/// </summary>
/// <returns></returns>
ToolResult Repair();
/// <summary>
/// 完成
/// </summary>
/// <returns></returns>
ToolResult Finish();
/// <summary>
/// 是否是 Idle 狀態
/// </summary>
/// <returns></returns>
bool IsIdleState();
/// <summary>
/// 是否執行狀態
/// </summary>
/// <returns></returns>
bool IsRunningState();
/// <summary>
/// 是否故障狀態
/// </summary>
/// <returns></returns>
bool IsMalfunctionState();
/// <summary>
/// 是否恢復狀態
/// </summary>
/// <returns></returns>
bool IsRecoveryState();
/// <summary>
/// 取得當前狀態
/// </summary>
/// <returns></returns>
ToolState GetToolState();
/// <summary>
/// 狀態機匯出 DotGraph
/// </summary>
/// <param name="path"></param>
void ExportDotGraph(string path);
}
}
定義設備工作 IToolWorkService 介面,它表示設備執行工作,執行操作。如下所示:
namespace Okcoder.Stateless.Common
{
/// <summary>
/// 設備工作流程介面
/// </summary>
public interface IToolWorkService
{
/// <summary>
/// 是否製造故障
/// </summary>
bool IsMakeFatal { get; set; }
/// <summary>
/// 開始工作
/// </summary>
void DoWork();
/// <summary>
/// 維修
/// </summary>
void Repair();
}
}
實作服務
實作狀態機服務 ToolStateService,它主要應用 Stateless 元件實現狀態的管理和觸發動件切換狀態,如下所示:
using Stateless;
using Stateless.Graph;
using System;
using System.IO;
using System.Text;
namespace Okcoder.Stateless.Common
{
/// <summary>
/// 狀態機服務
/// </summary>
public class ToolStateService : IToolStateService
{
private Action<ToolState> toolAction;// 動作
private StateMachine<ToolState, ToolTrigger> toolStateMachine;
public ToolStateService(Action<ToolState> toolAction)
{
//定義狀態機
this.toolStateMachine = new StateMachine<ToolState, ToolTrigger>(ToolState.Idle);
//設定每一個狀態所允許的動作
this.toolStateMachine.Configure(ToolState.Idle).Permit(ToolTrigger.Work, ToolState.Running);
this.toolStateMachine.Configure(ToolState.Running).Permit(ToolTrigger.Fatal, ToolState.Malfunction).Permit(ToolTrigger.Finish, ToolState.Idle);
this.toolStateMachine.Configure(ToolState.Malfunction).Permit(ToolTrigger.Repair, ToolState.Recovery);
this.toolStateMachine.Configure(ToolState.Recovery).Permit(ToolTrigger.Work, ToolState.Running).Permit(ToolTrigger.Finish, ToolState.Idle);
this.toolAction = toolAction;
}
/// <summary>
/// 初始化狀態
/// </summary>
public void Init()
{
this.toolAction(ToolState.Idle);
}
/// <summary>
/// 回應觸發動件
/// </summary>
/// <param name="trigger"></param>
private ToolResult Fire(ToolTrigger trigger)
{
ToolResult toolResult = new ToolResult();
try
{
if (!this.toolStateMachine.CanFire(trigger))
{
toolResult.IsOk = false;
toolResult.Desc = $"當前狀態是{this.toolStateMachine.State},不允許執行{trigger}操作";
}
else
{
this.toolStateMachine.Fire(trigger);
toolResult.IsOk = true;
toolResult.Desc = "Ok";
DoAction();
}
}
catch (InvalidOperationException ex)
{
toolResult.IsOk = false;
toolResult.Desc = ex.Message;
}
return toolResult;
}
/// <summary>
/// 判斷是否在狀態
/// </summary>
/// <param name="toolState"></param>
/// <returns></returns>
private bool IsInState(ToolState toolState)
{
return this.toolStateMachine.IsInState(toolState);
}
/// <summary>
/// 事件通知
/// </summary>
private void DoAction()
{
if (this.toolAction != null)
{
this.toolAction(this.toolStateMachine.State);
}
}
/// <summary>
/// 開始工作
/// </summary>
public ToolResult Work()
{
return this.Fire(ToolTrigger.Work);
}
/// <summary>
/// 遇到嚴重錯誤
/// </summary>
public ToolResult Fatal()
{
return this.Fire(ToolTrigger.Fatal);
}
/// <summary>
/// 維修
/// </summary>
public ToolResult Repair()
{
return this.Fire(ToolTrigger.Repair);
}
/// <summary>
/// 完成
/// </summary>
public ToolResult Finish()
{
return this.Fire(ToolTrigger.Finish);
}
/// <summary>
/// 是否是 Idle 狀態
/// </summary>
/// <returns></returns>
public bool IsIdleState()
{
return this.IsInState(ToolState.Idle);
}
/// <summary>
/// 是否執行狀態
/// </summary>
/// <returns></returns>
public bool IsRunningState()
{
return this.IsInState(ToolState.Running);
}
/// <summary>
/// 是否故障狀態
/// </summary>
/// <returns></returns>
public bool IsMalfunctionState()
{
return this.IsInState(ToolState.Malfunction);
}
/// <summary>
/// 是否恢復狀態
/// </summary>
/// <returns></returns>
public bool IsRecoveryState()
{
return this.IsInState(ToolState.Recovery);
}
public ToolState GetToolState()
{
return this.toolStateMachine.State;
}
/// <summary>
/// 狀態機匯出 DotGraph
/// </summary>
/// <param name="path"></param>
public void ExportDotGraph(string path)
{
var info = this.toolStateMachine.GetInfo();
string graph = UmlDotGraph.Format(info);
using (FileStream stream = File.OpenWrite(path))
{
using (StreamWriter writer = new StreamWriter(stream))
{
writer.Write(graph);
writer.Close();
}
stream.Close();
}
}
}
}
其中 StateMachine 是狀態機的核心類別,它有兩個泛型參數,分別表示 ToolState 和 ToolTrigger,建構函式中傳入初始狀態,在本範例中,傳入 ToolStage.Idle 即可。
StateMachine 透過物件實例的 Configure 方法設定允許的狀態及透過 Permit 方法設定當前狀態允許透過 Trigger 轉換的新狀態。
設定完成後,透過 Fire 方法回應觸發動件,透過 IsInState 方法判斷當前是否處於指定的狀態。
在狀態機服務類別建構函式中,接收一個 Action<string> 的委託方法,用於輸出狀態到 UI 層,當然也可以採用其他事件訂閱發佈的解耦方法。
其中 ToolResult 回傳值,表示狀態機執行後回傳的介面,它是一個類別,如下所示:
namespace Okcoder.Stateless.Common
{
/// <summary>
/// 工具執行結果
/// </summary>
public class ToolResult
{
public bool IsOk { get; set; }
public string Desc { get; set; }
}
}
實作工作服務 ToolWorkService,主要是模擬設備工具執行,當開始工作時,輸出文字到 UI 頁面上,如果有異常,則停止;若無異常,則執行完成,如下所示:
namespace Okcoder.Stateless.Common
{
/// <summary>
/// 設備工作服務
/// </summary>
public class ToolWorkService:IToolWorkService
{
private Action<string> _progress;
private IToolStateService _toolStateService;
private bool _isMakeFatal=false;
public bool IsMakeFatal
{
get { return _isMakeFatal; }
set { _isMakeFatal = value; }
}
public ToolWorkService(IToolStateService toolStateService, Action<string> progress,bool isMakeFatal=false)
{
_progress = progress;
_toolStateService = toolStateService;
_isMakeFatal = isMakeFatal;
}
public void DoWork()
{
BeginWork();
Work();
Finish();
}
public void Repair()
{
var result = this._toolStateService.Repair();
if (result.IsOk)
{
OutPutInfo($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")} 故障被修好了");
}
else
{
OutPutInfo($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")} 故障沒有被修好");
}
}
/// <summary>
/// 開始工作
/// </summary>
private void BeginWork()
{
var result = this._toolStateService.Work();
if (result.IsOk)
{
OutPutInfo($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")} 開始工作了");
}
else
{
OutPutInfo($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")} 開始工作失敗了");
}
}
/// <summary>
/// 工作過程
/// </summary>
private void Work()
{
for (int i = 0; i < 30; i++)
{
if (_isMakeFatal)
{
if (i > 10)
{
MarkFatal();// 執行一段時間,報故障
break;
}
}
OutPutInfo($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")} 當前正在工作 - {i}");
Thread.Sleep(300);
}
}
/// <summary>
/// 完成工作
/// </summary>
private void Finish()
{
var result = this._toolStateService.Finish();
if (result.IsOk)
{
OutPutInfo($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")} 完成工作了");
}
else
{
OutPutInfo($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")} {result.Desc}");
}
}
private void MarkFatal()
{
var result = this._toolStateService.Fatal();
if (result.IsOk)
{
OutPutInfo($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")} 哎呀,出錯了呢?");
}
else
{
OutPutInfo($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")} 製造故障失敗");
}
}
/// <summary>
/// 輸出資訊
/// </summary>
/// <param name="msg"></param>
private void OutPutInfo(string msg)
{
if (_progress != null)
{
_progress(msg);
}
}
}
}
其中建構函式接收一個 Action<string> 的委託,用於向 UI 輸出資訊,當然也可以是其他的形式。
UI 呼叫
在 Okcoder.Stateless.WinTool 專案中,建立 FrmMain 頁面,在建構函式中定義 IToolStateService 和 IToolWorkService 的物件實例,並且在 Load 方法中初始化。
當使用者點擊 Work 時,開始呼叫 IToolWorkService 的 Work 方法,在執行過程中,會輸出資訊到 UI 頁面。
在 UI 頁面有一個核取方塊,用於設定是否觸發異常,可以模擬有異常的場景,如果出現異常,則點擊 Repair 按鈕進行修復。
在執行各個動作時,可以看到不同的狀態變化。如下所示:
using Okcoder.Stateless.Common;
namespace Okcoder.Stateless.WinTool
{
public partial class FrmMain : Form
{
private IToolStateService toolStateService;
private IToolWorkService toolWorkService;
public FrmMain()
{
InitializeComponent();
this.toolStateService = new ToolStateService(ToolStateAction);
this.toolWorkService = new ToolWorkService(toolStateService, ToolWorkProcessAction, this.chkFatal.Checked);
}
private void FrmMain_Load(object sender, EventArgs e)
{
this.toolStateService.Init();
}
/// <summary>
/// 開始工作
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btnWork_Click(object sender, EventArgs e)
{
this.txtInfo.Text = string.Empty;
this.toolWorkService.IsMakeFatal = this.chkFatal.Checked;
Task.Run(() =>
{
this.toolWorkService.DoWork();
});
}
/// <summary>
/// 維修
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btnRepair_Click(object sender, EventArgs e)
{
this.toolWorkService.Repair();
}
/// <summary>
/// 匯出狀態機 DotGraph 檔案
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btnExport_Click(object sender, EventArgs e)
{
SaveFileDialog saveFileDialog = new SaveFileDialog();
saveFileDialog.DefaultExt = "txt";
saveFileDialog.Filter = "txt 檔案|*.txt";
saveFileDialog.FileName = "Okcoder.txt";
if (saveFileDialog.ShowDialog() == DialogResult.OK)
{
var filePath = saveFileDialog.FileName;
this.toolStateService.ExportDotGraph(filePath);
MessageBox.Show("匯出成功");
}
}
private void ToolStateAction(ToolState toolState)
{
this.Invoke(() =>
{
this.lblState.Text = toolState.ToString();
switch (toolState)
{
case ToolState.Idle:
case ToolState.Recovery:
this.btnWork.Enabled = true;
this.btnRepair.Enabled = false;
this.lblState.ForeColor = Color.Black;
break;
case ToolState.Running:
this.btnWork.Enabled = false;
this.btnRepair.Enabled = false;
this.lblState.ForeColor = Color.Goldenrod;
break;
case ToolState.Malfunction:
this.btnWork.Enabled = false;
this.btnRepair.Enabled = true;
this.lblState.ForeColor = Color.Red;
break;
}
this.Refresh();
});
}
private void ToolWorkProcessAction(string msg)
{
this.Invoke(() =>
{
this.txtInfo.AppendText(msg + "\r\n");
});
}
}
}
實例示範
經過上述步驟,執行程式,然後點擊 Work 按鈕,預設無故障時如下所示:

當勾選故障,模擬出現故障時,如下所示:

以上就是《推薦一款高效能狀態機管理解決方案》的全部內容,關於更多詳細內容,可參考官方文件。希望能夠一起學習,共同進步。
學習程式設計,從關注【老碼識途】開始,為大家分享更多文章!!!