LoginSignup
6
1

More than 5 years have passed since last update.

デザインパターン勉強会 第19回:Stateパターン

Last updated at Posted at 2018-01-30

はじめに

本エントリーは某社内で実施するデザインパターン勉強会向けの資料となります。
本エントリーで書籍「Java言語で学ぶデザインパターン入門」をベースに学習を進めますが、サンプルコードはC#に置き換えて解説します。

Stateパターンでは投稿者の都合により、別の例としてサンプルプログラム新規作成します。

第1回:Iteratorパターン
第2回:Adapterパターン
第3回:Template Methodパターン
第4回:Factory Methodパターン
第5回:Singletonパターン
第6回:Prototypeパターン
第7回:Builderパターン
第8回:Abstract Factoryパターン
第9回:Bridgeパターン
第10回:Strategyパターン
第11回:Compositeパターン
第12回:Decoratorパターン
第13回:Visitorパターン
第14回:Chain of Responsibilityパターン
第15回:Facadeパターン
第16回:Mediatorパターン
第18回:Mementoパターン

Stateパターンとは

Stateは「状態」という意味で、あるものの状態をクラスで表現するデザインパターンです。
たとえば機械、デジタル回路やプログラム(特にGUI)など状態によって出力が変化するものを設計するときに、それらの振る舞いを有限個の状態と遷移で表すモデル(有限オートマトン)があります。
それをプログラムで実装する場合、機械に直接組み込むアセンブリ言語やオブジェクト指向言語ではないC言語などでは、状態によって振る舞いが変わる様をアセンブリ言語ではJump命令、C言語ではSwitch文で表現します。
C#やJavaのようなオブジェクト指向言語ではStateパターンを使用することで分かり易く保守性の高いプログラミングができるようになります。

Stateパターンでのクラスは以下のようになります。

名 前 解 説
State 状態を表すインターフェース
Concrete State Stateによって定められたインターフェースに則り実装します
Context 状態を管理するインターフェース
Concrete Context Contextによって定められたインターフェースに則り実装します

次に実際の実装例を見てみましょう。

実装例

ここでは、例として簡単なRPGの戦闘のプログラムを使用します。
勇者は以下のコマンドが選択できます。

  • 戦う
  • 魔法
  • スキル
  • 防御
  • アイテム
  • 逃走

戦う相手のモンスターは様々な状態変化を引き起こす技をランダムで使用します。それぞれの状態変化により各コマンドの動作は以下のようになります。

statetable.JPG

まずこのプログラムを実装する方法として、デザインパターンを使用しない方法で実装した場合を見てみます。Switch文を使用します。

