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

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

まえがき

今回は、エアコンモデル(AirConditionerクラス)を実装していきます。
エアコンモデルは、今回のテーマである「ステートマシン図の実装」からは離れてしまうので、それっぽく動作するものに留めておきます。

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


StainLevel 列挙体

StainLevel列挙体は、汚れレベル解析処理結果を表し、レベルに応じてクリーニングモードを切り替えるために使用します。

public enum StainLevel
{
    // 汚れレベル未確定
    Unknown,
    // 汚れレベル低
    Low,
    // 汚れレベル高
    High,
}


AirConditioner クラス

エアコンモデルのAirConditionerクラスを実装していきます。

定数定義

定数として、目標温度や湿度の最大/最小値を定義します。

// 最大目標温度[℃]
public const int MaxTargetTemperature = 40;

// 最小目標温度[℃]
public const int MinTargetTemperature = 15;

// 最大湿度[%]
public const int MaxHumidity = 90;

// 最小湿度[%]
public const int MinHumidity = 20;


プロパティ定義

プロパティとして、エアコンの各種状態値(温度、湿度、汚れレベルなど)を定義します。

// 現在温度[℃]
public int Temperature { get; private set; }

// 目標温度[℃]
public int TargetTemperature { get; private set; }

// 湿度[%]
public int Humidity { get; private set; }

// 汚れレベル
public StainLevel StainLevel { get; private set; }

// 前回の汚れレベル
private StainLevel PrevStainLevel { get; set; }

// 汚れレベル解析時間カウント
private int AnalyseCount { get; set; }

// クリーニング時間カウント
private int CleanCount { get; set; }

// 汚れレベル解析完了フラグ : 汚れレベルがUnknown以外でtrue
private bool AnalyseFinished => (this.StainLevel != StainLevel.Unknown);

// クリーニング完了フラグ : 汚れレベルがHighでクリーニング時間20カウント以上 or 汚れレベルがLowでクリーニング時間10カウント以上
private bool CleanFinished => ((this.StainLevel == StainLevel.High && this.CleanCount >= 20) 
                            || (this.StainLevel == StainLevel.Low && this.CleanCount >= 10));


初期化処理

初期化処理では、各種プロパティに初期値をセットします。
今回はサンプルのため、設定値は適当です。
実際には、初期値設定以外にも各種ハードウェアの初期化処理などが実行されます。

public void Initialize()
{
    // 初期温度 = 30[℃]
    this.Temperature = 30;

    // 初期目標温度 = 最低目標温度[℃]
    this.TargetTemperature = AirConditioner.MinTargetTemperature;

    // 初期湿度 = 50[%]
    this.Humidity = 50;

    // 初期汚れレベル = Unknown
    this.StainLevel = StainLevel.Unknown;

    // 初期前回汚れレベル = Unknown
    this.PrevStainLevel = StainLevel.Unknown;

    // 汚れレベル解析時間カウントクリア
    this.AnalyseCount = 0;

    // クリーニング時間カウントクリア
    this.CleanCount = 0;
}


動作開始処理

動作開始処理では、エアコンの温度/湿度制御の開始処理を行います。
今回はサンプルのため、現在温度と湿度を初期値にリセットしています。

public void Start()
{
    // 現在温度 = 30[℃]
    this.Temperature = 30;

    // 現在湿度 = 50[%]
    this.Humidity = 50;
}


動作停止処理

動作開始処理では、エアコンの温度/湿度制御やクリーニングの停止処理を行います。
今回はサンプルのため、各種クリーニング状態を初期値にリセットしています。

public void Stop()
{
    // 汚れレベルリセット
    this.StainLevel = StainLevel.Unknown;

    // 前回汚れレベルリセット
    this.PrevStainLevel = StainLevel.Unknown;

    // 汚れレベル解析時間カウントクリア
    this.AnalyseCount = 0;

    // クリーニング時間カウントクリア
    this.CleanCount = 0;
}


目標温度設定処理

エアコンの目標温度は、ユーザがアップボタン / ダウンボタンを押下することによって上下させる事ができるようにします。
アップボタン / ダウンボタンそれぞれのインタフェースとして、それぞれUpメソッド、Downメソッドを用意しています。

public void Up()
{
    // 現在の目標温度が最大目標温度より小さい
    if (this.TargetTemperature < AirConditioner.MaxTargetTemperature)
    {
        // 目標温度をインクリメント
        this.TargetTemperature++;
    }
}

public void Down()
{
    // 現在の目標温度が最小目標温度より大きい
    if (this.TargetTemperature > AirConditioner.MinTargetTemperature)
    {
        // 目標温度をデクリメント
        this.TargetTemperature--;
    }
}


温度/湿度制御処理

エアコンが動作状態となっている時の、動作モードに応じた温度/湿度制御処理を実装します。
今回はサンプルのため、単純な温度や湿度の上下とリミット処理のみを実装します。
実際には、センサから取得した現在温度と、設定された目標温度からフィードバック制御などを行います。

public void CoolControl()
{
    // 目標温度が現在温度より低い
    if (this.TargetTemperature < this.Temperature)
    {
        // 現在温度をデクリメント
        this.Temperature--;
    }
}

public void HeatControl()
{
    // 目標温度が現在温度より高い
    if (this.TargetTemperature > this.Temperature)
    {
        // 現在温度をインクリメント
        this.Temperature++;
    }
}

public void DryControl()
{
    // 現在湿度が最小湿度より大きい
    if (this.Humidity > AirConditioner.MinHumidity)
    {
        // 現在湿度をデクリメント
        this.Humidity--;
    }
}


汚れレベル解析処理

クリーニング開始前の、汚れレベル解析処理を実装します。
今回はサンプルのため、時間経過と前回の解析結果に応じて汚れレベルを変化させます。
実際には、センサなどを使用して内部の汚れ具合を判定し、それに応じた汚れレベルを返すようにします。

public StainLevel StainLevelAnalys()
{
    // 汚れレベル解析時間カウントアップ
    this.AnalyseCount++;

    // 解析時間が5カウント以上(メソッドが5回以上呼び出された)
    if (this.AnalyseCount >= 5)
    {
        // 前回の汚れレベル解析結果
        switch (this.PrevStainLevel)
        {
            case StainLevel.Unknown:    // 不明(初回解析)
                // 汚れレベル = 低
                this.StainLevel = StainLevel.Low;
                break;
            case StainLevel.Low:        // 汚れレベル低
                // 汚れレベル = 高
                this.StainLevel = StainLevel.High;
                break;
            case StainLevel.High:       // 汚れレベル高
                // 汚れレベル = 低
                this.StainLevel = StainLevel.Low;
                break;
            default:
                break;
        }
    }
    else
    {
        // 汚れレベル = 未確定
        this.StainLevel = StainLevel.Unknown;
    }

    return this.StainLevel;
}


クリーニング処理

解析した汚れレベルに応じたクリーニング処理を行います。
今回はサンプルのため、単純な時間経過でクリーニング処理完了判定を行います。

public bool DeepCleanControl()
{
    // クリーニング時間カウントアップ
    this.CleanCount++;

    // クリーニング完了判定結果を返す
    return this.CleanFinished;
}

public bool LightCleanControl()
{
    // クリーニング時間カウントアップ
    this.CleanCount++;

    // クリーニング完了判定結果を返す
    return this.CleanFinished;
}


クリーニング完了処理

クリーニング処理が完了したら、その後始末処理を行います。 今回はサンプルのため、汚れレベル解析結果のバックアップと、各クリーニング状態のクリアを行います。

public void CleanEnd()
{
    // 汚れレベルを前回値としてバックアップ
    this.PrevStainLevel = this.StainLevel;

    // 汚れレベル解析結果クリア
    this.StainLevel = StainLevel.Unknown;

    // 汚れレベル解析時間カウントクリア
    this.AnalyseCount = 0;

    // クリーニング時間カウントクリア
    this.CleanCount = 0;
}


ソース全体を示します。

public class AirConditioner
{
    // 最大目標温度[℃]
    public const int MaxTargetTemperature = 40;

    // 最小目標温度[℃]
    public const int MinTargetTemperature = 15;

    // 最大湿度[%]
    public const int MaxHumidity = 90;

