そもそも
デザインパターンとは、様々なプログラムで再利用できる汎用的な設計パターンのことです。 プログラマの世界では、様々なデザインパターンが知られていますが、その中でも特に有名なのが「GoFデザインパターン」です。
ある問題があって、こう言うときはこんな設計をすれば良いよね。
みたいなのがデザインパターンのイメージ。
Wikiで調べてみた
Strategy パターン(ストラテジー -)は、コンピュータープログラミングの領域において、アルゴリズムを実行時に選択することができるデザインパターンである。 Strategyパターンはアルゴリズムを記述するサブルーチンへの参照をデータ構造の内部に保持する。
???????
簡単な言葉で書いてあるのを探してみた
Strategyパターンは、場面によって行いたい処理を分けたい場合に、それらの処理を外部にカプセル化することで必要な場面で必要な処理を使い分けられるようにするためのデザインパターンです。
処理を行う部分とその処理を利用する部分を分けているので、より柔軟に変更を加えることができるようになります。
さっきよりわかりやすい。
処理を「外部にカプセル化」することで、処理を行う部分と処理を利用する部分を分ける。
のをしたいみたい。
実際のコード
実際にストラテジーパターンを適応する前と後をコードで表してみたいと思います。
イメージが掴みやすくなるように意識したので、細かい部分は目を瞑ってもらえると嬉しいです。
Player
が存在していて、Player
は魔法を1つだけ覚えている。
Player
は覚えている魔法名を表示するメソッドと、覚えている魔法のダメージを表示するメソッドを持つ。
みたいなのをイメージ。
ストラテジーパターンを使わない場合。
class Player
{
private string $magicName;
private int $magicDamage;
// $magicTypeはEnumとかの方が良い。けど面倒なのでstring。
public function __construct(string $magicType)
{
switch ($magicType) {
case 'fire':
$this->magicName = 'ファイア';
$this->magicDamage = 10;
break;
case 'ice':
$this->magicName = 'アイス';
$this->magicDamage = 20;
break;
case 'dark':
$this->magicName = 'ダーク';
$this->magicDamage = 30;
break;
default:
throw new Exception("magicType: {$magicType} に対応する魔法は存在しません");
}
}
public function showMagicDamage(): void
{
echo '魔法ダメージ: ', $this->magicDamage, "\n";
}
public function showMagicName(): void
{
echo '魔法名: ', $this->magicName, "\n";
}
}
$player = new Player('fire');
$player->showMagicName();
$player->showMagicDamage();
#=> 魔法名: ファイア
# 魔法ダメージ: 10
$player = new Player('ice');
$player->showMagicName();
$player->showMagicDamage();
#=> 魔法名: アイス
# 魔法ダメージ: 20
$player = new Player('dark');
$player->showMagicName();
$player->showMagicDamage();
#=> 魔法名: ダーク
# 魔法ダメージ: 30
コンストラクタの引数$magicType
の値によって、プロパティに代入する値を切り替える。
それによってshowMagicName()
, showMagicDamage()
の出力が変わる。
みたいな感じです。
これでも動くのですが、$magicType
の値が増えるごとにswitch文の分岐が増えるのできついです。
また、Monster
みたいなモンスタークラスが追加されたとして、同じように魔法を持つという仕様だった場合を考えます。
すると同様のswitch文がMonster
にも書かれることになると思います。
つまり$magicType
が増えるたびに両方のクラスのswitch文に分岐を追加しなければならない状態となってしまいます。
Player
, Monster
だけでなくBoss
, PartyMember
みたいにクラスが増えていったとするとswitch文への条件の追加漏れが発生したりする可能性が高くなります。
では上記の実装をストラテジーパターンで書き直してみましょう。
ストラテジーパターンを使った場合
interface MagicInterface
{
public function name(): string;
public function damage(): int;
}
class Fire implements MagicInterface
{
public function name(): string
{
return 'ファイア';
}
public function damage(): int
{
return 10;
}
}
class Ice implements MagicInterface
{
public function name(): string
{
return 'アイス';
}
public function damage(): int
{
return 20;
}
}
class Dark implements MagicInterface
{
public function name(): string
{
return 'ダーク';
}
public function damage(): int
{
return 30;
}
}
class Player
{
public function __construct(private MagicInterface $magic)
{
}
public function showMagicDamage(): void
{
echo '魔法ダメージ: ', $this->magic->damage(), "\n";
}
public function showMagicName(): void
{
echo '魔法名: ', $this->magic->name(), "\n";
}
}
$fire = new Fire();
$ice = new Ice();
$dark = new Dark();
$player = new Player($fire);
$player->showMagicName();
$player->showMagicDamage();
#=> 魔法名: ファイア
# 魔法ダメージ: 10
$player = new Player($ice);
$player->showMagicName();
$player->showMagicDamage();
#=> 魔法名: アイス
# 魔法ダメージ: 20
$player = new Player($dark);
$player->showMagicName();
$player->showMagicDamage();
#=> 魔法名: ダーク
# 魔法ダメージ: 30
Player
がMagicInterface
を実装したそれぞれの魔法クラス(Fire
など)を受け取り、
処理を切り替えています。
このように実装すると新しい魔法が増えた際にはMagicInterface
を実装した魔法クラスを定義するだけで済むので、
ストラテジーパターンを使わなかった時のようなswitch文の条件追加漏れなどが発生しません。
作業量も少なく済むしミスも発生しにくくなります。
クラスの関係を図で表すと下記のようになります。
それぞれの魔法クラスはMagicInterface
を実装し、Player
はMagicInterface
を実装したクラスを内部で持つ。
のような関係になっています。
つまり、Player
はクラスそのもの(Fireなど
)に依存せず、抽象(インターフェース)に依存しているので、MagicInterface
を実装しているクラスであれば交換可能な状態になっています。
まとめ
switch文やif文、match式などで処理をいくつも切り替えているような実装があれば、ストラテジーパターンを適用するチャンスかもしれません。そのような実装に出会った場合は適用するか検討してみましょう!
補足
ミノ駆動さんの 「良いコード/悪いコードで学ぶ設計入門」 を読んでとても勉強になったのでおすすめです。この本でもプレイヤーと魔法の関係性でコードが書かれていますが、この記事ではだいぶ省略して書きました。