StatePatternBAD.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApp2
{
    class StatePatternBad
    {
        const int NORMAL = 0;
        const int POISON = 1;
        const int PARALYZED = 2;
        const int CONFUSED = 3;
        const int SILENCED = 4;
        const int CURSED = 5;
        static void Main(string[] args)
        {
            Random rnd = new Random();

            String braveName = "中尾";
            String[] braveMagic = { "火球", "雷大雲" };
            String[] braveSkill = { "一閃突", "ナイフ投げ" };
            String[] braveItems = { "薬草" };

            String monsterName = "田町の門番";
            String[] monsterSkill = { "毒煙玉", "子守唄", "スタンガン", "恐喝", "ガン付", "呪い" };

            int currentState = NORMAL;

            Console.WriteLine(monsterName + "が現れた。");

            while (true)
            {
                Console.WriteLine("どうする? [0:戦う][1:魔法][2:特技][3:防御][4:道具][5:逃走]");
                int command;
                int enemyCommand;
                int random;
                do
                {
                    command = ReadInt(6);
                } while (command == -1);
                switch (command)
                {
                    case 0:
                        switch (currentState)
                        {
                            case NORMAL:
                                Console.WriteLine(braveName + "は" + monsterName + "に攻撃した。");
                                break;
                            case POISON:
                                Console.WriteLine(braveName + "は" + monsterName + "に攻撃した。");
                                Console.WriteLine(braveName + "は毒のダメージを受けた。");
                                break;
                            case PARALYZED:
                                Console.WriteLine(braveName + "は体がしびれて動けない。");
                                break;
                            case CONFUSED:
                                random = rnd.Next(4);
                                if (random == 3)
                                {
                                    Console.WriteLine(braveName + "はわけが分からなくなってしまっている。");
                                }
                                else if (random == 2)
                                {
                                    Console.WriteLine(braveName + "は自分を攻撃した。");
                                }
                                else if (random == 1)
                                {
                                    Console.WriteLine(braveName + "はぼーっとしている。");
                                }
                                else
                                {
                                    Console.WriteLine(braveName + "は" + monsterName + "に攻撃した。");
                                }
                                break;
                            case SILENCED:
                                Console.WriteLine(braveName + "は" + monsterName + "に攻撃した。");
                                break;
                            case CURSED:
                                random = rnd.Next(4);
                                if (random > 0)
                                {
                                    Console.WriteLine(braveName + "は金縛りに遭って動けない。");
                                }
                                else
                                {
                                    Console.WriteLine(braveName + "は" + monsterName + "に攻撃した。");
                                }
                                break;
                        }
                        break;
                    case 1:
                        Console.WriteLine("使用する魔法は?");
                        printArray(braveMagic);
                        do
                        {
                            command = ReadInt(braveMagic.Length);
                        } while (command == -1);
                        switch (currentState)
                        {
                            case NORMAL:
                                Console.WriteLine(braveName + "は" + monsterName + "に" + braveMagic[command] + "をとなえた。");
                                break;
                            case POISON:
                                Console.WriteLine(braveName + "は" + monsterName + "に" + braveMagic[command] + "をとなえた。");
                                Console.WriteLine(braveName + "は毒のダメージを受けた。");
                                break;
                            case PARALYZED:
                                Console.WriteLine(braveName + "は体がしびれて動けない。");
                                break;
                            case CONFUSED:
                                random = rnd.Next(4);
                                if (random == 3)
                                {
                                    Console.WriteLine(braveName + "はわけが分からなくなってしまっている。");
                                }
                                else if (random == 2)
                                {
                                    Console.WriteLine(braveName + "は自分を攻撃した。");
                                }
                                else if (random == 1)
                                {
                                    Console.WriteLine(braveName + "はぼーっとしている。");
                                }
                                else
                                {
                                    Console.WriteLine(braveName + "は" + monsterName + "に" + braveMagic[command] + "をとなえた。");
                                }
                                break;
                            case SILENCED:
                                Console.WriteLine(braveName + "は魔法を唱えることができない。");
                                break;
                            case CURSED:
                                random = rnd.Next(4);
                                if (random > 0)
                                {
                                    Console.WriteLine(braveName + "は金縛りに遭って動けない。");
                                }
                                else
                                {
                                    Console.WriteLine(braveName + "は" + monsterName + "に" + braveMagic[command] + "をとなえた。");
                                }
                                break;
                        }
                        break;
                    case 2:
                        Console.WriteLine("使用するスキルは?");
                        printArray(braveSkill);
                        do
                        {
                            command = ReadInt(braveSkill.Length);
                        } while (command == -1);
                        switch (currentState)
                        {
                            case NORMAL:
                                Console.WriteLine(braveName + "は" + monsterName + "に" + braveSkill[command] + "をした。");
                                break;
                            case POISON:
                                Console.WriteLine(braveName + "は" + monsterName + "に" + braveSkill[command] + "をした。");
                                Console.WriteLine(braveName + "は毒のダメージを受けた。");
                                break;
                            case PARALYZED:
                                Console.WriteLine(braveName + "は体がしびれて動けない。");
                                break;
                            case CONFUSED:
                                random = rnd.Next(4);
                                if (random == 3)
                                {
                                    Console.WriteLine(braveName + "はわけが分からなくなってしまっている。");
                                }
                                else if (random == 2)
                                {
                                    Console.WriteLine(braveName + "は自分を攻撃した。");
                                }
                                else if (random == 1)
                                {
                                    Console.WriteLine(braveName + "はぼーっとしている。");
                                }
                                else
                                {
                                    Console.WriteLine(braveName + "は" + monsterName + "に" + braveSkill[command] + "をした。");
                                }
                                break;
                            case SILENCED:
                                Console.WriteLine(braveName + "は" + monsterName + "に" + braveSkill[command] + "をした。");
                                break;
                            case CURSED:
                                random = rnd.Next(4);
                                if (random > 0)
                                {
                                    Console.WriteLine(braveName + "は金縛りに遭って動けない。");
                                }
                                else
                                {
                                    Console.WriteLine(braveName + "は" + monsterName + "に" + braveSkill[command] + "をした。");
                                }
                                break;
                        }
                        break;
                    case 3:
                        switch (currentState)
                        {
                            case NORMAL:
                                Console.WriteLine(braveName + "は身をまもっている。");
                                break;
                            case POISON:
                                Console.WriteLine(braveName + "は身をまもっている。");
                                Console.WriteLine(braveName + "は毒のダメージを受けた。");
                                break;
                            case PARALYZED:
                                Console.WriteLine(braveName + "は体がしびれて動けない。");
                                break;
                            case CONFUSED:
                                random = rnd.Next(4);
                                if (random == 3)
                                {
                                    Console.WriteLine(braveName + "はわけが分からなくなってしまっている。");
                                }
                                else if (random == 2)
                                {
                                    Console.WriteLine(braveName + "は自分を攻撃した。");
                                }
                                else if (random == 1)
                                {
                                    Console.WriteLine(braveName + "はぼーっとしている。");
                                }
                                else
                                {
                                    Console.WriteLine(braveName + "は身をまもっている。");
                                }
                                break;
                            case SILENCED:
                                Console.WriteLine(braveName + "は身をまもっている。");
                                break;
                            case CURSED:
                                random = rnd.Next(4);
                                if (random > 0)
                                {
                                    Console.WriteLine(braveName + "は金縛りに遭って動けない。");
                                }
                                else
                                {
                                    Console.WriteLine(braveName + "は身をまもっている。");
                                }
                                break;
                        }
                        break;
                    case 4:
                        Console.WriteLine("使用するアイテムは?");
                        printArray(braveItems);
                        do
                        {
                            command = ReadInt(braveItems.Length);
                        } while (command == -1);
                        switch (currentState)
                        {
                            case NORMAL:
                                Console.WriteLine(braveName + "は" + braveItems[command] + "を使用した。");
                                break;
                            case POISON:
                                Console.WriteLine(braveName + "は" + braveItems[command] + "を使用した。");
                                Console.WriteLine(braveName + "は毒のダメージを受けた。");
                                break;
                            case PARALYZED:
                                Console.WriteLine(braveName + "は体がしびれて動けない。");
                                break;
                            case CONFUSED:
                                random = rnd.Next(4);
                                if (random == 3)
                                {
                                    Console.WriteLine(braveName + "はわけが分からなくなってしまっている。");
                                }
                                else if (random == 2)
                                {
                                    Console.WriteLine(braveName + "は自分を攻撃した。");
                                }
                                else if (random == 1)
                                {
                                    Console.WriteLine(braveName + "はぼーっとしている。");
                                }
                                else
                                {
                                    Console.WriteLine(braveName + "は身をまもっている。");
                                }
                                break;
                            case SILENCED:
                                Console.WriteLine(braveName + "は" + braveItems[command] + "を使用した。");
                                break;
                            case CURSED:
                                random = rnd.Next(4);
                                if (random > 0)
                                {
                                    Console.WriteLine(braveName + "は金縛りに遭って動けない。");
                                }
                                else
                                {
                                    Console.WriteLine(braveName + "は" + braveItems[command] + "を使用した。");
                                }
                                break;
                        }
                        break;
                    case 5:
                        switch (currentState)
                        {
                            case NORMAL:
                                Console.WriteLine(braveName + "は戦闘から逃げ出した。");
                                return;
                            case POISON:
                                Console.WriteLine(braveName + "は戦闘から逃げ出した。");
                                return;
                            case PARALYZED:
                                Console.WriteLine(braveName + "は体がしびれて動けない。");
                                break;
                            case CONFUSED:
                                random = rnd.Next(4);
                                if (random == 3)
                                {
                                    Console.WriteLine(braveName + "はわけが分からなくなってしまっている。");
                                }
                                else if (random == 2)
                                {
                                    Console.WriteLine(braveName + "は自分を攻撃した。");
                                }
                                else if (random == 1)
                                {
                                    Console.WriteLine(braveName + "はぼーっとしている。");
                                }
                                else
                                {
                                    Console.WriteLine(braveName + "は戦闘から逃げ出した。");
                                    return;
                                }
                                break;
                            case SILENCED:
                                Console.WriteLine(braveName + "は戦闘から逃げ出した。");
                                return;
                            case CURSED:
                                random = rnd.Next(4);
                                if (random > 0)
                                {
                                    Console.WriteLine(braveName + "は金縛りに遭って動けない。");
                                }
                                else
                                {
                                    Console.WriteLine(braveName + "は戦闘から逃げ出した。");
                                    return;
                                }
                                break;
                        }
                        break;
                }
                enemyCommand = rnd.Next(6);
                Console.WriteLine(monsterName + "は" + monsterSkill[enemyCommand] + "スキルを使用した。");
                currentState = enemyCommand;
                switch (enemyCommand)
                {
                    case NORMAL:
                        Console.WriteLine(braveName + "は正常状態になった。");
                        break;
                    case POISON:
                        Console.WriteLine(braveName + "は毒状態になった。");
                        break;
                    case PARALYZED:
                        Console.WriteLine(braveName + "は麻痺状態になった。");
                        break;
                    case CONFUSED:
                        Console.WriteLine(braveName + "は混乱状態になった。");
                        break;
                    case SILENCED:
                        Console.WriteLine(braveName + "は沈黙状態になった。");
                        break;
                    case CURSED:
                        Console.WriteLine(braveName + "は呪い状態になった。");
                        break;
                }
                command = -1;
            }
        }
        public static int ReadInt(int max)
        {//数値が入力されているか確認
            try
            {
                int num = int.Parse(Console.ReadLine());
                if (num >= max)
                {
                    Console.WriteLine("コマンド選んで!");
                    return -1;
                }
                return num;
            }
            catch (Exception)
            {
                Console.WriteLine("数字入力して!");
                return -1;
            }
        }
        public static void printArray(String[] str)
        {
            for (int i = 0; i < str.Length; i++)
            {
                Console.WriteLine(i + " : " + str[i]);
            }
        }
    }
}