    // 最小湿度[%]
    public const int MinHumidity = 20;

    // 現在温度[℃]
    public int Temperature { get; private set; }

    // 目標温度[℃]
    public int TargetTemperature { get; private set; }

    // 現在湿度
    public int Humidity { get; private set; }

    // 汚れレベル
    public StainLevel StainLevel { get; private set; }

    // 前回の汚れレベル
    private StainLevel PrevStainLevel { get; set; }

    // 汚れレベル解析時間カウント
    private int AnalyseCount { get; set; }

    // クリーニング時間カウント
    private int CleanCount { get; set; }

    // 汚れレベル解析完了フラグ
    private bool AnalyseFinished => (this.StainLevel != StainLevel.Unknown);

    // クリーニング完了フラグ
    private bool CleanFinished => ((this.StainLevel == StainLevel.High && this.CleanCount >= 20) 
                                || (this.StainLevel == StainLevel.Low && this.CleanCount >= 10));

    // 初期化処理
    public void Initialize()
    {
        this.Temperature = 30;

        this.TargetTemperature = AirConditioner.MinTargetTemperature;

        this.Humidity = 50;

        this.StainLevel = StainLevel.Unknown;

        this.PrevStainLevel = StainLevel.Unknown;

        this.AnalyseCount = 0;

        this.CleanCount = 0;
    }

    // 動作開始処理
    public void Start()
    {
        this.Temperature = 30;

        this.Humidity = 50;
    }

    // 動作停止処理
    public void Stop()
    {
        this.StainLevel = StainLevel.Unknown;

        this.PrevStainLevel = StainLevel.Unknown;

        this.AnalyseCount = 0;

        this.CleanCount = 0;
    }

    // 目標温度アップ
    public void Up()
    {
        if (this.TargetTemperature < AirConditioner.MaxTargetTemperature)
        {
            this.TargetTemperature++;
        }
    }

    // 目標温度ダウン
    public void Down()
    {
        if (this.TargetTemperature > AirConditioner.MinTargetTemperature)
        {
            this.TargetTemperature--;
        }
    }

    // 冷房制御処理
    public void CoolControl()
    {
        if (this.TargetTemperature < this.Temperature)
        {
            this.Temperature--;
        }
    }

    // 暖房制御処理
    public void HeatControl()
    {
        if (this.TargetTemperature > this.Temperature)
        {
            this.Temperature++;
        }
    }

    // 除湿制御処理
    public void DryControl()
    {
        if (this.Humidity > AirConditioner.MinHumidity)
        {
            this.Humidity--;
        }
    }

    // 汚れレベル解析処理
    public StainLevel StainLevelAnalys()
    {
        this.AnalyseCount++;

        if (this.AnalyseCount >= 5)
        {
            switch (this.PrevStainLevel)
            {
                case StainLevel.Unknown:
                    this.StainLevel = StainLevel.Low;
                    break;
                case StainLevel.Low:
                    this.StainLevel = StainLevel.High;
                    break;
                case StainLevel.High:
                    this.StainLevel = StainLevel.Low;
                    break;
                default:
                    break;
            }
        }
        else
        {
            this.StainLevel = StainLevel.Unknown;
        }

        return this.StainLevel;
    }

    // 入念クリーニング処理
    public bool DeepCleanControl()
    {
        this.CleanCount++;

        return this.CleanFinished;
    }

    // あっさりクリーニング処理
    public bool LightCleanControl()
    {
        this.CleanCount++;

        return this.CleanFinished;
    }

    // クリーニング完了処理
    public void CleanEnd()
    {
        this.PrevStainLevel = this.StainLevel;

        this.StainLevel = StainLevel.Unknown;

        this.AnalyseCount = 0;

        this.CleanCount = 0;
    }
}


次回予告

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

an-embedded-engineer.hateblo.jp

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

まえがき

今回は、前回に引き続きサンプルのエアコンステートマシンを実装していきます。

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


State クラス

State クラスを継承した各状態を実装します。
エアコンステートマシンでは、4 つのメイン状態と、6つのサブ状態を実装する必要があります。

ステートマシン 名称 詳細 備考
ModelStateMachine InitialState 初期状態
ModelStateMachine StopState 停止状態
ModelStateMachine RunningState 動作状態
ModelStateMachine CleanState クリーニング状態
RunningStateMachine CoolState 冷房制御状態
RunningStateMachine HeatState 暖房制御状態
RunningStateMachine DryState 除湿制御状態
CleanStateMachine StainLvelAnalysisState 汚れレベル解析状態
CleanStateMachine DeepCleanState 入念クリーニング状態
CleanStateMachine LightCleanState あっさりクリーニング状態
CleanStateMachine CleanFinalState クリーニング完了状態


各状態はシステム内で 1 つのインスタンスのみを持つべきなので、シングルトンで実装します。


InitialState クラス

InitialState クラスでは、Entry イベントハンドラでモデル(AirConditioner)の初期化処理を行います。
初期化完了後、Initialize トリガをステートマシンに送信します。

private void EntryEventHandler(StateMachine context)
{
    // StateMachineをダウンキャスト
    var stm = context.GetAs<ModelStateMachine>();

    // モデル(AirConditionerクラス)を取得
    var model = stm.Model;

    // モデルの初期化処理
    model.Initialize();

    // Initialiedトリガ送信
    stm.SendTrigger(InitializedTrigger.Instance);
}


また、トリガアクションハッシュテーブルに、Initialized トリガに対するアクション(InitializedTriggerHandler)を登録しておきます。

protected override TriggerActionMap GenerateTriggerActionMap()
{
    return new TriggerActionMap()
    {
        // Initializedトリガに対するアクションの登録
        { InitializedTrigger.Instance.Name, this.InitializedTriggerHandler },
    };
}


InitializedTriggerHandler では、コンテキスト(ステートマシン)に対して Stop 状態への状態遷移を要求します。

private void InitializedTriggerHandler(TriggerActionArgs args)
{
    // コンテキスト(ステートマシン)の取得
    var context = args.Context;

    // Stop状態への状態遷移を要求
    context.ChangeState(StopState.Instance);
}


ソース全体を示します。

public sealed class InitialState : State
{
    // シングルトンインスタンス
    public static InitialState Instance { get; private set; } = new InitialState();

    // コンストラクタ
    private InitialState() : base("Initial")
    {
        this.OnEntry += this.EntryEventHandler;
    }

    // トリガアクションハッシュテーブル生成
    protected override TriggerActionMap GenerateTriggerActionMap()
    {
        return new TriggerActionMap()
        {
            { InitializedTrigger.Instance.Name, this.InitializedTriggerHandler },
        };
    }

    // Entryイベントハンドラ
    private void EntryEventHandler(StateMachine context)
    {
        var stm = context.GetAs<ModelStateMachine>();

        var model = stm.Model;

        model.Initialize();

        stm.SendTrigger(InitializedTrigger.Instance);
    }

    // Initializedトリガハンドラ
    private void InitializedTriggerHandler(TriggerActionArgs args)
    {
        var context = args.Context;

        context.ChangeState(StopState.Instance);
    }
}


StopState クラス

StopState クラスでは、SwitchStart トリガに対して、RunningState への状態遷移を要求する処理を実装します。

protected override TriggerActionMap GenerateTriggerActionMap()
{
    return new TriggerActionMap()
    {
        // SwitchStartトリガに対するアクション
        { SwitchStartTrigger.Instance.Name, this.SwitchStartTriggerHandler },
    };
}

private void SwitchStartTriggerHandler(TriggerActionArgs args)
{
    // コンテキスト(ステートマシン)の取得
    var context = args.Context;

    // Running状態への状態遷移を要求
    context.ChangeState(RunningState.Instance);
}


ソース全体を示します。

public sealed class StopState : State
{
    // シングルトンインスタンス
    public static StopState Instance { get; private set; } = new StopState();

    // コンストラクタ
    private StopState() : base("Stop")
    {
    }

    // トリガアクションハッシュテーブル生成
    protected override TriggerActionMap GenerateTriggerActionMap()
    {
        return new TriggerActionMap()
        {
            { SwitchStartTrigger.Instance.Name, this.SwitchStartTriggerHandler },
        };
    }

