An Embedded Engineer’s Blog

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

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 の仕様では、エフェクト実行時の状態は『不定』となっていますが、本実装では状態変更後にしています