これで一応は動作しますが、かなり分かり難いコードになっています。
例えばこれでリリースしたあと、プロデューサーから次回作は、面白そうだから以下の状態異常パターンを追加しようと依頼を受けたとします。

statetable_add.JPG

このプログラムでは、Switch文全体にわたって修正する必要が出てきて大変です。コードも増加した状態数の分増大しさらに分かり難いコードとなってしまいます。
というわけでStateパターンの出番です。

Stateパターンで実装

Stateパターンで実装すると以下のようになります。

クラス図

StatePattern.JPG

クラス一覧

名 前 解 説
IState 状態を表すインターフェース
Normal 正常状態のクラス
Poison 毒状態のクラス
Sleep 眠り状態のクラス
Paralyzed 麻痺状態のクラス
Confused 混乱状態のクラス
Silence 沈黙状態のクラス
Curse 呪い状態のクラス
IAlly 味方インターフェース 
上表のContextインターフェースに該当するインターフェース
Brave 勇者クラス
味方インターフェースを実装する
Fight 戦闘をシミュレートするメインクラス

ソースコード

IStateインターフェース

状態変化を表すためのインターフェースです。
状態変更の度に状態のインスタンスを作成するのは非効率なのでSingletonパターンを実装しています。
各コマンドのメソッドを定義します。

