💭

【C#】Strategy パターン

2022/12/17に公開

はじめに

オブジェクト指向プログラミングよくわからない!キョです。
最近デザインパターンについて、学び直していますので、
個人的に普段の業務でも使えそうと思うデザインパターンについて、紹介したいと思います。

1.Strategy パターンとは

Wikiの説明は以下になります。

Strategy パターン(ストラテジー -)は、コンピュータープログラミングの領域において、アルゴリズムを実行時に選択することができるデザインパターンである。

https://ja.wikipedia.org/wiki/Strategy_パターン

「techscore」さんの説明もすごくわかりやすいと思います。

普通にプログラミングしていると、メソッドの中に溶け込んだ形でアルゴリズムを実装してしまうことがよくあります。if 文などで分岐させることでアルゴリズムを変更するような方法です。Strategy パターンでは、戦略の部分を意識して別クラスとして作成するようにしています。戦略x部分を別クラスとして作成しておき、戦略を変更したい場合には、利用する戦略クラスを変更するという方法で対応します。Strategy パターンを利用することで、メソッドの中に溶け込んだ形のアルゴリズムより柔軟でメンテナンスしやすい設計となります。

https://www.techscore.com/tech/DesignPattern/Strategy

でも、説明だけ見ると「アルゴリズムを実行時に選択」と「戦略クラスを変更する」の意味がふわっとしか頭に入らないので、下ではサンプルコードを作りながら説明したいと思います。

2.サンプルコード

サンプルコードを作るために、まず要件を設定してみました。
例えば、会社で以下の要件を満足するゲームを作るとします。

要件

  1. 二人対戦ターン制対戦ゲーム
  2. プレイヤーは「HP、MP、力、知力、防御力」という属性がある
  3. プレイヤーは一つのスキルを持つ
  4. スキルは下記の二つスキルとする
「パンチング」:相手に強力なパンチを与えて、「自身の力 - 相手の防御力」のダメージを与える。
「エネルギーボルト」:MPを5ポイントを消費して、防御力を無視するエネルギーの凝縮体を発射して、「自身の知力 x 2」のダメージを与える。

Strategy パターン知らない私が最初に作ったプログラム

要件を受けて、まだStrategy パターンをしらなかった、私が作ったプログラムは以下になります。
注目したい部分はプレイヤーの「スキル」部分です。
スキルは列挙型で定義しています。
そして、スキルの

  • スキル名
  • ダメージ計算方法
  • 消費MP

は全部Playerクラスの「Attack」メソッドに定義して、
switchでプレイヤーが発動するスキルを判断しています。

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 Intelligence { get; } = 10;
	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)
	{
		var damage = 0;
		switch (this.Skill)
		{
			case Skill.Punching:
				Console.WriteLine($"「{this.Name}」が「{targetPlayer.Name}」に対して、「パンチング」スキルを発動した!");
				damage = this.Strength - targetPlayer.Defense;
				targetPlayer.LostHp(damage);
				Console.WriteLine($"「{this.Name}」が「{targetPlayer.Name}」に対して、{damage}のダメージを与えた!");
				break;

			case Skill.EnergyBall:
				Console.WriteLine($"「{this.Name}」が「{targetPlayer.Name}」に対して、「エネルギーボルト」スキルを発動した!");
				damage = this.Intelligence * 2;
				LostMp(5);
				targetPlayer.LostHp(damage);
				Console.WriteLine($"「{this.Name}」が「{targetPlayer.Name}」に対して、{damage}のダメージを与えた!");
				break;
		}
	}

	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);
		Console.WriteLine($"「{attackedPlayer.Name}」が攻撃を受けて、残りのHpは{attackedPlayer.Hp}。");
	}
}
対戦結果
「戦士」のターン!
「戦士」が「メイジ」に対して、「パンチング」スキルを発動した!
「戦士」が「メイジ」に対して、15のダメージを与えた!
「メイジ」が攻撃を受けて、残りのHpは15。
「メイジ」のターン!
「メイジ」が「戦士」に対して、「エネルギーボルト」スキルを発動した!
「メイジ」が「戦士」に対して、20のダメージを与えた!
「戦士」が攻撃を受けて、残りのHpは10。
「戦士」のターン!
「戦士」が「メイジ」に対して、「パンチング」スキルを発動した!
「戦士」が「メイジ」に対して、15のダメージを与えた!
「メイジ」が攻撃を受けて、残りのHpは0。
試合終了!勝者は「戦士」!!!

