UMLのステートマシン図を実装する for C# - その6
まえがき
ステートマシンをCUI(コマンドライン)で動作させるアプリケーションを実装していきます。
Messenger クラス
状態遷移など、ステートマシンの内部状態を把握しやすくするために、任意の文字列(ログなど)をアプリケーションに送信するMessengerクラスを定義します。
送信された文字列の表示方法はアプリケーションの種類(CUI / GUI)によって変更できるように、デリゲートとして登録するようにします。
// メッセージ受信ハンドラ(デリゲート) public delegate void MessageReceivedHandler(string message); public static class Messenger { // メッセージ受信イベント(デリゲート) public static MessageReceivedHandler OnMessageReceived; // メッセージ送信 public static void Send(string message) { // メッセージ受信ハンドラが登録されていたら呼び出し Messenger.OnMessageReceived?.Invoke(message); } }
ステートマシンベースクラス(変更)
Messengerによるログ送信処理をステートマシンの各ベースクラスに追加します。
それぞれの箇所を示します。
StateMachine クラス
public void SendTrigger(Trigger trigger) { Messenger.Send($"Send Trigger : {trigger.Name}"); // ★ this.CurrentState?.SendTrigger(this, trigger); } public void ChangeState(State new_state, Effect effect = null) { if (this.CurrentState != new_state) { var old_state = this.CurrentState; this.CurrentState?.ExecuteExitAction(this); this.CurrentState = new_state; this.PreviousState = old_state; if (old_state != null) // ★ { // ★ Messenger.Send($"State Changed : {old_state} => {new_state}"); // ★ } // ★ effect?.Execute(this); this.CurrentState?.ExecuteEntryAction(this); } }
State クラス
public void ExecuteEntryAction(StateMachine context) { Messenger.Send($"Entry : {this.Name}"); //★ this.OnEntry?.Invoke(context); } public void ExecuteDoAction(StateMachine context) { Messenger.Send($"Do : {this.Name}"); //★ this.OnDo?.Invoke(context); } public void ExecuteExitAction(StateMachine context) { Messenger.Send($"Exit : {this.Name}"); //★ this.OnExit?.Invoke(context); } public void SendTrigger(StateMachine context, Trigger trigger) { if (this.TriggerActionMap.ContainsKey(trigger.Name) == true) { Messenger.Send($"Trigger : {trigger.Name}"); //★ var action = this.TriggerActionMap[trigger.Name]; var args = new TriggerActionArgs(context, trigger); action(args); } }
Effect クラス
public void Execute(StateMachine context) { Messenger.Send($"Execute : {this.Name}"); // ★ this.ExecuteAction(context); }
Program クラス
最後に、コンソールアプリケーション部分の実装です。
コンソールアプリケーション用プロジェクトを作成し、Mainメソッドを実装していきます。
class Program { static void Main(string[] args) { } }
メッセージ受信ハンドラ
ステートマシンから送信されたメッセージの受信ハンドラを実装します。
今回はコンソールアプリケーションなので、メッセージを受信したらコンソールにメッセージを出力するようにします。
// メッセージ受信イベントハンドラ登録 Messenger.OnMessageReceived += (message) => { // コンソールにメッセージを出力 Console.WriteLine(message); };
インスタンス生成
エアコンモデルおよびエアコンステートマシンのインスタンスを生成します。
// エアコンモデルのインスタンス生成 var model = new AirConditioner(); // エアコンステートマシンのインスタンス生成 var stm = new ModelStateMachine(model);
ループ処理
ステートマシンへのトリガ送信や状態遷移を行うためのループ処理を定義します。
ユーザから終了のリクエストがあるまで、ループし続けるようにします。
// exitフラグ初期化 var exit = false; // exitフラグがtrueになるまでループ while(exit == false) { }
ステートマシン定常処理
ループ処理内で、ステートマシンの更新処理(Update)を呼び出します。
更新処理内では、ステートマシンの「現在の状態」における「Doイベント」処理が実行されます(定義されていれば)。
// ステートマシンの更新(各状態のDoイベント処理)
stm.Update();
状態表示
ループ処理内で、モデルの現在の状態(温度 / 湿度など)を表示します。
Programクラス内にPrintメソッドを実装し、ループ処理内で呼び出すようにします。
// モデルの状態表示
Print(model);
Printメソッドでは、エアコンの目標温度、現在温度、現在湿度をコンソールに出力します。
static void Print(AirConditioner model) { // 目標温度 Console.Write($"Target Temp : {model.TargetTemperature}[deg] | "); // 現在温度 Console.Write($"Temp : {model.Temperature}[deg] | "); // 現在湿度 Console.Write($"Humidity : {model.Humidity}[%]"); // 改行 Console.WriteLine(); }
ユーザ入力
エアコンを動かすために、ユーザからの入力を受け付けます。 今回は、ユーザにコンソールからコマンド文字列を入力させるさせるようにします。
// 入力プロンプト出力 Console.Write(">"); // コマンド入力 var command = Console.ReadLine();
コマンド制御
ユーザから入力されたコマンドに応じて、エアコンステートマシンやエアコンモデルに対する要求を送信します。
また、ユーザから"exit"を要求されたらループを抜けてアプリケーションを終了させます。
コマンド | 制御 | 備考 |
---|---|---|
start | ステートマシンにSwitchStartトリガ送信 | |
stop | ステートマシンにSwitchStopトリガ送信 | |
cool | ステートマシンにSwitchCoolトリガ送信 | |
heat | ステートマシンにSwitchHeatトリガ送信 | |
dry | ステートマシンにSwitchDryトリガ送信 | |
clean | ステートマシンにSwitchCleanトリガ送信 | |
up | エアコンモデルに目標温度アップ要求送信 | |
down | エアコンモデルに目標温度ダウン要求送信 | |
exit | exitフラグセット(アプリケーション終了要求) | |
それ以外 | 何もしない |
switch (command) { case "start": // ステートマシンにSwitchStartトリガ送信 stm.SendTrigger(SwitchStartTrigger.Instance); break; case "stop": // ステートマシンにSwitchStopトリガ送信 stm.SendTrigger(SwitchStopTrigger.Instance); break; case "cool": // ステートマシンにSwitchCoolトリガ送信 stm.SendTrigger(SwitchCoolTrigger.Instance); break; case "heat": // ステートマシンにSwitchHeatトリガ送信 stm.SendTrigger(SwitchHeatTrigger.Instance); break; case "dry": // ステートマシンにSwitchDryトリガ送信 stm.SendTrigger(SwitchDryTrigger.Instance); break; case "clean": // ステートマシンにSwitchDryトリガ送信 stm.SendTrigger(SwitchCleanTrigger.Instance); break; case "up": // エアコンモデルに目標温度アップ要求送信 model.Up(); break; case "down": // エアコンモデルに目標温度ダウン要求送信 model.Down(); break; case "exit": // exitフラグセット(アプリケーション終了要求) exit = true; break; default: // 何もしない break; }
全体
ソース全体を示します。
class Program { static void Main(string[] args) { Messenger.OnMessageReceived += (message) => { Console.WriteLine(message); }; var model = new AirConditioner(); var stm = new ModelStateMachine(model); var exit = false; while(exit == false) { stm.Update(); Print(model); Console.Write(">"); var command = Console.ReadLine(); switch (command) { case "start": stm.SendTrigger(SwitchStartTrigger.Instance); break; case "stop": stm.SendTrigger(SwitchStopTrigger.Instance); break; case "cool": stm.SendTrigger(SwitchCoolTrigger.Instance); break; case "heat": stm.SendTrigger(SwitchHeatTrigger.Instance); break; case "dry": stm.SendTrigger(SwitchDryTrigger.Instance); break; case "clean": stm.SendTrigger(SwitchCleanTrigger.Instance); break; case "up": model.Up(); break; case "down": model.Down(); break; case "exit": exit = true; break; default: break; } } } static void Print(AirConditioner model) { Console.Write($"Target Temp : {model.TargetTemperature}[deg] | "); Console.Write($"Temp : {model.Temperature}[deg] | "); Console.Write($"Humidity : {model.Humidity}[%]"); Console.WriteLine(); } }
次回予告
次回は、今回作成したアプリケーションのGUI版を実装していきます。