IState.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace StatePattern
{
    interface  IState//状態を表すインターフェース
    {
        void AttackCMD(String name, String target);//攻撃コマンド
        void MagicCMD(String name, String magic, String target);//魔法コマンド
        void SkillCMD(String name, String skill, String target);//スキルコマンド
        void DefenceCMD(String name);//防御コマンド
        void ItemCMD(String name, String item);//アイテムコマンド
        int EscapeCMD(String name);//逃走コマンド
    }
}

Normalクラス

Stateを実装しています。
状態変更の度に状態のインスタンスを作成するのは非効率なのでSingletonパターンを実装しています。
正常な状態の各コマンドを表現しています。

Normal.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace StatePattern
{
    class Normal : IState//通常の状態
    {
        /*
        状態変更の度にインスタンス作成されるとメモリの無駄になるため
        Sineltonを使用する
        */
        private static Normal singleton = new Normal();
        private Normal()
        {
        }
        public static IState GetInstance()
        {
            return singleton;
        }

        //通常の状態の各戦闘コマンド
        public void AttackCMD(String name, String target)
        {
            Console.WriteLine(name + "は" + target + "に攻撃した。");
        }
        public void MagicCMD(String name, String magic, String target)
        {
            Console.WriteLine(name + "は" + target + "に" + magic + "をとなえた。");
        }
        public void SkillCMD(String name, String skill, String target)
        {
            Console.WriteLine(name + "は" + target + "に" + skill + "をした。");
        }
        public void DefenceCMD(String name)
        {
            Console.WriteLine(name + "は身をまもっている。");
        }
        public void ItemCMD(String name, String item)
        {
            Console.WriteLine(name + "は" + item + "を使用した。");
        }
        public int EscapeCMD(String name)
        {
            Console.WriteLine(name + "は戦闘から逃げ出した。");
            return 1;
        }
        public override String ToString()
        {
            return "通常";
        }
    }
}


Poisonクラス

Stateを実装しています。
状態変更の度に状態のインスタンスを作成するのは非効率なのでSingletonパターンを実装しています。
毒状態なので各コマンドの最後には毒ダメージを受けます。

Poison.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace StatePattern
{
    class Poison : IState//毒の状態
    {
        /*
        状態変更の度にインスタンス作成されるとメモリの無駄になるため
        Sineltonを使用する
        */
        private static Poison singleton = new Poison();
        private Poison()
        {
        }
        public static IState getInstance()
        {
            return singleton;
        }

        //毒の状態の各戦闘コマンド(コマンド実行後は必ずダメージを受ける)
        public void AttackCMD(String name, String target)
        {
            Console.WriteLine(name + "は" + target + "に攻撃した。");
            Console.WriteLine(name + "は毒のダメージを受けた。");
        }
        public void MagicCMD(String name, String magic, String target)
        {
            Console.WriteLine(name + "は" + target + "に" + magic + "をとなえた。");
            Console.WriteLine(name + "は毒のダメージを受けた。");
        }
        public void SkillCMD(String name, String skill, String target)
        {
            Console.WriteLine(name + "は" + target + "に" + skill + "をした。");
            Console.WriteLine(name + "は毒のダメージを受けた。");
        }
        public void DefenceCMD(String name)
        {
            Console.WriteLine(name + "は身をまもっている。");
            Console.WriteLine(name + "は毒のダメージを受けた。");
        }
        public void ItemCMD(String name, String item)
        {
            Console.WriteLine(name + "は" + item + "を使用した。");
            Console.WriteLine(name + "は毒のダメージを受けた。");
        }
        public int EscapeCMD(String name)
        {
            Console.WriteLine(name + "は戦闘から逃げ出した。");
            return 1;
        }
        public override String ToString()
        {
            return "毒";
        }
    }
}