プログラムは正常に動きましたね。
でも、どのスキルのスキル名がどうなっているか、
ダメージをどう計算するか、MP消費どうなっているかが全部一つのメソッド「Attack」に書いていますので、
もし、後でスキルを追加したい要望が来た時、そのたびに、
この「Attack」メソッドを修正して、新しいスキルの分岐を追加しないといけないので、
修正が既存のすべてのスキルに影響を与える可能性がありますので、怖いですね。。。!

Strategy パターンを利用してリファクタリング

ここで、プレイヤーの「スキル」がまさにStrategyパターンの説明にあった

  • 実行時に選択するアルゴリズム
  • 戦略クラス

になります。
なので、この「スキル」を実行時に選択できるようにクラスとして抽出します。
リファクタリング後のプログラムは以下になります。

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)
	{
		var damage = attackingPlayer.Strength - attackedPlayer.Defense;
		attackedPlayer.LostHp(damage);
		Console.WriteLine($"「{attackingPlayer.Name}」が「{attackedPlayer.Name}」に対して、{damage}のダメージを与えた!");
	}

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

public class EnergyBall : Skill
{
	public void Attack(Player attackingPlayer, Player attackedPlayer)
	{
		var damage = attackingPlayer.Intelligence * 2;
		attackingPlayer.LostMp(5);
		attackedPlayer.LostHp(damage);
		Console.WriteLine($"「{attackingPlayer.Name}」が「{attackedPlayer.Name}」に対して、{damage}のダメージを与えた!");
	}
	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 Intelligence { get; } = 10;
	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);
	}

	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クラスは変更ないので割愛します。

リファクタリング後と前の変更点は以下です。

  1. Skillを列挙型からインターフェースに変更して、実現必要のメソッドを定義
  2. 具体的なスキルはSkillインターフェースを実現したクラスに変更して、スキルの「スキル名」、「ダメージ計算方法」、「消費MP」を各自のクラス内に記載
  3. Playerクラスの「Attack」メソッドでは、コンストラクタからもらった「スキル」のメソッドを呼び出すように修正

このStrategy パターンを利用したリファクタリングによって、
今後スキルを追加したい要望が来る時の改修を想像してみましょう。
例えば、「元気玉」スキルを追加する場合は以下の改修だけで行けるでしょう。

元気玉:みんなの元気を集めて、相手に100ダメージを与える!
var player2 = new Player("俺は悟空", new SpiritBomb());
public class SpiritBomb : Skill
{
	public void Attack(Player attackingPlayer, Player attackedPlayer)
	{
		var damage = 100;
		attackedPlayer.LostHp(damage);
		Console.WriteLine($"「{attackingPlayer.Name}」が「{attackedPlayer.Name}」に対して、{damage}のダメージを与えた!");
	}

	public string GetSkillName() => "元気玉!投げる(っ'-')っ>>>ブォン";
}
  1. 「元気玉」スキルクラスを追加
  2. プレイヤーに元気玉スキルを渡す

これだけで改修が完了!!!

対戦結果
「戦士」のターン!
「戦士」が「俺は悟空」に対して、「パンチング」スキルを発動した!
「戦士」が「俺は悟空」に対して、15のダメージを与えた!
「俺は悟空」が攻撃を受けて、残りのHpは15。
「俺は悟空」のターン!
「俺は悟空」が「戦士」に対して、「元気玉!投げる('-')>>>ブォン」スキルを発動した!
「俺は悟空」が「戦士」に対して、100のダメージを与えた!
「戦士」が攻撃を受けて、残りのHpは0。
試合終了!勝者は「俺は悟空」!!!

終わり

Strategyパターンについて説明してみました。
Strategyパターンを利用して、もっとメンテナンスしやすいプログラムを作ることができるでしょう!
誰かのお役に立てれば幸いです。

参考サイト

https://ja.wikipedia.org/wiki/Strategy_パターン
https://www.techscore.com/tech/DesignPattern/Strategy

Discussion