C#でシンプルなStatePatternを組んだ


StatePattern

デザインパターンの一角, StatePatternをC#で組んでみました.

Stateパターンを使えば, 呼び出し側のコードでif文/switch-case文を書かなくて良いというメリットがあります.
今回は, 汎用的に使えるState/Contextのクラスを作ってみます.

ソースはこちら

State

public class State
{
    public StateContext Context
    {
        get;
        private set;
    }

    public delegate void stateEnterEvent();
    public stateEnterEvent OnEnter;

    public delegate void stateExitEvent();
    public stateEnterEvent OnExit;

    public State(StateContext context)
    {
        Context = context;
    }
}

ベースとなるステートクラスでは, 「自分のステートに遷移した時」と「自分のステートから出て行った時」のイベントを用意しています.

Context

各Stateを管理して, Stateの遷移を管理・実行するクラスです.

public class StateContext
{
    public List<State> StateList = new List<State>();
    public State CurrentState { get; private set; }

    // Allow transit to self state
    public bool SelfTransit = true;

    object Locker = new object();

    public void setCurrentState(State state)
    {
        if (state == null || !StateList.Contains(state))
            return;

        CurrentState = state;
    }

    public void addState(State state)
    {
        if (state == null || StateList.Contains(state))
            return;
        StateList.Add(state);
    }

    public void transitState(State targetState)
    {
        if (targetState == null || (StateList.Contains(targetState) && SelfTransit))
        {
            return;
        }

        lock (Locker)
        {
            CurrentState?.OnExit();
            CurrentState = targetState;
            CurrentState?.OnEnter();
        }
    }
}

状態の追加には「addState」, 遷移には「transitState」を呼び出せばOKです.

使い方

State/StateContextを継承した自作State/自作Contextを作り, AddStateを使ってStateを登録していきます.
遷移時のイベントは「onEnter」「onExit」に登録可能です.
詳しいコードはGitHubに上げているので, そちらをご覧ください.

一つ一つのStateが大きな場合

↓のように, 具象クラスを別ファイルで実装してみると良いと思います.

LargeState.cs
public class LargeState : State
{
    public LargetState(LargetContext context, LargeState next) : base(context)
    {
         ...          
    }   
}

有効利用できそうなケース

UnityでUI組むときとかに使えそうです.

UIの場合, ユーザからの入力情報は変わらないけど, 階層構造をもっていて画面遷移する事が多いです.
Stateに応じた入力処理・画面遷移を組めば, 呼び出し側のコード(ビヘイビア)は綺麗に保てそうです.

StatePatternを使う上での注意点

呼び出すメソッドが共通化できる時に使わないと, 却ってコードの煩雑化を招きます

例えば, こんなケース.

public class StateA : State
{
     public void myOwnMethodA();
}

public class StateB : State
{
     public void myOwnMethodB();
}

...

public class MyContext : Context
{
     public void onInput()
     {
        if (CurrentState.is(StateA))
        {
           (CurrentState as StateA).myOwnMethodA();
        }
        ...
     }
}

StateAとStateBの間で異なるインタフェースしか提供されないと, 結局Context側で無駄な条件分岐が発生します.

本来やりたかったのは「呼び出し側のコードがif文/switch-case文を使いたくない」という事なので, 本末転倒です.
「あ, このメソッドは共通化できるな」という見通しが立ってから使った方がよさそうです.

(...という認識であってるかなー? ご意見あれば, コメント欄に書き込んで下さると勉強になります)