Sleepクラス

Stateを実装しています。
状態変更の度に状態のインスタンスを作成するのは非効率なのでSingletonパターンを実装しています。
眠り状態なので各コマンドは実行できませんが、体力が回復します。

Sleep.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace StatePattern
{
    class Sleep : IState
    {
        /*
        状態変更の度にインスタンス作成されるとメモリの無駄になるため
        Sineltonを使用する
        */
        private static Sleep singleton = new Sleep();
        private Sleep()
        {
        }
        public static IState getInstance()
        {
            return singleton;
        }

        //ねむりの状態の各戦闘コマンド(ねむって何もできないが、体力が回復する)
        public void AttackCMD(String name, String target)
        {
            Console.WriteLine(name + "はねむっている。");
            Console.WriteLine(name + "は体力が回復した。");
        }
        public void MagicCMD(String name, String magic, String target)
        {
            Console.WriteLine(name + "はねむっている。");
            Console.WriteLine(name + "は体力が回復した。");
        }
        public void SkillCMD(String name, String skill, String target)
        {
            Console.WriteLine(name + "はねむっている。");
            Console.WriteLine(name + "は体力が回復した。");
        }
        public void DefenceCMD(String name)
        {
            Console.WriteLine(name + "はねむっている。");
            Console.WriteLine(name + "は体力が回復した。");
        }
        public void ItemCMD(String name, String item)
        {
            Console.WriteLine(name + "はねむっている。");
            Console.WriteLine(name + "は体力が回復した。");
        }
        public int EscapeCMD(String name)
        {
            Console.WriteLine(name + "はねむっている。");
            Console.WriteLine(name + "は体力が回復した。");
            return 0;
        }
        public override String ToString()
        {
            return "眠り";
        }
    }
}

Paralyzedクラス

Stateを実装しています。
状態変更の度に状態のインスタンスを作成するのは非効率なのでSingletonパターンを実装しています。
麻痺状態なので各コマンドは実行できません。

Paralyzed.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace StatePattern
{
    class Paralyzed : IState
    {
        /*
        状態変更の度にインスタンス作成されるとメモリの無駄になるため
        Sineltonを使用する
        */
        private static Paralyzed singleton = new Paralyzed();
        private Paralyzed()
        {
        }
        public static IState getInstance()
        {
            return singleton;
        }

        //麻痺の状態の各戦闘コマンド(動けないので何もできない。)
        public void AttackCMD(String name, String target)
        {
            Console.WriteLine(name + "は体がしびれて動けない。");
        }
        public void MagicCMD(String name, String magic, String target)
        {
            Console.WriteLine(name + "は体がしびれて動けない。");
        }
        public void SkillCMD(String name, String skill, String target)
        {
            Console.WriteLine(name + "は体がしびれて動けない。");
        }
        public void DefenceCMD(String name)
        {
            Console.WriteLine(name + "は体がしびれて動けない。");
        }
        public void ItemCMD(String name, String item)
        {
            Console.WriteLine(name + "は体がしびれて動けない。");
        }
        public int EscapeCMD(String name)
        {
            Console.WriteLine(name + "は体がしびれて動けない。");
            return 0;
        }
        public override String ToString()
        {
            return "麻痺";
        }
    }
}

Confusedクラス

Stateを実装しています。
状態変更の度に状態のインスタンスを作成するのは非効率なのでSingletonパターンを実装しています。
混乱しているため、4分の1の確率でコマンドが成功します。

