An Embedded Engineer’s Blog

とある組み込みエンジニアの備忘録的なブログです。

UMLのステートマシン図を実装する for C# - その6

まえがき

ステートマシンをCUI(コマンドライン)で動作させるアプリケーションを実装していきます。

f:id:an-embedded-engineer:20190414174321p:plain
エアコンステートマシン


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版を実装していきます。