    // SwitchStartトリガハンドラ
    private void SwitchStartTriggerHandler(TriggerActionArgs args)
    {
        var context = args.Context;

        context.ChangeState(RunningState.Instance);
    }
}


RunningState クラス

RunningState クラスでは、サブステートマシンとして RunningStateMachine を持つ必要があります。

// サブステートマシン
public RunningStateMachine SubContext { get; private set; }


RunningStateMachine は、最初に Running 状態に入った(Entry イベント)時のみインスタンスが生成されます。
Hisoty 疑似状態を実現するために、Running 状態を出た際に最後のサブ状態を記憶しておく必要があるためです。

private void EntryEventHandler(StateMachine context)
{
    // 初回入場時(サブステートマシンのインスタンスが生成されていない)
    if (this.SubContext == null)
    {
        // 親ステートマシン(ModelStateMachine)の取得
        var parent = context.GetAs<ModelStateMachine>();

        // サブステートマシン(RunningStateMachine)のインスタンスを生成
        this.SubContext = new RunningStateMachine(parent);
    }
}


また、サブステートマシンの定常的な処理を実行するため、Do イベントハンドラで、サブステートマシンの Update 処理を実行します。

private void DoEventHandler(StateMachine context)
{
    // サブステートマシンの更新
    this.SubContext.Update();
}


RunningStete クラスでは、Running 状態そのものに対するトリガと、サブ状態に対するトリガを受け取れるようにする必要があります。

protected override TriggerActionMap GenerateTriggerActionMap()
{
    return new TriggerActionMap()
    {
        // SwitchStopトリガに対するアクション
        { SwitchStopTrigger.Instance.Name, this.SwitchStopTriggerHandler },
        // SwitchCleanトリガに対するアクション
        { SwitchCleanTrigger.Instance.Name, this.SwitchCleanTriggerHandler },
        // SwitchCoolトリガに対するアクション
        { SwitchCoolTrigger.Instance.Name, this.SubContextTriggerHandler },
        // SwitchHeatトリガに対するアクション
        { SwitchHeatTrigger.Instance.Name, this.SubContextTriggerHandler },
        // SwitchDryトリガに対するアクション
        { SwitchDryTrigger.Instance.Name, this.SubContextTriggerHandler },
    };
}


SwitchStop トリガを受信した場合は、Stop 状態への遷移を要求し、その際に Trigger に設定された Effect をパラメータとして受け渡します。
SwitchClean トリガを受信した場合は、Clean 状態への遷移を要求します。

private void SwitchStopTriggerHandler(TriggerActionArgs args)
{
    // コンテキスト(ステートマシン)の取得
    var context = args.Context;

    // 状態遷移時に実行するEffectを取得
    var effect = args.Trigger.Effect;

    // Stop状態への状態遷移を要求
    context.ChangeState(StopState.Instance, effect);
}

private void SwitchCleanTriggerHandler(TriggerActionArgs args)
{
    // コンテキスト(ステートマシン)の取得
    var context = args.Context;

    // Clean状態への状態遷移を要求
    context.ChangeState(CleanState.Instance);
}


サブステートマシン用のトリガ(SwitchCool / SwitchHeat / SwitchDry)を受信した場合は、サブステートマシンに受信したトリガを送信します。

private void SubContextTriggerHandler(TriggerActionArgs args)
{
    // 受信したトリガを取得
    var trigger = args.Trigger;

    // コンテキスト(サブステートマシン)を取得
    var context = this.SubContext;

    // サブステートマシンにトリガを送信
    context.SendTrigger(trigger);
}


ソース全体を示します。

public sealed class RunningState : State
{
    // シングルトンインスタンス
    public static RunningState Instance { get; private set; } = new RunningState();

    // サブステートマシン
    public RunningStateMachine SubContext { get; private set; }

    // コンストラクタ
    private RunningState() : base("Running")
    {
        this.OnEntry += this.EntryEventHandler;
        this.OnDo += this.DoEventHandler;
    }

    // トリガアクションハッシュテーブル生成
    protected override TriggerActionMap GenerateTriggerActionMap()
    {
        return new TriggerActionMap()
        {
            { SwitchStopTrigger.Instance.Name, this.SwitchStopTriggerHandler },
            { SwitchCleanTrigger.Instance.Name, this.SwitchCleanTriggerHandler },
            { SwitchCoolTrigger.Instance.Name, this.SubContextTriggerHandler },
            { SwitchHeatTrigger.Instance.Name, this.SubContextTriggerHandler },
            { SwitchDryTrigger.Instance.Name, this.SubContextTriggerHandler },
        };
    }

    // Entryイベントハンドラ
    private void EntryEventHandler(StateMachine context)
    {
        if (this.SubContext == null)
        {
            var parent = context.GetAs<ModelStateMachine>();

            this.SubContext = new RunningStateMachine(parent);
        }
    }

    // Doイベントハンドラ
    private void DoEventHandler(StateMachine context)
    {
        this.SubContext.Update();
    }

    // SwitchStopトリガハンドラ
    private void SwitchStopTriggerHandler(TriggerActionArgs args)
    {
        var context = args.Context;

        var effect = args.Trigger.Effect;

        context.ChangeState(StopState.Instance, effect);
    }

    // SwitchCleanトリガハンドラ
    private void SwitchCleanTriggerHandler(TriggerActionArgs args)
    {
        var context = args.Context;

        context.ChangeState(CleanState.Instance);
    }

    // サブステートマシン用トリガハンドラ
    private void SubContextTriggerHandler(TriggerActionArgs args)
    {
        var trigger = args.Trigger;

        var context = this.SubContext;

        context.SendTrigger(trigger);
    }
}


CoolState クラス

CoolState クラスでは、Do イベントハンドラで冷房制御を行うため、モデル(AirConditioner)の冷房制御処理(CoolControl)を呼び出します。

private void DoEventHandler(StateMachine context)
{
    // 親ステートマシン(RunningStateMachine)の取得
    var stm = context.GetAs<RunningStateMachine>();

    // モデル(AirConditioner)の取得
    var model = stm.Model;

    // 冷房制御処理呼び出し
    model.CoolControl();
}


CoolState クラスでは、動作モード(暖房 / 除湿)切り替えスイッチ押下によるトリガを受信できるようにしておきます。

protected override TriggerActionMap GenerateTriggerActionMap()
{
    return new TriggerActionMap()
    {
        // SwitchHeatトリガに対するアクション
        { SwitchHeatTrigger.Instance.Name, this.SwitchHeatTriggerHandler },
        // SwitchDryトリガに対するアクション
        { SwitchDryTrigger.Instance.Name, this.SwitchDryTriggerHandler },
    };
}


SwitchHeat トリガを受信した場合は、Heat 状態への遷移を要求します。
SwitchDry トリガを受信した場合は、Dry 状態への遷移を要求します。

private void SwitchHeatTriggerHandler(TriggerActionArgs args)
{
    // コンテキスト(ステートマシン)の取得
    var context = args.Context;

    // Heat状態への状態遷移を要求
    context.ChangeState(HeatState.Instance);
}

private void SwitchDryTriggerHandler(TriggerActionArgs args)
{
    // コンテキスト(ステートマシン)の取得
    var context = args.Context;

    // Dry状態への状態遷移を要求
    context.ChangeState(DryState.Instance);
}


ソース全体を示します。

public sealed class CoolState : State
{
    // シングルトンインスタンス
    public static CoolState Instance { get; private set; } = new CoolState();

    // コンストラクタ
    private CoolState() : base("Cool")
    {
        this.OnDo += this.DoEventHandler;
    }

    // トリガアクションハッシュテーブル生成
    protected override TriggerActionMap GenerateTriggerActionMap()
    {
        return new TriggerActionMap()
        {
            { SwitchHeatTrigger.Instance.Name, this.SwitchHeatTriggerHandler },
            { SwitchDryTrigger.Instance.Name, this.SwitchDryTriggerHandler },
        };
    }

    // Doイベントハンドラ
    private void DoEventHandler(StateMachine context)
    {
        var stm = context.GetAs<RunningStateMachine>();

        var model = stm.Model;

        model.CoolControl();
    }

    // SwitchHeatトリガハンドラ
    private void SwitchHeatTriggerHandler(TriggerActionArgs args)
    {
        var context = args.Context;

        context.ChangeState(HeatState.Instance);
    }

