【デザインパターン】ストラテジーパターンについて

この記事の概要


対象者:デザインパターンを勉強したい方
内容:ストラテジーパターンについての説明と例

1.はじめに

こんにちは。エンジニアの許です。

最近GoF(Gang of Four)デザインパターンについて勉強していますので、その中のストラテジーパターンについて紹介したいと思います。

2.Strategy パターンとは

Strategy とは英語で「戦略」を意味する言葉です。
Strategy パターンではいろんな「戦略」を事前に定義して、場面によって「戦略」の切り替えが簡単に行えるようになるデザインパターンです。
プログラミングの領域において、この戦略はアルゴリズムです。
つまり、アルゴリズムを実行時に選択することができるデザインパターンです。

Strategy パターンを示す図(UML)

フリー百科事典『ウィキペディア(Wikipedia)』により引用

3.サンプルケース

それではさっそくStrategy パターンを利用して、サンプルコードを作ってみましょう。
まずは、以下の要件が来たと仮定します。

要件

1.ターン制二人対戦ゲーム
2.プレイヤーは「HP」、「MP」、「力」と「防御力」属性を持っている
3.各プレイヤーはスキルを一つ持つことが可能
4.スキルは以下になります。

「パンチング」:相手に強力なパンチを与えて、「自身の力 - 相手の防御力」のダメージを与える。
「エネルギーボルト」:MP5ポイントを消費して、防御力を無視するエネルギーの凝縮体を発射して、「消費MP x 2」のダメージを与える。

上の要件を受けた私が作った最初のコードは下になります。

var player1 = new Player("俺は戦士", Skill.Punching);
var player2 = new Player("俺はメイジ", Skill.EnergyBall);
new Battle(player1, player2).Start();

public enum Skill
{
    Punching,
    EnergyBall
}

public class Player
{
    public int Hp { get; private set; } = 30;
    public int Mp { get; private set; } = 30;
    public int Strength { get; } = 30;
    public int Defense { get; } = 15;
    public string Name { get; init; }
    public Skill Skill { get; init; }

    public Player(string name, Skill skill)
    {
        this.Name = name;
        this.Skill = skill;
    }

    public void Attack(Player targetPlayer)
    {
        switch (this.Skill)
        {
            case Skill.Punching:
                Console.WriteLine($"{this.Name} が {targetPlayer.Name}に対して、「パンチング」スキルを発動した!");
                targetPlayer.LostHp(this.Strength - targetPlayer.Defense);
                break;
            
            case Skill.EnergyBall:
                Console.WriteLine($"{this.Name} が {targetPlayer.Name}に対して、「エネルギーボルト」スキルを発動した!");
                LostMp(5);
                targetPlayer.LostHp(5 * 2);
                break;
        }

        Console.WriteLine($"{targetPlayer.Name} が攻撃を受けて、残りのHpは{targetPlayer.Hp}。");
    }

    public void LostMp(int mp) => this.Mp = (this.Mp > mp) ? this.Mp - mp : 0;

    public void LostHp(int hp) => this.Hp = (this.Hp > hp) ? this.Hp - hp : 0;
}

public class Battle
{
    private List<Player> Players = new List<Player>();

    public Battle(Player player1, Player player2)
    {
        this.Players.Add(player1);
        this.Players.Add(player2);
    }

    public void Start()
    {
        var turnIndex = 1;
        while(Players.All(player => (player.Hp > 0)))
        {
            turnIndex = (turnIndex == 0) ? 1 : 0;
            PlayerTurn(turnIndex);
        }

        Console.WriteLine($"試合終了!勝者は{this.Players[turnIndex].Name} !!!");
    }

    private void PlayerTurn(int playerIndex)
    {
        var attackingPlayer = this.Players[playerIndex];
        var attackedPlayer = this.Players[(playerIndex == 0) ? 1 : 0] ;
        Console.WriteLine($"{attackingPlayer.Name} のターン!");
        attackingPlayer.Attack(attackedPlayer);
    }
}

バトル結果は下になります。

俺は戦士 のターン!
俺は戦士 が 俺はメイジに対して、「パンチング」スキルを発動した!
俺はメイジ が攻撃を受けて、残りのHpは15。
俺はメイジ のターン!
俺はメイジ が 俺は戦士に対して、「エネルギーボルト」スキルを発動した!
俺は戦士 が攻撃を受けて、残りのHpは20。
俺は戦士 のターン!
俺は戦士 が 俺はメイジに対して、「パンチング」スキルを発動した!
俺はメイジ が攻撃を受けて、残りのHpは0。
試合終了!勝者は俺は戦士 !!!

問題なく動きましたね。
ここで注目したいところは「Attack」メソッドです。
スキルはまだ二つしかないので、switch使っても、まあまあな感じですね。
でも、どのスキルのダメージをどう計算するか、MP消費はどうなっているかが全部一つのメソッドに書いているのがどうですかね。
※スキルの名前まで覚えないと!

要望殺到

ある日、スキルを五つ増やしたいの要望が来ました。
そして、修正後の「Attack」メソッドが以下になりました。
スキル五つしか追加していないのに、もう絶望的ですね。