Confused.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace StatePattern
{
    class Confused : IState
    {
        /*
        状態変更の度にインスタンス作成されるとメモリの無駄になるため
        Sineltonを使用する
        */
        private static Confused singleton = new Confused();
        private Confused()
        {
        }
        public static IState getInstance()
        {
            return singleton;
        }

        //混乱の状態の各戦闘コマンド(ランダムで行動する)
        private static Random rnd = new Random();

        public void AttackCMD(String name, String target)
        {
            int random = rnd.Next(4);
            if (random == 3)
            {
                Console.WriteLine(name + "はわけが分からなくなってしまっている。");
            }
            else if (random == 2)
            {
                Console.WriteLine(name + "は自分を攻撃した。");
            }
            else if (random == 1)
            {
                Console.WriteLine(name + "はぼーっとしている。");
            }
            else
            {
                Console.WriteLine(name + "は" + target + "に攻撃した。");
            }
        }
        public void MagicCMD(String name, String magic, String target)
        {
            int random = rnd.Next(4);
            if (random == 3)
            {
                Console.WriteLine(name + "はわけが分からなくなってしまっている。");
            }
            else if (random == 2)
            {
                Console.WriteLine(name + "は自分を攻撃した。");
            }
            else if (random == 1)
            {
                Console.WriteLine(name + "はぼーっとしている。");
            }
            else
            {
                Console.WriteLine(name + "は" + target + "に" + magic + "をとなえた。");
            }
        }
        public void SkillCMD(String name, String skill, String target)
        {
            int random = rnd.Next(4);
            if (random == 3)
            {
                Console.WriteLine(name + "はわけが分からなくなってしまっている。");
            }
            else if (random == 2)
            {
                Console.WriteLine(name + "は自分を攻撃した。");
            }
            else if (random == 1)
            {
                Console.WriteLine(name + "はぼーっとしている。");
            }
            else
            {
                Console.WriteLine(name + "は" + target + "に" + skill + "をした。");
            }
        }
        public void DefenceCMD(String name)
        {
            int random = rnd.Next(4);
            if (random == 3)
            {
                Console.WriteLine(name + "はわけが分からなくなってしまっている。");
            }
            else if (random == 2)
            {
                Console.WriteLine(name + "は自分を攻撃した。");
            }
            else if (random == 1)
            {
                Console.WriteLine(name + "はぼーっとしている。");
            }
            else
            {
                Console.WriteLine(name + "は身をまもっている。");
            }
        }
        public void ItemCMD(String name, String item)
        {
            int random = rnd.Next(4);
            if (random == 3)
            {
                Console.WriteLine(name + "はわけが分からなくなってしまっている。");
            }
            else if (random == 2)
            {
                Console.WriteLine(name + "は自分を攻撃した。");
            }
            else if (random == 1)
            {
                Console.WriteLine(name + "はぼーっとしている。");
            }
            else
            {
                Console.WriteLine(name + "は" + item + "を使用した。");
            }
        }
        public int EscapeCMD(String name)
        {
            int random = rnd.Next(4);
            if (random == 3)
            {
                Console.WriteLine(name + "はわけが分からなくなってしまっている。");
                return 0;
            }
            else if (random == 2)
            {
                Console.WriteLine(name + "は自分を攻撃した。");
                return 0;
            }
            else if (random == 1)
            {
                Console.WriteLine(name + "はぼーっとしている。");
                return 0;
            }
            else
            {
                Console.WriteLine(name + "は戦闘から逃げ出した。");
                return 1;
            }
        }
        public override String ToString()
        {
            return "混乱";
        }
    }
}

Silenceクラス

Stateを実装しています。
状態変更の度に状態のインスタンスを作成するのは非効率なのでSingletonパターンを実装しています。
沈黙状態のため魔法は使えません。

Silence.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace StatePattern
{
    class Silence : IState
    {
        /*
        状態変更の度にインスタンス作成されるとメモリの無駄になるため
        Sineltonを使用する
        */
        private static Silence singleton = new Silence();
        private Silence()
        {
        }
        public static IState getInstance()
        {
            return singleton;
        }

        //沈黙の状態の各戦闘コマンド(魔法が使えない。)
        public void AttackCMD(String name, String target)
        {
            Console.WriteLine(name + "は" + target + "に攻撃した。");
        }
        public void MagicCMD(String name, String magic, String target)
        {
            Console.WriteLine(name + "は魔法を唱えることができない。");
        }
        public void SkillCMD(String name, String skill, String target)
        {
            Console.WriteLine(name + "は" + target + "に" + skill + "をした。");
        }
        public void DefenceCMD(String name)
        {
            Console.WriteLine(name + "は身をまもっている。");
        }
        public void ItemCMD(String name, String item)
        {
            Console.WriteLine(name + "は" + item + "を使用した。");
        }
        public int EscapeCMD(String name)
        {
            Console.WriteLine(name + "は戦闘から逃げ出した。");
            return 1;
        }
        public override String ToString()
        {
            return "沈黙";
        }
    }
}

Curseクラス

Stateを実装しています。
状態変更の度に状態のインスタンスを作成するのは非効率なのでSingletonパターンを実装しています。
呪われているので4分の3の確率で金縛りに遭い動けません。

