推薦一款高效能狀態機管理解決方案

推薦一款高效能狀態機管理解決方案

在實際軟體開發中,尤其是工業軟體,每一款設備都有複雜的狀態以及狀態之間的切換的功能需求,在這種情況下,如何管理狀態以及狀態之間切換,和對應狀態下的功能控制,成為非常重要的一個問題。

最後更新 2025/8/13 上午8:25
老码识途呀
預計閱讀 13 分鐘
分類
分享 .NET
標籤
.NET C# 工業軟體 狀態機

在實際軟體開發中,尤其是工業軟體,每一款設備都有複雜的狀態以及狀態之間的切換功能需求,在這種情況下,如何管理狀態以及狀態之間切換,和對應狀態下的功能控制,成為一個非常重要的問題。如果處理不好,那麼這種繁複的狀態將成為「像麵條一樣」纏繞耦合,一團亂麻,真的就是「剪不斷,理還亂」。那如何解決這個問題呢?今天我們以一篇簡單的小例子,簡述如何透過 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):

  1. Idle 狀態,表示當前沒有執行任務,處於空閒狀態。
  2. Running 狀態,表示當前正在工作,處於執行狀態。
  3. Malfunction 狀態,表示當前有異常,處於故障狀態。
  4. Recovery 狀態,表示經過維修,從故障中恢復了,處於修復狀態。

它擁有觸發動件 (Trigger):

  1. Work,表示當前開始工作,它可以由 Idle 狀態進入,也可以從 Recovery 狀態進入,程式會進入 Running 狀態。
  2. Fatal,表示遇到異常,它只可以由 Running 狀態進入,程式會進入 Malfunction 狀態。
  3. Repair,表示修復異常,它只可以由 Malfunction 狀態進入,程式會進入 Recovery 狀態。
  4. 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 按鈕,預設無故障時如下所示:

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

以上就是《推薦一款高效能狀態機管理解決方案》的全部內容,關於更多詳細內容,可參考官方文件。希望能夠一起學習,共同進步。

學習程式設計,從關注【老碼識途】開始,為大家分享更多文章!!!

繼續探索

延伸閱讀

更多文章
同分類 / 同標籤 2025/3/18

(7)從護士到C#開發者--物件導向程式設計基礎

作為一名從護理行業轉行的程式設計師,我將分享如何透過醫護工作經驗來理解物件導向程式設計的概念。本文將介紹類別、物件、屬性、方法等物件導向的核心概念,並結合醫療保健領域的實例來加深理解。

繼續閱讀
同分類 / 同標籤 2025/2/25

.NET 10 Preview 1 發佈

今天 .NET 10 Preview 1 發佈了,我第一時間下載,升級了 Avalonia UI 專案和部落格網站,前者功能測試及 AOT 發佈正常,後者偵錯正常,Docker 暫時未成功

繼續閱讀