    // SwitchDryトリガハンドラ
    private void SwitchDryTriggerHandler(TriggerActionArgs args)
    {
        var context = args.Context;

        context.ChangeState(DryState.Instance);
    }
}


HeatState クラス

HeatState クラスでは、Do イベントハンドラで暖房制御を行うため、モデル(AirConditioner)の暖房制御処理(HeatControl)を呼び出します。

private void DoEventHandler(StateMachine context)
{
    // 親ステートマシン(RunningStateMachine)の取得
    var stm = context.GetAs<RunningStateMachine>();

    // モデル(AirConditioner)の取得
    var model = stm.Model;

    // 暖房制御処理呼び出し
    model.HeatControl();
}


HeatState クラスでは、動作モード(冷房 / 除湿)切り替えスイッチ押下によるトリガを受信できるようにしておきます。

protected override TriggerActionMap GenerateTriggerActionMap()
{
    return new TriggerActionMap()
    {
        // SwitchCoolトリガに対するアクション
        { SwitchCoolTrigger.Instance.Name, this.SwitchCoolTriggerHandler },
        // SwitchDryトリガに対するアクション
        { SwitchDryTrigger.Instance.Name, this.SwitchDryTriggerHandler },
    };
}


SwitchCool トリガを受信した場合は、Cool 状態への遷移を要求します。
SwitchDry トリガを受信した場合は、Dry 状態への遷移を要求します。

private void SwitchCoolTriggerHandler(TriggerActionArgs args)
{
    // コンテキスト(ステートマシン)の取得
    var context = args.Context;

    // Cool状態への状態遷移を要求
    context.ChangeState(CoolState.Instance);
}

private void SwitchDryTriggerHandler(TriggerActionArgs args)
{
    // コンテキスト(ステートマシン)の取得
    var context = args.Context;

    // Dry状態への状態遷移を要求
    context.ChangeState(DryState.Instance);
}


ソース全体を示します。

public sealed class HeatState : State
{
    // シングルトンインスタンス
    public static HeatState Instance { get; private set; } = new HeatState();

    // コンストラクタ
    private HeatState() : base("Heat")
    {
        this.OnDo += this.DoEventHandler;
    }

    // トリガアクションハッシュテーブル生成
    protected override TriggerActionMap GenerateTriggerActionMap()
    {
        return new TriggerActionMap()
        {
            { SwitchCoolTrigger.Instance.Name, this.SwitchCoolTriggerHandler },
            { SwitchDryTrigger.Instance.Name, this.SwitchDryTriggerHandler },
        };
    }

    // Doイベントハンドラ
    private void DoEventHandler(StateMachine context)
    {
        var stm = context.GetAs<RunningStateMachine>();

        var model = stm.Model;

        model.HeatControl();
    }

    // SwitchCoolトリガハンドラ
    private void SwitchCoolTriggerHandler(TriggerActionArgs args)
    {
        var context = args.Context;

        context.ChangeState(CoolState.Instance);
    }

    // SwitchDryトリガハンドラ
    private void SwitchDryTriggerHandler(TriggerActionArgs args)
    {
        var context = args.Context;

        context.ChangeState(DryState.Instance);
    }
}


DryState クラス

DryState クラスでは、Do イベントハンドラで除湿制御を行うため、モデル(AirConditioner)の除湿制御処理(DryControl)を呼び出します。

private void DoEventHandler(StateMachine context)
{
    // 親ステートマシン(RunningStateMachine)の取得
    var stm = context.GetAs<RunningStateMachine>();

    // モデル(AirConditioner)の取得
    var model = stm.Model;

    // 除湿制御処理呼び出し
    model.DryControl();
}


DryState クラスでは、動作モード(冷房 / 暖房)切り替えスイッチ押下によるトリガを受信できるようにしておきます。

protected override TriggerActionMap GenerateTriggerActionMap()
{
    return new TriggerActionMap()
    {
        // SwitchCoolトリガに対するアクション
        { SwitchCoolTrigger.Instance.Name, this.SwitchCoolTriggerHandler },
        // SwitchHeatトリガに対するアクション
        { SwitchHeatTrigger.Instance.Name, this.SwitchHeatTriggerHandler },
    };
}


SwitchCool トリガを受信した場合は、Cool 状態への遷移を要求します。
SwitchHeat トリガを受信した場合は、Heat 状態への遷移を要求します。

private void SwitchCoolTriggerHandler(TriggerActionArgs args)
{
    // コンテキスト(ステートマシン)の取得
    var context = args.Context;

    // Cool状態への状態遷移を要求
    context.ChangeState(CoolState.Instance);
}

private void SwitchHeatTriggerHandler(TriggerActionArgs args)
{
    // コンテキスト(ステートマシン)の取得
    var context = args.Context;

    // Heat状態への状態遷移を要求
    context.ChangeState(HeatState.Instance);
}


ソース全体を示します。

public sealed class DryState : State
{
    // シングルトンインスタンス
    public static DryState Instance { get; private set; } = new DryState();

    // コンストラクタ
    private DryState() : base("Dry")
    {
        this.OnDo += this.DoEventHandler;
    }

    // トリガアクションハッシュテーブル生成
    protected override TriggerActionMap GenerateTriggerActionMap()
    {
        return new TriggerActionMap()
        {
            { SwitchCoolTrigger.Instance.Name, this.SwitchCoolTriggerHandler },
            { SwitchHeatTrigger.Instance.Name, this.SwitchHeatTriggerHandler },
        };
    }

    // Doイベントハンドラ
    private void DoEventHandler(StateMachine context)
    {
        var stm = context.GetAs<RunningStateMachine>();

        var model = stm.Model;

        model.DryControl();
    }

    // SwitchCoolトリガハンドラ
    private void SwitchCoolTriggerHandler(TriggerActionArgs args)
    {
        var context = args.Context;

        context.ChangeState(CoolState.Instance);
    }

    // SwitchHeatトリガハンドラ
    private void SwitchHeatTriggerHandler(TriggerActionArgs args)
    {
        var context = args.Context;

        context.ChangeState(HeatState.Instance);
    }
}


CleanState クラス

CleanState クラスでは、サブステートマシンとしてCleanStateMachineを持つ必要があります。

// サブステートマシン
public CleanStateMachine SubContext { get; private set; }


CleanStateMachineは、Clena状態に入る(Entryイベント)たびにインスタンスを生成します。
クリーニングモードに入るたびに汚れレベルの解析からやり直すため、Clean状態に入るたび、サブステートマシンの状態を初期状態にリセットする必要があるためです。

private void EntryEventHandler(StateMachine context)
{
    // 親ステートマシン(Model StateMachine)の取得
    var parent = context.GetAs<ModelStateMachine>();

    // サブステートマシン(CleanStateMachine)の生成
    this.SubContext = new CleanStateMachine(parent);
}


また、サブステートマシンの定常的な処理を実行するため、Do イベントハンドラで、サブステートマシンの Update 処理を実行します。
さらに、サブステートマシンが完了状態(CleanFinalState)になっていた場合、クリーニング処理が完了となるため、ステートマシンにRunning状態への状態遷移を要求します。
このとき、エフェクトとしてCleanEndEffectを指定します。

private void DoEventHandler(StateMachine context)
{
    // サブステートマシンの更新
    this.SubContext.Update();

    // サブステートマシンの状態がクリーニング完了状態(CleanFinalState)
    if (this.SubContext.CurrentState is CleanFinalState)
    {
        // 完了遷移で実行するエフェクト(CleanEndEffect)を取得
        var effect = CleanEndEffect.Instance;

        // Running状態への状態遷移を要求
        context.ChangeState(RunningState.Instance, effect);
    }
}


CleanState クラスでは、ストップスイッチ押下によるトリガを受信できるようにしておきます。

protected override TriggerActionMap GenerateTriggerActionMap()
{
    return new TriggerActionMap()
    {
        // SwitchStopトリガに対するアクション
        { SwitchStopTrigger.Instance.Name, this.SwitchStopTriggerHandler },
    };
}


SwitchStop トリガを受信した場合は、Stop 状態への遷移を要求し、その際に Trigger に設定された Effect をパラメータとして受け渡します。

