AutomatonymousでState Machineを作ろう!
2019-10-28
azblob://2022/11/11/eyecatch/2019-10-28-statemachine-automatonymous-000-e1572233701656.jpg

はじめに

オートマトンはご存知でしょうか?

基本情報技術者試験や応用技術者試験を勉強された方は、概念については聞いたことがあると思います。

何かしらのプログラムが、現在の状態とその状態から他の状態へ遷移するルールを持ち、それに従って処理を行うものの概念です。

他の言葉では状態機械、State Machine等といい、その中で状態の数が有限のものを「有限オートマトン」といったりもします。

今回は、使用するライブラリ中でState Machineと呼ばれているので、以後State Machineと呼びます。

この概念はプログラムにおいても実装可能で、GoFのデザインパターンのうちのStateパターンのような形でコードに落とし込むこともでき、このパターンを実際に使うようなこともあります。

.Netにあるライブラリで、このState Machineを簡単に実装することができるものがあります。

名前をAutomatonymousといい、以前RabbitMQでメッセージキューを使う記事において使用したライブラリであるMassTransitの開発者グループによって開発されています。

今回は、このライブラリを使って.Net環境でState Machineを作成する方法を解説したいと思います。

State Machineを実装しよう!

まずはプロジェクトを作成しましょう。

.Net Coreのコンソールアプリケーションのテンプレートでプロジェクトを作成します。

作成できたら、まずはNugetからAutomatonymousのライブラリーをインストールしてきます。

執筆時点での最新バージョンは4.1.6だったので、それ以降であれば動作するはずです。

そうしたら次にStateClassというクラスを追加します。

クラスの中のコードは以下の通りです。

class StateClass
{
    public State CurrentState { get; set; }
}

これが、State Machineの中身になります。

CurrentStateの部分に現在のStateを保持しています。

また、State Machineとして他の情報も持たせたくなった場合はここに追加のフィールドを追加することで実現できます。

次に、StateMachineBehaviorというクラスを作成します。

class StateMachineBehavior : AutomatonymousStateMachine<StateClass>
{
    public StateMachineBehavior()
    {
    }
}

このクラスにはAutomatonymousStateMachineというジェネリックの抽象クラスを継承させるのが肝で、これによってこのクラスがState Machineの振る舞いを規定すること、また、その振舞いで使用するStateのクラスとしてStateClassを使用することをここで規定しています。

続いて、このState Machineが持ちうるStateと状態変化を起こしうるEventを規定していきましょう。

StateMachineBehaviorのクラスの中に追加してください。

// 持ちうるStateたちです。
public State StateA { get; private set; }
public State StateB { get; private set; }
public State StateC { get; private set; }
// 起きうるEventたちです。
public Event TransitionToA { get; private set; }
public Event TransitionToB { get; private set; }
public Event TransitionToC { get; private set; }

これらが、今回規定するStateと発生させるEvent群になります。

この追記が終わったら、振舞いを定義していきましょう。
State Machineは初期状態ではStateを持ちません。

なので、最初にイベントを受け取ったらどういった動作をするか、どの状態に遷移するかといったコードを追加します。

同じく、StateMachineBehavior.cs内に追加してください。

public StateMachineBehavior()
{
    // 初期状態でイベントを受け取った際の動作です。
    Initially(
        When(TransitionToA)
        .Then(context =>
        Console.WriteLine("初期状態として状態Aに遷移しました"))
        .TransitionTo(StateA),
        When(TransitionToB)
        .Then(context =>
        Console.WriteLine("初期状態として状態Bに遷移しました"))
        .TransitionTo(StateB),
        When(TransitionToC)
        .Then(context =>
        Console.WriteLine("初期状態として状態Cに遷移しました"))
        .TransitionTo(StateC)
        );
}

ここはクラスのコンストラクタですが、この中で各振る舞いを規定します。

このコードだと、TransitionToAというイベントを受け取ったらStateA(状態A)に遷移し、同じような遷移を行うイベントがB、C同様に設定されています。
これで、初期状態でのイベントに対する反応を規定できました。

次に、各Stateの状態でイベントを受け取った際の動作のコードを追加します。

StateMachineBehaviorのコンストラクタの中に追記してください。

// それぞれStateがA,B,Cの時にイベントを受け取った際の動作です
During(StateA,
    When(TransitionToA)
    .Then(context =>
    Console.WriteLine("既に状態Aです")),
    When(TransitionToB)
    .Then(context =>
    Console.WriteLine("状態Bに遷移しました"))
    .TransitionTo(StateB),
    When(TransitionToC)
    .Then(context =>
    Console.WriteLine("状態Cに遷移しました"))
    .TransitionTo(StateC)
    );
During(StateB,
    When(TransitionToA)
    .Then(context =>
    Console.WriteLine("状態Aに遷移しました"))
    .TransitionTo(StateA),
    When(TransitionToB)
    .Then(context =>
    Console.WriteLine("既に状態Bです")),
    When(TransitionToC)
    .Then(context =>
    Console.WriteLine("状態Cに遷移しました"))
    .TransitionTo(StateC)
    );
During(StateC,
    When(TransitionToA)
    .Then(context =>
    Console.WriteLine("状態Aに遷移しました"))
    .TransitionTo(StateA),
    When(TransitionToB)
    .Then(context =>
    Console.WriteLine("状態Bに遷移しました"))
    .TransitionTo(StateB),
    When(TransitionToC)
    .Then(context =>
    Console.WriteLine("既に状態Cです"))
    );

それぞれ、現在の状態がStateA、B、Cだった時に各イベントを受け取った際の振舞いを規定しています。

前のコードと同様に.Thenの部分にイベントを受け取った際の動作、.TransitionToの部分にその後遷移する先の状態を定義していますが、これらはなくても動作します。

なので、遷移したくない場合は.TransitionToの部分を書かないこともできます。

さて、ここまでで、State Machineのコアの部分は完成しました。

最後に、このState MachineをProgram.csで呼び出すコードを追加します。

Main関数の中に以下のコードを追加してください。

var state = new StateClass();
var stateMachineBehavior = new StateMachineBehavior();

// 初期状態でTransitionToAのイベントを発生
stateMachineBehavior.RaiseEvent(state, stateMachineBehavior.TransitionToA);

// StateAの状態でTransitionToBイベントを発生
stateMachineBehavior.RaiseEvent(state, stateMachineBehavior.TransitionToB);
// StateBの状態でTransitionToBイベントを発生
stateMachineBehavior.RaiseEvent(state, stateMachineBehavior.TransitionToB);

この部分では、まず状態を保持するクラスと振舞いを規定したクラスをインスタンス化します。

そして、振舞いを規定したクラスのRaiseEventメソッドを呼び出すことで、イベントを発生させます。

このコードを実行すると、

初期状態として状態Aに遷移しました
状態Bに遷移しました
既に状態Bです

の3行が表示されるはずです。

それぞれの状態でEventが実行されるにしたがって処理が行われ、状態が遷移することが確認できます。

さいごに

今回は、Automatonymousというライブラリを使ってState Machineを実装する方法を解説しました。

State Machineにおいて各状態が定義され、それぞれの状態でイベントが発生すると別の状態に遷移するといった振る舞いがコードの中でどのように実装されているかが分かったと思います。

参考になれば幸いです。