Curse.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace StatePattern
{
    class Curse : IState
    {
        /*
        状態変更の度にインスタンス作成されるとメモリの無駄になるため
        Sineltonを使用する
        */
        private static Curse singleton = new Curse();
        private Curse()
        {
        }
        public static IState getInstance()
        {
            return singleton;
        }

        //混乱の状態の各戦闘コマンド(ランダムで行動する)
        private static Random rnd = new Random();

        public void AttackCMD(String name, String target)
        {
            int random = rnd.Next(4);
            if (random > 0)
            {
                Console.WriteLine(name + "は金縛りに遭って動けない。");
            }
            else
            {
                Console.WriteLine(name + "は" + target + "に攻撃した。");
            }
        }
        public void MagicCMD(String name, String magic, String target)
        {
            int random = rnd.Next(4);
            if (random > 0)
            {
                Console.WriteLine(name + "は金縛りに遭って動けない。");
            }
            else
            {
                Console.WriteLine(name + "は" + target + "に" + magic + "をとなえた。");
            }
        }
        public void SkillCMD(String name, String skill, String target)
        {
            int random = rnd.Next(4);
            if (random > 0)
            {
                Console.WriteLine(name + "は金縛りに遭って動けない。");
            }
            else
            {
                Console.WriteLine(name + "は" + target + "に" + skill + "をした。");
            }
        }
        public void DefenceCMD(String name)
        {
            int random = rnd.Next(4);
            if (random > 0)
            {
                Console.WriteLine(name + "は金縛りに遭って動けない。");
            }
            else
            {
                Console.WriteLine(name + "は身をまもっている。");
            }
        }
        public void ItemCMD(String name, String item)
        {
            int random = rnd.Next(4);
            if (random > 0)
            {
                Console.WriteLine(name + "は金縛りに遭って動けない。");
            }
            else
            {
                Console.WriteLine(name + "は" + item + "を使用した。");
            }
        }
        public int EscapeCMD(String name)
        {
            int random = rnd.Next(4);
            if (random > 0)
            {
                Console.WriteLine(name + "は金縛りに遭って動けない。");
                return 0;
            }
            else
            {
                Console.WriteLine(name + "は戦闘から逃げ出した。");
                return 1;
            }
        }
        public override String ToString()
        {
            return "呪い";
        }
    }
}

IAllyインターフェース

味方を表すインターフェースです。
各状態インスタンスを受け取り、状態毎の処理を委任します。
changeStateメソッドを呼び出すことで状態が変遷します。

IAlly.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace StatePattern
{
    interface IAlly//味方を表すインターフェース
    {
        void SetMagic(String[] magic);//魔法を登録
        void SetSkill(String[] skill);//スキルを登録
        void SetItem(String[] item);//アイテムを登録
        void ChangeState(IState state);//状態変化
        void Attack(String target);//攻撃コマンド実行
        void Magic(int magic, String target);//魔法コマンド実行
        void Skill(int skill, String target);//スキルコマンド実行
        void Defence();//防御コマンド実行
        void Item(int item);//アイテムコマンド実行
        int Escape();//逃走コマンド実行
    }
}

Braveクラス

Allyインターフェースを実装した勇者を表すクラスです。

Brave.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace StatePattern
{
    class Brave : IAlly//勇者クラス
    {
        private String name;//名前
        private String[] magic;//使える魔法
        private String[] skill;//使えるスキル
        private String[] item;//保有アイテム
        private IState state;//状態

        public Brave(String name)
        {
            this.name = name;
            this.state = Normal.GetInstance();
        }

        public void SetMagic(String[] magic)
        {
            this.magic = magic;
        }
        public void SetSkill(String[] skill)
        {
            this.skill = skill;
        }
        public void SetItem(String[] item)
        {
            this.item = item;
        }
        public void ChangeState(IState state)
        {
            this.state = state;
            Console.WriteLine(this.name + "は" + this.state + "状態になった。");
        }
        public void Attack(String target)
        {
            state.AttackCMD(name, target);
        }
        public void Magic(int magicNum, String target)
        {
            state.MagicCMD(name, magic[magicNum], target);
        }
        public void Skill(int skillNum, String target)
        {
            state.SkillCMD(name, skill[skillNum], target);
        }
        public void Defence()
        {
            state.DefenceCMD(name);
        }
        public void Item(int itemNum)
        {
            state.ItemCMD(name, item[itemNum]);
        }
        public int Escape()
        {
            return state.EscapeCMD(name);
        }
    }
}

Fightクラス

状態異常技をと勇者の戦いを表すメインクラスです。