private void SwitchStopTriggerHandler(TriggerActionArgs args)
{
    // コンテキスト(ステートマシン)の取得
    var context = args.Context;

    // 状態遷移時に実行するEffectを取得
    var effect = args.Trigger.Effect;

    // Stop状態への状態遷移を要求
    context.ChangeState(StopState.Instance, effect);
}


ソース全体を示します。

public sealed class CleanState : State
{
    // シングルトンインスタンス
    public static CleanState Instance { get; private set; } = new CleanState();

    // サブステートマシン
    public CleanStateMachine SubContext { get; private set; }

    // コンストラクタ
    private CleanState() : base("Clean")
    {
        this.OnEntry += this.EntryEventHandler;
        this.OnDo += this.DoEventHandler;
    }

    // トリガアクションハッシュテーブル生成
    protected override TriggerActionMap GenerateTriggerActionMap()
    {
        return new TriggerActionMap()
        {
            { SwitchStopTrigger.Instance.Name, this.SwitchStopTriggerHandler },
        };
    }

    // Entryイベントハンドラ
    private void EntryEventHandler(StateMachine context)
    {
        var parent = context.GetAs<ModelStateMachine>();

        this.SubContext = new CleanStateMachine(parent);
    }

    // Doイベントハンドラ
    private void DoEventHandler(StateMachine context)
    {
        this.SubContext.Update();

        if (this.SubContext.CurrentState is CleanFinalState)
        {
            var effect = CleanEndEffect.Instance;

            context.ChangeState(RunningState.Instance, effect);
        }
    }

    // SwitchStopトリガハンドラ
    private void SwitchStopTriggerHandler(TriggerActionArgs args)
    {
        var context = args.Context;

        var effect = args.Trigger.Effect;

        context.ChangeState(StopState.Instance, effect);
    }
}


StainLevelAnalysisState クラス

StainLevelAnalysisState クラスでは、Doイベントハンドラで汚れレベルを解析し、解析が完了したら汚れレベルに応じたクリーニングモード(LightClean / DeepClean)への遷移を要求します。

private void DoEventHandler(StateMachine context)
{
    // 親ステートマシン(CleanStateMachine)の取得
    var stm = context.GetAs<CleanStateMachine>();

    // モデル(AirConditioner)の取得
    var model = stm.Model;

    // 汚れレベル解析処理呼び出し
    var level = model.StainLevelAnalys();

    // 汚れレベル判定
    switch (level)
    {
        case StainLevel.Unknown:    // 未確定
            /* Nothing to do */
            break;
        case StainLevel.Low:    // 汚れレベル低
            // LightClean状態への状態遷移を要求
            stm.ChangeState(LightCleanState.Instance);
            break;
        case StainLevel.High:   // 汚れレベル高
            // DeepClean状態への状態遷移を要求
            stm.ChangeState(DeepCleanState.Instance);
            break;
        default:
            /* Nothing to do */
            break;
    }
}


ソース全体を示します。

public sealed class StainLevelAnalysisState : State
{
    // シングルトンインスタンス
    public static StainLevelAnalysisState Instance { get; private set; } = new StainLevelAnalysisState();

    // コンストラクタ
    private StainLevelAnalysisState() : base("Stain Level AnalysisState")
    {
        this.OnDo += this.DoEventHandler;
    }

    // トリガアクションハッシュテーブル生成
    protected override TriggerActionMap GenerateTriggerActionMap()
    {
        return new TriggerActionMap()
        {
        };
    }

    // Doイベントハンドラ
    private void DoEventHandler(StateMachine context)
    {
        var stm = context.GetAs<CleanStateMachine>();

        var model = stm.Model;

        var level = model.StainLevelAnalys();

        switch (level)
        {
            case StainLevel.Unknown:
                /* Nothing to do */
                break;
            case StainLevel.Low:
                stm.ChangeState(LightCleanState.Instance);
                break;
            case StainLevel.High:
                stm.ChangeState(DeepCleanState.Instance);
                break;
            default:
                /* Nothing to do */
                break;
        }
    }
}


DeepCleanState クラス

DeepCleanState クラスでは、Doイベントハンドラで入念クリーニング処理を実行し、クリーニングが完了したらクリーニング完了(CleanFinal)状態への遷移を要求します。

private void DoEventHandler(StateMachine context)
{
    // 親ステートマシン(CleanStateMachine)の取得
    var stm = context.GetAs<CleanStateMachine>();

    // モデル(AirConditioner)の取得
    var model = stm.Model;

    // 入念クリーニング制御処理呼び出し
    var result = model.DeepCleanControl();

    // クリーニング処理完了
    if (result == true)
    {
        // CleanFinal状態への状態遷移を要求
        stm.ChangeState(CleanFinalState.Instance);
    }
}


ソース全体を示します。

public sealed class DeepCleanState : State
{
    // シングルトンインスタンス
    public static DeepCleanState Instance { get; private set; } = new DeepCleanState();

    // コンストラクタ
    private DeepCleanState() : base("Deep Clean")
    {
        this.OnDo += this.DoEventHandler;
    }

    // トリガアクションハッシュテーブル生成
    protected override TriggerActionMap GenerateTriggerActionMap()
    {
        return new TriggerActionMap()
        {
        };
    }

    // Doイベントハンドラ
    private void DoEventHandler(StateMachine context)
    {
        var stm = context.GetAs<CleanStateMachine>();

        var model = stm.Model;

        var result = model.DeepCleanControl();

        if (result == true)
        {
            stm.ChangeState(CleanFinalState.Instance);
        }
    }
}


LightCleanState クラス

LightCleanState クラスでは、Doイベントハンドラであっさりクリーニング処理を実行し、クリーニングが完了したらクリーニング完了(CleanFinal)状態への遷移を要求します。

private void DoEventHandler(StateMachine context)
{
    // 親ステートマシン(CleanStateMachine)の取得
    var stm = context.GetAs<CleanStateMachine>();

    // モデル(AirConditioner)の取得

    // あっさりクリーニング制御処理呼び出し
    var result = model.LightCleanControl();

    // クリーニング処理完了
    if (result == true)
    {
        // CleanFinal状態への状態遷移を要求
        stm.ChangeState(CleanFinalState.Instance);
    }
}


ソース全体を示します。

public sealed class LightCleanState : State
{
    // シングルトンインスタンス
    public static LightCleanState Instance { get; private set; } = new LightCleanState();

    // コンストラクタ
    private LightCleanState() : base("Light Clean")
    {
        this.OnDo += this.DoEventHandler;
    }

    // トリガアクションハッシュテーブル生成
    protected override TriggerActionMap GenerateTriggerActionMap()
    {
        return new TriggerActionMap()
        {
        };
    }

    // Doイベントハンドラ
    private void DoEventHandler(StateMachine context)
    {
        var stm = context.GetAs<CleanStateMachine>();

        var model = stm.Model;

        var result = model.LightCleanControl();

        if (result == true)
        {
            stm.ChangeState(CleanFinalState.Instance);
        }
    }
}


CleanFinalState クラス

CleanFinalStateクラスは、CleanStateMachineの最終状態のため、特に何もしません。 ソース全体を示します。

public sealed class CleanFinalState : State
{
    public static CleanFinalState Instance { get; private set; } = new CleanFinalState();

    private CleanFinalState() : base("Clean Final")
    {
    }

    protected override TriggerActionMap GenerateTriggerActionMap()
    {
        return new TriggerActionMap()
        {
        };
    }
}


次回予告

今回で状態遷移部分はすべて実装できたので、次回はエアコンのモデル部分(AirConditionerクラス)の実装を行っていきます。

an-embedded-engineer.hateblo.jp

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

まえがき

今回は、前回説明したサンプルのエアコンステートマシンを実装していきます。

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


StateMachine クラス

StateMachine クラスを継承したステートマシンを実装します。
エアコンステートマシンでは、3 つのステートマシンを実装する必要があります。

名称 詳細 備考
ModelStateMachine エアコン全体のステートマシン
RunningStateMachine Running 状態のサブステートマシン
CleanStateMachine Clean 状態のサブステートマシン


ModelStateMachine クラス