public void Attack(Player targetPlayer)
{
    switch (this.Skill)
    {
        case Skill.Punching:
            Console.WriteLine($"{this.Name} が {targetPlayer.Name}に対して、「パンチング」スキルを発動した!");
            targetPlayer.LostHp(this.Strength - targetPlayer.Defense);
            break;
        case Skill.EnergyBall:
            Console.WriteLine($"{this.Name} が {targetPlayer.Name}に対して、「エネルギーボルト」スキルを発動した!");
            LostMp(5);
            targetPlayer.LostHp(5 * 2);
            break;
        case Skill.新規スキル1:
            ・・・
            break;
        case Skill.新規スキル2:
            ・・・
            break;
        case Skill.新規スキル3:
            ・・・
            break;
        case Skill.新規スキル4:
            ・・・
            break;
        case Skill.新規スキル5:
            ・・・
            break;
    }
    Console.WriteLine($"{targetPlayer.Name} が攻撃を受けて、残りのHpは{targetPlayer.Hp}。");
}

ここでStrategy パターンの出番です。
Strategy パターンについての説明はまだ覚えていますでしょうか。
「いろんな「戦略」を事前に定義して、場面によって「戦略」の切り替えが簡単に行えるようになるデザインパターンです。」
今のケースで、「スキル」がまさに説明中の「戦略」とみてもいいでしょう。
なので、やる必要があるのは「スキル」を個別に定義して、「プレイヤー」に渡すような修正です。
つまり、「プレイヤー」はスキルを使うだけで、スキルの詳細を知らなくてもいいです。

リファクタリング後のコードは以下になります。
変更点は以下です。
1.Skillをインターフェースに変更
2.PunchingとEnergyBallをSkillインターフェースを実現したクラスに変更
3.PlayerのSkill型のプロパティはコンストラクタからSkillインターフェースを実現したインスタンスを受け取る
4.PlayerのAttackメソッドではSkillのAttackメソッドを呼び出すだけ

var player1 = new Player("俺は戦士", new Punching());
var player2 = new Player("俺はメイジ", new EnergyBall());
new Battle(player1, player2).Start();

public interface Skill
{
    public void Attack(Player attackingPlayer, Player attackedPlayer);
    public string GetSkillName();
}

public class Punching : Skill
{
    public void Attack(Player attackingPlayer, Player attackedPlayer)
    {
        attackedPlayer.LostHp(attackingPlayer.Strength - attackedPlayer.Defense);
    }

    public string GetSkillName() => "パンチング"; 
}

public class EnergyBall : Skill
{
    public void Attack(Player attackingPlayer, Player attackedPlayer)
    {
        attackingPlayer.LostMp(5);
        attackedPlayer.LostHp(5 * 2);
    }

    public string GetSkillName() => "エネルギーボルト";
}

public class Player
{
    public int Hp { get; private set; } = 30;
    public int Mp { get; private set; } = 30;
    public int Strength { get; } = 30;
    public int Defense { get; } = 15;
    public string Name { get; init; }
    public Skill Skill { get; init; }

    public Player(string name, Skill skill)
    {
        this.Name = name;
        this.Skill = skill;
    }

    public void Attack(Player targetPlayer)
    {
        Console.WriteLine($"{this.Name} が {targetPlayer.Name}に対して、「{this.Skill.GetSkillName()}」スキルを発動した!");
        this.Skill.Attack(this, targetPlayer);
        Console.WriteLine($"{targetPlayer.Name} が攻撃を受けて、残りのHpは{targetPlayer.Hp}。");
    }

    public void LostMp(int mp) => this.Mp = (this.Mp > mp) ? this.Mp - mp : 0;

    public void LostHp(int hp) => this.Hp = (this.Hp > hp) ? this.Hp - hp : 0;
}
// ※Battleクラスは変化ないです。

どうでしょう。これで新しいスキルを追加する時は「新しいスキルクラス」一つを追加して、利用したい時、プレイヤーに渡すだけで済みます!

さっそく、「元気玉」というスキルを追加してみましょう。
スキル効果は「すべてのMPを消費して、相手に100のダメージを与える」です。

public class SpiritBomb: Skill
{
    public void Attack(Player attackingPlayer, Player attackedPlayer)
    {
        attackingPlayer.LostMp(attackingPlayer.Mp);
        attackedPlayer.LostHp(100);
    }

    public string GetSkillName() => "★元気玉★";
}

var player1 = new Player("俺は戦士", new Punching());
var player2 = new Player("俺は悟空", new SpiritBomb());
new Battle(player1, player2).Start();

1.SpiritBombクラス追加
2.新規追加したSpiritBombをプレイヤーに渡す
これで、修正完了!

バトル結果

俺は戦士 のターン!
俺は戦士 が 俺は悟空に対して、「パンチング」スキルを発動した!
俺は悟空 が攻撃を受けて、残りのHpは15。
俺は悟空 のターン!
俺は悟空 が 俺は戦士に対して、「★元気玉★」スキルを発動した!
俺は戦士 が攻撃を受けて、残りのHpは0。
試合終了!勝者は俺は悟空 !!!

4.おわりのご挨拶と資料リンク集

これでサンプルケースを利用して、Strategy パターンについて説明してみました。
それでは皆様、よき開発ライフをお送りください。

関連記事

プロジェクトストーリー

技術

コメント

この記事へのコメントはありません。

カテゴリー

TOP
TOP