1. はじめに
FactoryMethodパターン、ご存知ですか?
比較的、デザインパターンの中では有名な方...と思っていますが、躓いたことがある人・躓いている人、結構多いんじゃないんでしょうか?
実務でぴったりなタスクがあったので意識してコーディングしていましたが、深堀って調べてたら割と自身の理解が浅いと気づきました。
この気にちゃんと抑えようと思い、記事にまとめてようと思います。
今回は、ポケモンバトルにおける、あの工程 にフォーカスを当てて紹介させていただきます。
それでは!どうぞっ!
※ 僕なりの理解です。
※ どなたかの概念理解の助けになれば幸いです。
2. 対象読者
- オブジェクト指向のデザインパターンに興味のある方
- ポケモンが好きな方
※ ポケモン知らない人は本記事は楽しめないかもです。
3. Factory Methodパターンとは
GoFによって提唱された、23のデザインパターンの1つです。
Factory Method パターン は、スーパークラスで「生成のためのインターフェース」だけを決め、サブクラスで「生成される具象オブジェクトの型」を切り替えられるようにするパターンです。
横文字も漢字も多くて難しいですよね。
文章だけで理解できる方、だいぶ稀だと思います。
👉 ポイント
- 生成ロジックを呼び出し元から切り離せる
- サブクラスごとに異なる生成方法を実装しやすい
- → 結果として、「呼び出し側に実際の生成が隠される(カプセル化)」
- → 呼び出し側の変更を最小限に抑えつつ、新しい具象クラスを追加できる
ここまでで理解できた方は、この後の文章は時間の無駄かもです。
4. ポケモンバトルやったことありますか?
すみません。雑談です。
僕、ルビサファ世代です。(アニメしかしらない)
ORASのバトル、熱かった記憶あります。(周りが)
ガチ勢じゃないんですが、ゲームのストーリーは楽しんでやってた記憶あります。
さて、本題ですが。
オンラインのポケモンバトルっていろいろルールあると思います。オーソドックスな流れってこんな感じですよね。
- オンラインで対戦相手と出会う
- お互いの手持ち6匹を確認する
- バトルに向けて3匹選定する
- バトル開始
実はこの時、FactoryMethodパターンをみんなやってます。
それは...。「対戦前の3匹選定」 です!
5. 対戦前の3匹選定ってどんな操作だっけ?
相手の6匹を確認した上で、自分はどの3匹で勝ちに行くか、結構頭を使う大事なフェーズですよね。
バトル経験のある方だと当たり前のルールだと思いますが、改めてポイントを記載させていただきます。
5-1. 選定におけるポイント
👉 お互いの選出状況を知ることはNG
ここでいう"選出"というのは、"バトルポケモンを選出するロジック"、と捉えることができると思います。
もちろん、そのロジックは対戦相手に知られちゃいけないですよね。
僕:「あ、生成処理のカプセル化だ。」
👉 バトルが始まっても、3匹全ての正体は明かされない
バトルスタート後、最初の一匹はすぐに明かされますが、他の選出結果は伏せられますよね。
プログラムでいうと、"バトルポケモン"は配列ではなくオブジェクトだと思います。
👉 いざバトルが始まってやることは、先頭一匹に関する操作
バトルポケモンが、どの三匹であれどんな状態であれどんな型であれ...。
使うメソッドは決まってます。
サトシ: 「行け!〇〇!」
ちょっと寄せて記載しましたが、全部FactoryMethod パターンの特徴です。
では、次は少し具体化してコードベースで説明させていただきます。
6. 対戦前の3匹選定をコード解説してみる
では、今回のテーマ "対戦前の3匹選定" をコードで解説していきます。
6-1. 本記事でのバトルの流れ
※ 本記事のバトルの仕様です。
以下をイメージしてください。
- ルームAで準備(自分が)
- ルームBで準備(対戦相手が)
- バトルスタート
これを、こんな感じでバトルを実装していこうと思います。
function main() {
// 0. バトルに必要な手持ちポケモンリストなどを用意
// 1. ルームAで「自分の選出」を実行
// 2. ルームBで「対戦相手の選出」を実行
// 3. バトル開始
}
6-2. 型定義と共通クラスの定義
今回の肝は2つ目のクラス、BattlePokemon
です。
一番手のポケモンを呼び出せるようにプロパティを用意しましょう。
// ─── エンティティ定義 ──────────────────────────────
// ポケモンエンティティ
interface Pokemon {
name: string;
}
// BattlePokemon をクラスとして定義
class BattlePokemon {
constructor(
public first: Pokemon,
public second: Pokemon,
public third: Pokemon
) {}
}
// ─── Factory Method の抽象定義 ─────────────────────────
abstract class PokemonFactory {
// Factory Method: 具体的な生成は各サブクラスで実装する
abstract createBattlePokemon(pokemons: Pokemon[]): BattlePokemon;
}
-
Pokemon
: ポケモンです。 -
BattlePokemon
: バトルポケモンです。三匹を内包するオブジェクトです。メソッド:first
で一匹目を繰り出します。 -
PokemonFactory
: バトルポケモンを選出するロジックに凝集します。6匹配列で受け取って、3匹を選出するイメージです。
6-2. バトルクラスの実装
"バトルの準備"と、"バトル開始"を実装をしましょう。
「バトルポケモンオブジェクトを準備し、開始する」という責務に凝集する、Battleクラスを実装します。
// ─── Battleクラスの定義 ─────────────────────────
class OnlineBattle {
// ファクトリーの依存性注入により、生成処理の詳細は受け取ったファクトリーに委譲される
constructor(private factory: PokemonFactory) {}
// バトル用のBattlePokemonを生成する
prepareBattle(pokemons: Pokemon[]): BattlePokemon {
return this.factory.createBattlePokemon(pokemons);
}
startBattle(myBattle: BattlePokemon, yourBattle: BattlePokemon): void {
console.log(`自分: いけ! ${myBattle.first.name}!`);
console.log(`対戦相手: いけ! ${yourBattle.first.name}!`);
}
}
👉 ポイント
コンストラクタでファクトリーを受け取ることにより、後から容易に、異なる生成戦略を採用できるようになります。
つまり、OnlineBattle のコードを一切変更することなく、利用するファクトリー(=生成戦略)を差し替えるだけで機能改修を行うことができるため、拡張性と保守性が向上しますよね。
もし。FactoryMethodパターンを採用しなかった場合...。
OnlineButtleクラスのprepareBattle()
内で、自分も対戦相手も選出ロジックが露出することになってしまいます。
さすがに嫌ですよね。出来レースだ...。
6-3. ファクトリークラスの実装
では、選出ロジックを実装していきましょう。
// ─── 具象ファクトリーの実装 ─────────────────────────
// 自分用のファクトリー:戦略的な選出ロジックを実装
class MyPokemonFactory extends PokemonFactory {
createBattlePokemon(pokemons: Pokemon[]): BattlePokemon {
const [first, second, third] = this.select.handle(pokemons);
return new BattlePokemon(first, second, third);
}
}
// 対戦相手用のファクトリー:シンプルに最初の3匹を選出
class YourPokemonFactory extends PokemonFactory {
createBattlePokemon(pokemons: Pokemon[]): BattlePokemon {
return new BattlePokemon(pokemons[0], pokemons[1], pokemons[2]);
}
}
僕は選出ロジックを切り出して、そっちの方でテクニカルに選出しちゃいました。
対戦相手は最初の三匹をそのまま選出するんですね。大丈夫ですかね。
👉 ポイント
なにはともあれ。
- ファクトリークラス内で、
BattlePokemon
インスタンス生成をしているため、生成ロジックはクライアントから完全に隠蔽されてますよね。 - また、具象クラスの実装により、戦略的に選出ロジックを切り替えることができますよね。
6-4. バトルの実装
では最後に、バトルを実装します。
function main(): void {
// それぞれのトレーナーの手持ちポケモンリスト
const myPokemons: Pokemon[] = [
{ name: "ルカリオ" },
{ name: "ブラッキー" },
{ name: "カバルドン" },
{ name: "マリルリ" },
{ name: "グライオン" },
{ name: "ゲンガー" },
];
const yourPokemons: Pokemon[] = [
{ name: "ガルーラ" },
{ name: "バジャーモ" },
{ name: "ゲンガー" },
{ name: "スイクン" },
{ name: "ゲッコウガ" },
{ name: "ガブリアス" },
];
// BattlePokemonファクトリーを生成
const myFactory = new MyPokemonFactory();
const yourFactory = new YourPokemonFactory();
// 各ルームごとに異なるファクトリーでバトル開始準備を行う
const myBattlePrepareRoom = new OnlineBattle(myFactory);
const yourBattlePrepareRoom = new OnlineBattle(yourFactory);
// お互いのバトルポケモンを選出する
const myBattlePokemon = myBattleRoom.prepareBattle(myPokemons);
const yourBattlePokemon = yourBattleRoom.prepareBattle(yourPokemons);
// バトル開始
myBattleRoom.startBattle(myBattlePokemon, yourBattlePokemon);
}
main();
// ログ
// 自分: いけ! ルカリオ!
// 対戦相手: いけ! ガルーラ!
ここまで実装すればバトルを開始できそうですね。
7. まとめ
どうでしょうか?
最後に、FactoryMethodパターンの要点をまとめて終わろうと思います。
項目 | 説明 |
---|---|
生成処理の隠蔽 | ファクトリークラス内で new を使って BattlePokemon のインスタンスを生成するため、生成ロジックをクライアント側から完全に隠蔽できる。直接 new を書く手間やその後の変更が不要になり、生成ルールをカプセル化可能。 |
柔軟な生成戦略 | 複数の具象ファクトリー(たとえば MyPokemonFactory や YourPokemonFactory )を用いるため、どんな選出ロジックも簡単に切り替えることができる仕組み。誰がどのルールでポケモンを選ぶか、といった戦略の違いを柔軟に実現できる点が魅力。 |
依存性注入による拡張性 | 呼び出し側(Battle )は、あくまでファクトリーを注入するだけでOK。つまり、生成処理の詳細に縛られることなく、必要に応じた生成戦略(=ファクトリー)を後から差し替えることができるため、Open/Closed Principleにも則った柔軟な設計が実現可能。 |
つまり FactoryMethod は、「生成処理の完全な隠蔽」と「簡単な生成戦略の差し替え」を両立を意識したデザインパターンです!
8. 補足
8-1. Factoryに選出ロジックを注入したほうがよいのでは?
今回の例では、OnlineBattleクラスが具象ファクトリー(MyPokemonFactory や YourPokemonFactory)を依存性注入(DI)によって受け取り、ファクトリーに生成を委ねる設計になっています。
本記事の設計では。
-
生成処理の隠蔽
クライアント側(ここではOnlineBattle)は、内部で new を使ってインスタンスを生成する処理を全く意識せず、ただファクトリーのcreateBattlePokemon()
メソッドを呼び出すだけとなっています。伝統的なGoFのFactory Methodパターンの考え方を体現できている認識でいます。 -
柔軟な生成戦略の切り替え
本記事の設計では、具象ファクトリー自体が各々異なる生成ロジック(戦略)を持っているため、どのファクトリーを利用するかを外部から決められるという点が魅力だと自負しています。
DIを使って具象ファクトリーを注入することで、後から簡単にテスト用や別の戦略のファクトリーに差し替えることが可能かと。
GoFのFactory Methodの基本構成では、
- 「Creator(抽象 or 具体)クラス」に factoryMethod() がある
- 「ConcreteCreator」が factoryMethod() をオーバーライドしてConcreteProduct を生成する
- 「Creator」が持つ別メソッド anOperation() 内で factoryMethod() を呼び出して業務ロジックと生成を組み合わせる
といった形式が、書籍上の典型図になっているようです。
本記事では、抽象クラスPokemonFactoryがあり、OnlineBattleが生成呼び出し部分を担うややDI寄りの形であるため、従来よりも「Creator役の責務」を切り分けながら運用する設計に近いと考えております。
現場ではGoFの基本構造をベースにDIや他のパターンをミックスするのが一般的のようですので、比較的アレンジには近いですが、依存性注入の技法を取り入れることで、より柔軟かつテストしやすい設計となっている点は、現代的なアプローチと言えるのかな... と。
もし、従来の「Factoryに選出ロジックを注入する」純粋なGoFスタイルと、本記事の設計を厳密に比較されるのであれば、ニュアンスファクトリーであるのは間違いないとは理解しております。
8-2. それFactoryMethodじゃなくてStrategyじゃね?
うんうん、たしかにドキッとしました。
これについてはちゃんと抑えた後別記事で言及しようと思います。(本記事でも気になるところあればコメントください)
生成処理(newの呼び出し)がファクトリーに隠蔽され、クライアント側はその生成結果(BattlePokemon)を受け取るだけになっているため、目的・責務の観点ではFactory Methodの理にかなってる認識でいます。
また、Strategyパターンは「アルゴリズムの差し替え」が主目的だと思います。
そのため、生成自体を隠蔽するかどうかは必須要件ではないかなぁとも思っているところです。
9. 最後に
僕の好きなポケモンはチルタリスです。
その次に好きなポケモンはリザードンです。
あと、なんだかんだルカリオが好きです。
以上、あざしたっ!