ModelStateMachine クラスは、エアコンモデルの AirConditioner クラス(後述)を引数として受け取るコンストラクタを定義します。
コンストラクタ内では、各状態(State)からモデルの処理を呼び出し可能にするため、エアコンモデルを Public プロパティにセットします。
その後、StateMachine クラスで定義された ChangeToInitalState メソッドを呼び出すことで初期状態への遷移を開始します。
また、継承元の StateMachine クラスで宣言された抽象メソッドの GetInitialState を定義し、ステートマシンの初期状態を返すようにします。


public class ModelStateMachine : StateMachine
{
    // エアコンモデル
    public AirConditioner Model { get; }

    // コンストラクタ
    public ModelStateMachine(AirConditioner model)
    {
        this.Model = model;

        this.ChangeToInitialState();
    }

    // 初期状態取得
    protected override State GetInitialState()
    {
        return InitialState.Instance;
    }
}


RunningStateMachine クラス

ModelStateMachine クラスはサブステートマシンなので、親である ModelStateMachine クラスを引数として受け取るコンストラクタを定義します。
コンストラクタでは、受け取った ModelStateMachine クラス、およびそこから取得できるエアコンモデル(AirConditioner クラス)を Public プロパティにセットします。 あとは ModelStateMachine クラスと同様、ChangeToInitialState メソッドの呼び出しと、GetInitialState メソッドの定義を行います。
RunningStateMachine の初期状態は、前回説明したとおり、Cool 状態となります。 (システム初期化後初めて Running 状態に遷移した場合は、履歴が残されていないため、Cool 状態に遷移する)

public class RunningStateMachine : StateMachine
{
    // 親ステートマシン
    public ModelStateMachine Parent { get; }

    // エアコンモデル
    public AirConditioner Model { get; }

    // コンストラクタ
    public RunningStateMachine(ModelStateMachine parent)
    {
        this.Parent = parent;

        this.Model = parent.Model;

        this.ChangeToInitialState();
    }

    // 初期状態取得
    protected override State GetInitialState()
    {
        return CoolState.Instance;
    }
}


CleanStateMachine クラス

CleanStateMachine クラスも RunningStateMachine と同様に定義します。 CleanStateMachine の初期状態は StainLevelAnalysis 状態となります。

public class CleanStateMachine : StateMachine
{
    // 親ステートマシン
    public ModelStateMachine Parent { get; }

    // エアコンモデル
    public AirConditioner Model { get; }

    // コンストラクタ
    public CleanStateMachine(ModelStateMachine parent)
    {
        this.Parent = parent;

        this.Model = parent.Model;

        this.ChangeToInitialState();
    }

    // 初期状態取得
    protected override State GetInitialState()
    {
        return StainLevelAnalysisState.Instance;
    }
}


Trigger クラス

Trigger クラスを継承した各トリガを実装します。
エアコンステートマシンでは、7 つのトリガを実装する必要があります。

名称 詳細 備考
InitializedTrigger 初期化完了トリガ
SwitchStartTrigger スタートボタン押下トリガ
SwitchStopTrigger ストップボタン押下トリガ
SwitchCoolTrigger 冷房ボタン押下トリガ
SwitchHeatTrigger 暖房ボタン押下トリガ
SwitchDryTrigger 除湿ボタン押下トリガ
SwitchCleanTrigger クリーニングボタン押下トリガ


各トリガはシステム内で 1 つのインスタンスのみを持つべきなので、シングルトンで実装します。
また、状態遷移時にエフェクトを実行する必要がある場合は、Effect クラスを継承したエフェクトをベースクラスのコンストラクタに受け渡します。

InitializedTrigger クラス

public sealed class InitializedTrigger : Trigger
{
    public static InitializedTrigger Instance { get; private set; } = new InitializedTrigger();

    public InitializedTrigger() : base("Initialized Trigger")
    {
    }
}


SwitchStartTrigger クラス

public sealed class SwitchStartTrigger : Trigger
{
    // シングルトンインスタンス
    public static SwitchStartTrigger Instance { get; private set; } = new SwitchStartTrigger();

    // コンストラクタ
    public SwitchStartTrigger() : base("Switch Start Trigger", SwitchStartEffect.Instance)
    {
    }
}


SwitchStopTrigger クラス

public sealed class SwitchStopTrigger : Trigger
{
    // シングルトンインスタンス
    public static SwitchStopTrigger Instance { get; private set; } = new SwitchStopTrigger();

    // コンストラクタ
    public SwitchStopTrigger() : base("Switch Stop Trigger", SwitchStopEffect.Instance)
    {
    }
}


SwitchCoolTrigger クラス

public sealed class SwitchCoolTrigger : Trigger
{
    // シングルトンインスタンス
    public static SwitchCoolTrigger Instance { get; private set; } = new SwitchCoolTrigger();

    // コンストラクタ
    public SwitchCoolTrigger() : base("Switch Cool Trigger")
    {
    }
}


SwitchHeatTrigger クラス

public sealed class SwitchHeatTrigger : Trigger
{
    // シングルトンインスタンス
    public static SwitchHeatTrigger Instance { get; private set; } = new SwitchHeatTrigger();

    // コンストラクタ
    public SwitchHeatTrigger() : base("Switch Heat Trigger")
    {
    }
}


SwitchDryTrigger クラス

public sealed class SwitchDryTrigger : Trigger
{
    // シングルトンインスタンス
    public static SwitchDryTrigger Instance { get; private set; } = new SwitchDryTrigger();

    // コンストラクタ
    public SwitchDryTrigger() : base("Switch Dry Trigger")
    {
    }
}


SwitchCleanTrigger クラス

public sealed class SwitchCleanTrigger : Trigger
{
    // シングルトンインスタンス
    public static SwitchCleanTrigger Instance { get; private set; } = new SwitchCleanTrigger();

    // コンストラクタ
    public SwitchCleanTrigger() : base("Switch Clean Trigger")
    {
    }
}


Effect クラス

Effect クラスを継承した各エフェクトを実装します。
エアコンステートマシンでは、3 つのエフェクトを実装する必要があります。

名称 詳細 備考
SwitchStartEffect スタートボタン押下エフェクト
SwitchStopEffect ストップボタン押下エフェクト
CleanEndEffect クリーニング完了エフェクト


各エフェクトはシステム内で 1 つのインスタンスのみを持つべきなので、シングルトンで実装します。
また、ベースクラスのEffectクラスで宣言された抽象メソッドExecuteActionをオーバーライドすることで、エフェクト固有の動作を定義します。

SwitchStartEffect クラス

SwitchStartEffectクラスでは、スタートボタンが押下され、Running状態に遷移する際に行われる処理を定義します。

protected override void ExecuteAction(StateMachine context)
{
    // 親ステートマシン(ModelStateMachine)を取得
    var stm = context.GetAs<ModelStateMachine>();

    // モデル(AirConditioner)を取得
    var model = stm.Model;

    // エアコンのスタート処理を呼び出し
    model.Start();
}


ソース全体を示します。

public sealed class SwitchStartEffect : Effect
{
    // シングルトンインスタンス
    public static SwitchStartEffect Instance { get; private set; } = new SwitchStartEffect();

    // コンストラクタ
    public SwitchStartEffect() : base("Switch Start Effect")
    {
    }

    // エフェクトアクション
    protected override void ExecuteAction(StateMachine context)
    {
        var stm = context.GetAs<ModelStateMachine>();

        var model = stm.Model;

        model.Start();
    }
}


SwitchStopEffect クラス

SwitchStoptEffectクラスでは、ストップボタンが押下され、Stop状態に遷移する際に行われる処理を定義します。

protected override void ExecuteAction(StateMachine context)
{
    // 親ステートマシン(ModelStateMachine)を取得
    var stm = context.GetAs<ModelStateMachine>();

    // モデル(AirConditioner)を取得
    var model = stm.Model;

    // エアコンの停止処理を呼び出し
    model.Stop();
}


ソース全体を示します。

public sealed class SwitchStopEffect : Effect
{
    // シングルトンインスタンス
    public static SwitchStopEffect Instance { get; private set; } = new SwitchStopEffect();

    // コンストラクタ
    public SwitchStopEffect() : base("Switch Stop Effect")
    {
    }