Fight.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace StatePattern
{
    class Fight
    {
        static void Main(string[] args)
        {
            Random rnd = new Random();

            IAlly nakao = new Brave("中尾");
            String[] nakaoMagic = { "火球", "雷大雲" };
            String[] nakaoSkill = { "一閃突", "ナイフ投げ" };
            String[] nakaoItem = { "薬草" };
            nakao.SetMagic(nakaoMagic);
            nakao.SetSkill(nakaoSkill);
            nakao.SetItem(nakaoItem);

            String monsterName = "田町の門番";
            String[] monsterSkill = { "毒煙玉", "子守唄", "スタンガン", "恐喝", "ガン付", "呪い" };

            IState[] states = { Poison.getInstance(), Sleep.getInstance(), Paralyzed.getInstance(), Confused.getInstance(), Silence.getInstance(), Curse.getInstance(), Poison.getInstance() };

            int escape = 0;

            Console.WriteLine(monsterName + "が現れた。");

            while (true)
            {
                Console.WriteLine("どうする? [0:戦う][1:魔法][2:特技][3:防御][4:道具][5:逃走]");
                int command;
                do
                {
                    command = ReadInt(states.Length);
                } while (command == -1);
                switch (command)
                {
                    case 0:
                        nakao.Attack(monsterName);
                        break;
                    case 1:
                        Console.WriteLine("使用する魔法は?");
                        printArray(nakaoMagic);
                        do
                        {
                            command = ReadInt(nakaoMagic.Length);
                        } while (command == -1);
                        nakao.Magic(command, monsterName);
                        break;
                    case 2:
                        Console.WriteLine("使用するスキルは?");
                        printArray(nakaoSkill);
                        do
                        {
                            command = ReadInt(nakaoSkill.Length);
                        } while (command == -1);
                        nakao.Skill(command, monsterName);
                        break;
                    case 3:
                        nakao.Defence();
                        break;
                    case 4:
                        Console.WriteLine("使用するアイテムは?");
                        printArray(nakaoItem);
                        do
                        {
                            command = ReadInt(nakaoItem.Length);
                        } while (command == -1);
                        nakao.Item(command);
                        break;
                    case 5:
                        escape = nakao.Escape();
                        break;
                }
                if (escape == 1)
                {
                    return;
                }
                int enemyCommand = rnd.Next(6);
                Console.WriteLine(monsterName + "は" + monsterSkill[enemyCommand] + "スキルを使用した。");
                nakao.ChangeState(states[enemyCommand]);
                command = -1;
            }
        }
        public static int ReadInt(int max)
        {//数値が入力されているか確認
            try
            {
                int num = int.Parse(Console.ReadLine());
                if (num >= max)
                {
                    Console.WriteLine("コマンド選んで!");
                    return -1;
                }
                return num;
            }
            catch (Exception)
            {
                Console.WriteLine("数字入力して!");
                return -1;
            }
        }
        public static void printArray(String[] str)
        {
            for (int i = 0; i < str.Length; i++)
            {
                Console.WriteLine(i + " : " + str[i]);
            }
        }
    }
}

実行結果

実行結果は以下のようになります。
※ランダム要素があるため、多少異なる場合があります。

田町の門番が現れた。
どうする? [0:戦う][1:魔法][2:特技][3:防御][4:道具][5:逃走]
0
中尾は田町の門番に攻撃した。
田町の門番は恐怖の衝撃スキルを使用した。
中尾は混乱状態になった。
どうする? [0:戦う][1:魔法][2:特技][3:防御][4:道具][5:逃走]
5
中尾は自分を攻撃した。
田町の門番は電気ショックスキルを使用した。
中尾は麻痺状態になった。
どうする? [0:戦う][1:魔法][2:特技][3:防御][4:道具][5:逃走]
5
中尾は体がしびれて動けない。
田町の門番はガンにらみスキルを使用した。
中尾は沈黙状態になった。
どうする? [0:戦う][1:魔法][2:特技][3:防御][4:道具][5:逃走]
5
中尾は戦闘から逃げ出した。

考察

Stateパターンを使用することで、状態別の振る舞いを分かり易く表現することができました。ここで先ほどと同じように次回作へ向けての更改依頼を受けても既存のプログラムを大きく改変することなく、新たに追加された状態異常のクラスを追加することで対応できます。

このデザインパターンは機械やGUIの設計で有効なパターンといえます。
例えば、乗車券の券売機のプログラムで、普通車両、指定席、特急、子供等で、料金が違ったり発券方法が違ったりしますが、それぞれのモードを「状態」で表すことで分かり易くプログラミングができます。
GUIプログラミングの例でいえば、あるウィンドウの振る舞いをログインするユーザ毎に変える必要がある場合、個々のユーザでログインした「状態」で表すことで分かり易くプログラミングができます。

状態によって処理の振る舞いを状態クラスに委任しているという動きはアルゴリズムを委譲して切り替えるStrategyパターンと似ており、実際に第10回Strategyパターンにあるクラス図を見ると構造がよく似ています。

  • Strategyパターン
    StrategyPattern.JPG

  • Stateパターン
    StatePattern.JPG

Strategyパターンはあるメソッドの目的「ゴール」は同じだが、処理内容「戦略」が違い使い分けたい場合に、Stateパターンはメソッド「ユーザ操作」は同じだが、状態によって処理内容や出力「振る舞い」が違う場合に使うといいと思います。

6
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
1