実際のソフトウェア開発、特に産業用ソフトウェアでは、各デバイスに複雑な状態と状態間の切り替え機能要件があります。このような場合、状態と状態間の切り替え、および対応する状態の機能制御をどのように管理するかは、非常に重要な問題になります。うまく処理できなければ、この煩雑な状態は「麺のように」絡み合い、複雑に絡み合ってしまい、「切っても切れない、整理できない」状態になります。では、この問題をどのように解決すればよいのでしょうか。今回は簡単な小さな例を用いて、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はステートマシンのコアクラスであり、2つのジェネリックパラメータ(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ボタンをクリックします。デフォルトでは障害なしで次のようになります:

障害をチェックして異常をシミュレートすると、次のようになります:

以上が「おすすめの高性能ステートマシン管理ソリューション」の全内容です。詳細については公式ドキュメントを参照してください。一緒に学び、共に成長できることを願っています。
プログラミング学習は、【老码识途】をフォローして始めましょう。さらに多くの記事を共有します!!!