    // エフェクトアクション
    protected override void ExecuteAction(StateMachine context)
    {
        var stm = context.GetAs<ModelStateMachine>();

        var model = stm.Model;

        model.Stop();
    }
}


CleanEndEffect クラス

CleanEndEffectでは、クリーニング処理が完了し、Running状態に遷移する際に行われる処理を定義します。

// エフェクトアクション
protected override void ExecuteAction(StateMachine context)
{
    // 親ステートマシン(ModelStateMachine)を取得
    var stm = context.GetAs<ModelStateMachine>();

    // モデル(AirConditioner)を取得
    var model = stm.Model;

    // エアコンのクリーニング完了処理を呼び出し
    model.CleanEnd();
}


ソース全体を示します。

public sealed class CleanEndEffect : Effect
{
    // シングルトンインスタンス
    public static CleanEndEffect Instance { get; private set; } = new CleanEndEffect();

    // コンストラクタ
    public CleanEndEffect() : base("Clean End Effect")
    {
    }

    // エフェクトアクション
    protected override void ExecuteAction(StateMachine context)
    {
        var stm = context.GetAs<ModelStateMachine>();

        var model = stm.Model;

        model.CleanEnd();
    }
}


次回予告

次回も引き続きステートマシンの実装を行っていきます。

an-embedded-engineer.hateblo.jp

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

まえがき

今回は、前回作成したベースクラスを使って実装するステートマシンのサンプルについて説明します。

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


上記ステートマシンは、エアコンの状態遷移を表したものになります。
なお、今回作成するステートマシンはサンプルのため、現実のエアコンの挙動とは異なる場合があります。

エアコンステートマシンは大きく 3 つの状態に別れます。

状態 詳細 備考
Initial 初期状態
Stop エアコン停止状態
Running エアコン稼働状態
Clean エアコンクリーニング状態


Initial 状態

Initial 状態はステートマシン開始前の初期状態(疑似状態)です。
システム起動後、各種初期化が完了すると、「T_Initialized」トリガが発行され、Stop 状態に遷移します。


Stop 状態

Stop 状態は、エアコンが停止し、ユーザからの要求を待機している状態です。
ユーザがリモコンのスタートボタンを押すと、「T_SwitchStart」トリガが発行され、Running 状態に遷移します。
また、状態遷移時に「E_SwitchStartEffect」エフェクトが実行されます。


Running 状態

Running 状態は、エアコンが通常動作している状態です。 今回作成するエアコンでは、冷房/暖房/除湿の動作モードがあり、それぞれユーザのリモコン操作で切り替えることができるものとします。

Running 状態でユーザがストップボタンを押すと「T_SwitchStop」トリガが発行され、Stop 状態に遷移します。
また、状態遷移時に「E_SwitchStopEffect」エフェクトが実行されます。


Running 状態でユーザが内部クリーニングボタンを押すと「T_SwitchClean」トリガが発行され、Clean 状態に遷移します。


Running 状態でユーザが各種モード切り替えボタンを押すことでサブ状態が変化し、動作モードを変更することができます。

Running 状態のサブ状態を示します。

状態 詳細 備考
History 履歴疑似状態
Cool 冷房状態
Heat 暖房状態
Dry 除湿状態


History サブ状態

前回 Running 状態を出た時のサブ状態を記憶しておくための疑似状態です。
Running 状態への初回遷移時には、履歴が存在しないため Cool 状態に遷移します。


Cool サブ状態

Cool サブ状態は、冷房制御を行っている状態です。
指定された目標温度になるように室内の温度制御を行います。


Cool サブ状態でユーザが暖房ボタンを押すと「T_SwitchHeat」トリガが発行され、Heat サブ状態に遷移します。


Cool サブ状態でユーザが除湿ボタンを押すと「T_SwitchDry」トリガが発行され、Dry サブ状態に遷移します。


Heat サブ状態

Heat サブ状態は、暖房制御を行っている状態です。
指定された目標温度になるように室内の温度制御を行います。


Heat サブ状態でユーザが冷房ボタンを押すと「T_SwitchCool」トリガが発行され、Cool サブ状態に遷移します。


Heat サブ状態でユーザが除湿ボタンを押すと「T_SwitchDry」トリガが発行され、Dry サブ状態に遷移します。


Dry サブ状態

Dry サブ状態は、除湿制御を行っている状態です。
湿度が下がるように室内の湿度制御を行います。


Dry サブ状態でユーザが冷房ボタンを押すと「T_SwitchCool」トリガが発行され、Cool サブ状態に遷移します。


Dry サブ状態でユーザが暖房ボタンを押すと「T_SwitchHeat」トリガが発行され、Heat サブ状態に遷移します。


Clean 状態

Clean 状態は、エアコンが内部クリーニングをしている状態です。 今回作成するエアコンでは、内部の汚れレベル(Low / High)を自動的に判断し、レベルに応じてクリーニングモード(入念 / あっさり)を自動的に切り替えます。

Running 状態でユーザがストップボタンを押すと「T_SwitchStop」トリガが発行され、Stop 状態に遷移します。
また、状態遷移時に「E_SwitchStopEffect」エフェクトが実行されます。


クリーニング処理が完了すると、自動的に Running 状態に遷移します。
また、状態遷移時に「E_CleanEndEffect」エフェクトが実行されます。


Clean 状態に入ると、汚れレベルの判定およびクリーニング処理が自動的に行われ、その際にサブ状態も自動的に遷移します。


Clean 状態のサブ状態を示します。

状態 詳細 備考
Initial 開始疑似状態
StainLevelAnalysis 汚れレベル解析状態
DeepClean 入念クリーニング状態
LightClean あっさりクリーニング状態
Final 最終状態


Initial サブ状態

Inital サブ状態は Clean 状態におけるサブステートマシン開始前の初期状態(疑似状態)です。
Clean 状態に入ると、サブ状態は自動的に StainLevelAnalysis サブ状態に遷移します。


StainLevelAnalysis サブ状態

StainLevelAnalysis サブ状態は、汚れレベルの解析を行っている状態です。 汚れレベルの解析が終了すると、解析された汚れレベルに応じて、自動的に状態遷移が発生します。

汚れレベル(Stain Level)が High だった場合、DeepClean サブ状態に遷移します。
また、汚れレベルが Low だった場合は LightClean サブ状態に遷移します。


DeepClean サブ状態

DeepClean サブ状態は、入念クリーニングを行っている状態です。 クリーニング処理が終了すると、Final 状態に自動的に遷移します。


LightClean サブ状態

LightClean サブ状態は、あっさりクリーニングを行っている状態です。 クリーニング処理が終了すると、Final 状態に自動的に遷移します。


Final サブ状態

Final サブ状態は、クリーニング処理が完了した状態です。 Final サブ状態に遷移すると、Clean 状態は自動的に Running 状態に遷移します。


次回予告

次回はいよいよステートマシンを実装していきます。

an-embedded-engineer.hateblo.jp

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

まえがき

ステートマシン(状態機械)の実装方法にはいくつか方法があり、有名なものとして GoF デザインパターンState パターンがありますが、UML で表記されるステートマシン図の実装方法について書かれた Web ページなどをあまり見かけないので、State パターンをベースに実装してみました。

ベースクラス定義

ステートマシンを実装する上での共通処理をベースクラスとして定義します。

クラス名 役割 備考
StateMachine トリガの送信、状態遷移の管理
State 状態ごとの動作、トリガに対する動作の制御
Trigger 状態遷移を発生させるためのトリガ
Effect 状態遷移時に実行されるアクション


StateMachine クラス

StateMachine クラスはステートマシンそのものを表すクラスで、トリガの送信や状態遷移の管理を行います。
StateMachine クラスは抽象クラスとして定義します。

public abstract class StateMachine
{
}


プロパティとしては、現在の状態(CurrentState)と、ひとつ前の状態(PreviousState)を保持します。

// 現在の状態
public State CurrentState { get; private set; }

// ひとつ前の状態
public State PreviousState { get; private set; }


ステートマシン開始後の初期状態を設定するためのメソッドを定義します。
初期状態として何を設定したいかは、ステートマシンに依存するので、初期状態を取得する抽象メソッドと、 取得した初期状態に遷移させるためのメソッドを定義します。

// 初期状態の取得
protected abstract State GetInitialState();

