【PHP】Template Methodパターンについて
はじめに
Template Methodパターンは、アルゴリズムの全体がほとんどそっくりなクラス群があるときに、その共通部分を基底クラスに定義しておいて、異なっている部分を派生クラスに定義することで穴埋め問題にするパターンです。RPGゲームで、操作しているキャラクターが敵に攻撃する際の処理を例に挙げてこのパターンについて解説します。
実装例
RPGゲームで、操作しているキャラクターが敵に攻撃する際の処理を例に挙げます。
キャラクターが敵に攻撃する際は、以下の手順に従います。
- 敵を探す
- 敵まで移動する
- 敵を攻撃する
キャラクターには複数の種類があり、キャラクターによって攻撃方法が異なります。
上記の処理をTemplate Methodパターンで実装した例を下記に示します。
<?php
interface Character
{
public function attackEnemy(): void;
}
abstract class AbstractCharacter implements Character
{
// 攻撃の手順はすべてのキャラクターで共通
public function attackEnemy(): void
{
$enemy = $this->searchEnemy();
$this->moveToEnemy($enemy);
$this->attack($enemy);
}
// searchEnemyとmoveToEnemyはすべてのキャラクターで共通の処理
private function searchEnemy(): Enemy
{
// 敵を探す処理
}
private function moveToEnemy(Enemy $enemy): void
{
// 敵まで移動する処理
}
// attackはキャラクターごとに異なる処理
abstract protected function attack(Enemy $enemy): void;
}
<?php
/**
* 戦士
*/
class Warrior extends AbstractCharacter
{
protected function attack(Enemy $enemy): void
{
// 戦士特有の攻撃処理
}
}
/**
* 魔法使い
*/
class Mage extends AbstractCharacter
{
protected function attack(Enemy $enemy): void
{
// 魔法使い特有の攻撃処理
}
}
すべてのキャラクターで、敵を攻撃する際の手順と、敵を探す処理と敵まで移動する処理は共通しています。そのため、これらの処理を基底クラスであるAbstractCharacterクラスに定義しています。
一方、敵を攻撃する処理はキャラクターごとに異なります。そのため、この処理をAbstractCharacterクラスに抽象メソッドとして定義しておいて、実際の実装部分は派生クラスに任せています。
今回の場合は、AbstractCharacterクラスを継承して、戦士と魔法使いの派生クラスを作成しています。そして、これらの派生クラスに、戦士と魔法使い固有の攻撃処理を定義しています。
利点
このパターンの利点は、実装する際の負担を軽減し生産性を上げられる点です。
新しいキャラクターの攻撃処理を追加する際に、このパターンを使わずにその都度すべての攻撃の手順を実装する場合を考えてみます。
この場合だと、「敵を探す → 敵まで移動する → 敵を攻撃する」という攻撃の手順を完全に把握していないと新しいキャラクターの攻撃処理を追加できません。追加の度に、これらの手順の全体を考慮しないといけないのは面倒ですし生産性も低いです。
Template Methodパターンを使えば、新しいキャラクターの攻撃処理追加の際に、個々のキャラクター固有の攻撃方法を考えるだけで済みます。追加の度にわざわざ攻撃手順の全体を考慮する必要はありません。このパターンを使わないときと比べて、明らかに考えないといけないことが減るので、実装の負担が低く生産性が高いです。
欠点
このパターンの欠点は、継承を使うことによる密結合と、実装の柔軟性の低さです。
継承をしている以上、基底クラスの変更がすべての派生クラスに予期しない影響を与えてしまう可能性が高いです。また、派生クラスは基底クラスで決められた手順の枠組みから外れることができません。攻撃の手順が異なる新たなキャラクターを追加したくても非常に困難であり、実装の柔軟性は低いです。
これらのことから、イレギュラーな派生クラスを追加する必要があまり考えられない場合のみ、このパターンを安心して使っていくことができます。しかし、一般的には継承よりも疎結合な委譲を選択した方が良い場合が多いので、Template Methodパターンを使うよりも、委譲を用いるStrategyなどのパターンを使うことをまずは検討するべきです。
注意点
下記に示すように、直接AbstractCharacterに依存する実装を作成してしまうのは避けるべきです。
// キャラクターの攻撃処理のクライアントコードです
class ClientCode
{
// このようにクライアントが直接AbstractCharacterに依存するのは避けるべき
public function __construct(
private AbstractCharacter $character
) {}
public function execute(): void
{
$this->character->attackEnemy();
}
}
上記のように実装してしまうと、AbstractCharacterの大枠に当てはまらない実装を追加する必要がある場合に対応できません。
AbstractCharacterではなく、インターフェイスであるCharacterに依存させるべきです。
class ClientCode
{
// インターフェイスであるCharacterに依存させるべき
public function __construct(
private Character $character
) {}
public function execute(): void
{
$this->character->attackEnemy();
}
}
インターフェイスに依存させておけば、AbstractCharacterの大枠に当てはまらない実装を追加する必要がある場合にも簡単に対応できます。また、もしAbstractCharacterを修正したい場合にも、クライアントコードに変更の影響が出てしまうことを防ぐことができます。
まとめ
Template Methodパターンは、アルゴリズムの骨格を基底クラスに定めて、具体的な処理内容を派生クラスに穴埋めさせることで、実装する際の負担を軽減し生産性を高めるパターンです。継承関係を使っているので結合度が高いのと、基底クラスで定められた枠組みから外れるような実装を追加したい場合は非常に難しいという欠点を持っています。このパターンは、処理の全体の枠組みが安定的で、将来的にイレギュラーな手順の変更があまり予想できない場合に有効なパターンであると言えます。
Discussion