C#でGoFのデザインパターン~Stateパターン編~
2019-10-28
azblob://2022/11/11/eyecatch/2019-10-28-csharp-gof-statepattern-000-e1572252959610.jpg

はじめに

※10/29更新 StateパターンとStrategyパターンの違いが明確になるようにしました。

GoF(Gang of Four)という人たちをご存知でしょうか?

「Design Patterns: Elements of Reusable Object-Oriented Software」という書籍を書いた著者4人のことを指し、デザインパターンの始祖といわれています。

デザインパターンとは、ソフトウェア開発においてよく突き当りがちな問題とそれに対する解決策のセットをまとめたもので、「こういうことをしたいときはこのようにコードを書こうね」という指針を示してくれます。

デザインパターンといっても様々なものがあり、その中でも書籍内で紹介されたソフトウェアのプログラムに関するもの23個のパターンの集まりを特にGoFのデザインパターンと呼ばれています。

Stateパターンとは

さて、以前のブログで、Automatonymousを使用してState Machineを実装する方法を解説しました。

State Machineを簡単に、分かりやすい書き方で実装できる.Net用のライブラリというのがこのAutomatonymousだったわけですが、このような「あるインスタンスが複数の状態のうちのいずれかにあり、その状態によって振る舞いが変わる」というようなプログラムは、ライブラリに頼らずに書くこともできます。

それがGoFのデザインパターンの中のStateパターンというもので、いくつかのクラスと一つのインターフェースを用いて実装できます。

今回は、C#を用いてStateパターンを実装する方法を解説していきたいと思います。

Stateパターン実装

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

作成できたら、次にプロジェクトにIStateという名前のインターフェースを追加します。
コードは以下のものをコピーしてください。

    interface IState
    {
        void Method1();
        IState Method2();
    }

これが、それぞれの状態での振舞いを規定します。
Method2においては返り値がIState型であることを覚えておいてください。

次に、状態の一つを示すState1というクラスを実装します。

    class State1 : IState
    {
        public void Method1()
        {
            Console.WriteLine("State1の状態でMethod1が実行されました");
        }

        public IState Method2()
        {
            Console.WriteLine("State1の状態でMethod2が実行されました");
            Console.WriteLine("StateがState2に遷移します");
            return new State2();
        }
    }

このクラスはIStateのインターフェースを継承しているので、Method1、Method2という振舞いを実装する必要があります。
Method2の返り値の型はIState型でした。
ここで返すインスタンスは、Method2の処理が終了した後に遷移する状態のクラスのものになります。
これによって、これらの状態を持つクラスの状態遷移を実現します。

もう一つの状態を示すState2も用意しましょう。

    class State2 : IState
    {
        public void Method1()
        {
            Console.WriteLine("State2の状態でMethod1が実行されました");
        }

        public IState Method2()
        {
            Console.WriteLine("State2の状態でMethod2が実行されました");
            Console.WriteLine("StateがState1に遷移します");
            return new State1();
        }
    }

これらのクラスは、どちらもIStateというインターフェースを実装しているためにMethod1、Method2という振舞いができますが、状態によってその振舞いの実装が異なることを覚えておいてください。

次に、これらの状態を持ちうる本体となるクラスを用意します。


    class Context
    {
        private IState _state;

        public Context(IState state)
        {
            _state = state;
        }

        // 現在のStateを返します。
        public void CurrentState()
        {
            Console.WriteLine($"現在のStateは{_state.GetType().Name}です");
        }

        // Stateを変更します。
        public void ChangeState(IState state)
        {
            _state = state;
            Console.WriteLine($"Stateが{ _state.GetType().Name}に変更されました");
        }

        // それぞれのStateにおけるMethodを実行します。
        public void ExcuteMethod1()
        {
            _state.Method1();
        }

        public void ExcuteMethod2()
        {
            _state = _state.Method2();
        }
    }

このContextクラスは、インスタンス化する際にIStateを継承した何かしらのクラスを引数にとり、保持しておきます。

そして、ExcuteMethod1,ExcuteMethod2が呼ばれた際にそのクラスで実装されたメソッドを実行します。
特にExcuteMethod2においては、返り値としてやってきた状態インスタンスを自分の状態を示す_stateに代入することで、自分の状態を切り替えています。

なので、もしインスタンス化時にState1を受け取っていればExcuteMethod1が呼ばれた際にはState1のMethod1が実行され、State2を受け取っていればState2のMethod1が実行されます。

また、このContextのインスタンスの状態を変更するためにChangeStateメソッドを用意し、そこで引数に取ってきた状態に変更することもできます。

このようにして、同じクラスが状態によって別の振る舞いをするといった動作を実装することができます。

最後に、このプログラムを実際に呼び出す部分を作成します。
既存のProgram.csにコードを追加していきます。

    class Program
    {
        static void Main(string[] args)
        {
            var context = new Context(new State1());
            context.ExcuteMethod2();
            context.CurrentState();
            context.ExcuteMethod1();
        }
    }

ここではContextクラスに状態1を初期状態として渡してインスタンス化し、それからメソッドをいくつか実行しています。

このコードを実行して以下の4行が出力されれば成功です。
特に、Method2を実行するとState1とState2の状態が相互に入れ替わることに注意してください。

State1の状態でMethod2が実行されました
StateがState2に遷移します
現在のStateはState2です
State2の状態でMethod1が実行されました

おわりに

今回は、GoFのデザインパターンの一つであるStateパターンをC#で実装する方法を解説しました。

Stateパターンは、同じクラスのインスタンスでも中身の状態が変化したときに動作を変化させたいという要件があるときに有効です。

GoFのデザインパターンは全部で23パターンあり、どれもオブジェクト指向プログラミングをフル活用しているので、このデザインパターンを学ぶことはオブジェクト指向を学ぶのにも適していると考えています。

これから他のパターンについてもコード付きで解説していきたいと考えているので、それらもぜひご覧ください。