// 初期状態への遷移
protected void ChangeToInitialState()
{
    var initial_state = this.GetInitialState();

    this.ChangeState(initial_state);
}


続いて、トリガの送信です。
ステートマシンは、トリガの発生によって状態遷移が発生します。
本実装では、ステートマシンが外部からのトリガを受信すると、 現在の状態(CurrentState)に対してトリガを送信します。
CurrentState は受信したトリガに応じて、アクションの実行や状態遷移を発生させます。

// トリガの送信
public void SendTrigger(Trigger trigger)
{
    this.CurrentState?.SendTrigger(this, trigger);
}


次に、状態遷移処理です。
UML のステートマシンは、以下のような手続きに従って状態遷移を行います。

  1. 現在の状態における Exit アクションの実行(定義されている場合)
  2. 状態を変更
  3. エフェクト処理を実行(定義されている場合) *1
  4. 新しい状態における Entry アクションの実行(定義されている場合)
// 状態遷移
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;

        effect?.Execute(this);

        this.CurrentState?.ExecuteEntryAction(this);
    }
}


各状態における Do アクションは、その状態であり続ける限り定期的に実行されます。
外部から Do アクションを実行するための Update メソッドを定義します。

// 更新
public void Update()
{
    this.CurrentState?.ExecuteDoAction(this);
}


最後に StateMachine クラスを継承先のサブクラスにキャストするためのメソッドを定義します。

public T GetAs<T>() where T : StateMachine
{
    if (this is T stm)
    {
        return stm;
    }
    else
    {
        throw new InvalidOperationException($"State Machine is not {nameof(T)}");
    }
}


ソース全体を以下に示します。

public abstract class StateMachine
{
    public State CurrentState { get; private set; }

    public State PreviousState { get; private set; }

    protected abstract State GetInitialState();

    public StateMachine()
    {
    }

    public void SendTrigger(Trigger trigger)
    {
        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;

            effect?.Execute(this);

            this.CurrentState?.ExecuteEntryAction(this);
        }
    }

    public void Update()
    {
        this.CurrentState?.ExecuteDoAction(this);
    }

    public T GetAs<T>() where T : StateMachine
    {
        if (this is T stm)
        {
            return stm;
        }
        else
        {
            throw new InvalidOperationException($"State Machine is not {nameof(T)}");
        }
    }

    protected void ChangeToInitialState()
    {
        var initial_state = this.GetInitialState();

        this.ChangeState(initial_state);
    }
}


StateEventHandler デリゲート

StateEventHandler デリゲートは、State(状態)に定義されたイベント(Entry / Do / Exit)の呼び出しを行うためのデリゲートです。

StateMachine を引数として受け取るデリゲートとして定義します。

public delegate void StateEventHandler(StateMachine conetxt);


TriggerActionMap クラス

TriggerActionMap クラスは、Trigger 受診時に実行するアクションを登録するハッシュテーブルです。
文字列を Key、TriggerActionArgs を引数にとる Action を Value とした Dictionary クラスを継承します。

public class TriggerActionMap : Dictionary<string, Action<TriggerActionArgs>>
{
}


TriggerActionArgs クラス

TriggerActionArgs クラスは、Trigger 受信に実行されるアクションに渡される引数です。
State の親となる StateMachine と、受信した Trigger を含みます。

public class TriggerActionArgs
{
    public StateMachine Context { get; }

    public Trigger Trigger { get; }

    public TriggerActionArgs(StateMachine context, Trigger trigger)
    {
        this.Context = context;

        this.Trigger = trigger;
    }
}


Trigger クラス

Trigger クラスは、状態遷移を発生させるトリガを表すクラスです。
Trigger クラスは抽象クラスとして定義し、Trigger 名と(必要に応じて)Effect を保持します。

public abstract class Trigger
{
    public string Name { get; }

    public Effect Effect { get; }

    public Trigger(string name, Effect effect = null)
    {
        this.Name = name;

        this.Effect = effect;
    }
}


Effect クラス

Effect クラスは、状態遷移時に実行されるエフェクトを表すクラスです。
Effect クラスは抽象クラスとして定義します。

プロパティとしては Effect 名(Name)を保持します。

また、エフェクトで実行するアクション(ExecuteAction)を抽象メソッドとして定義し、継承先で実装させます。

最後に、StateMachine か Effect を実行するためのパブリックメソッド(Execute)を定義します。

public abstract class Effect
{
    string Name { get; }

    protected abstract void ExecuteAction(StateMachine context);

    public Effect(string name)
    {
        this.Name = name;
    }

    public void Execute(StateMachine context)
    {
        this.ExecuteAction(context);
    }
}


State クラス

State クラスはステートマシンの1つの状態を表すクラスで、 状態ごとのイベント(Entry / Do / Exit)やトリガに対するアクションを定義します。
State クラスは抽象クラスとして定義します。

public abstract class State
{
}


プロパティとしては、以下の情報を保持します。

プロパティ名 役割 備考
Name string 状態名
OnEntry StateEventHandler Entry イベント処理
OnDo StateEventHandler Do イベント処理
OnExit StateEventHandler Exit イベント処理
TriggerActionMap TriggerActionMap Trigger 受信時に実行するアクションのハッシュテーブル


public string Name { get; }

protected StateEventHandler OnEntry;
protected StateEventHandler OnDo;
protected StateEventHandler OnExit;

private TriggerActionMap TriggerActionMap { get; set; }


Trigger を受信した際の動作は状態ごとに異なるため、TriggerActionMap の生成を継承先で行うための抽象メソッド(GenerateTriggerActionMap)を定義し、コンストラクタで実行します。

protected abstract TriggerActionMap GenerateTriggerActionMap();

public State(string name)
{
    this.Name = name;

    this.TriggerActionMap = this.GenerateTriggerActionMap();
}


続いて、状態ごとのイベント(Entry / Do / Exit)を StateMachine から実行させるためのパブリックメソッドを定義します。
それぞれのイベントにイベントハンドラが定義されていたら、そのイベントハンドラを呼び出すようにします。

public void ExecuteEntryAction(StateMachine context)
{
    this.OnEntry?.Invoke(context);
}

public void ExecuteDoAction(StateMachine context)
{
    this.OnDo?.Invoke(context);
}

public void ExecuteExitAction(StateMachine context)
{
    this.OnExit?.Invoke(context);
}


最後に、状態に対して Trigger が送信された時の動作を定義します。
TriggerActionMap に送信された Trigger の名前が登録されていたら、Trigger に対応する Action を取得します。
StateMacine と Trigger を TriggerActionArgs にセットし、それを引数として Action を実行します。

public void SendTrigger(StateMachine context, Trigger trigger)
{
    if (this.TriggerActionMap.ContainsKey(trigger.Name) == true)
    {
        var action = this.TriggerActionMap[trigger.Name];

        var args = new TriggerActionArgs(context, trigger);

        action(args);
    }
}


ソース全体を以下に示します。

public abstract class State
{
    public string Name { get; }

    protected StateEventHandler OnEntry;
    protected StateEventHandler OnDo;
    protected StateEventHandler OnExit;

    private TriggerActionMap TriggerActionMap { get; set; }

    protected abstract TriggerActionMap GenerateTriggerActionMap();

    public State(string name)
    {
        this.Name = name;

        this.TriggerActionMap = this.GenerateTriggerActionMap();
    }

    public void ExecuteEntryAction(StateMachine context)
    {
        this.OnEntry?.Invoke(context);
    }

    public void ExecuteDoAction(StateMachine context)
    {
        this.OnDo?.Invoke(context);
    }

    public void ExecuteExitAction(StateMachine context)
    {
        this.OnExit?.Invoke(context);
    }

    public void SendTrigger(StateMachine context, Trigger trigger)
    {
        if (this.TriggerActionMap.ContainsKey(trigger.Name) == true)
        {
            var action = this.TriggerActionMap[trigger.Name];

            var args = new TriggerActionArgs(context, trigger);

            action(args);
        }
    }
}


次回予告

次回は、今回作成したベースクラスを使って実装する、サンプルのステートマシンについて説明します。

an-embedded-engineer.hateblo.jp

*1:UML の仕様では、エフェクト実行時の状態は『不定』となっていますが、本実装では状態変更後にしています