0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ポケモンで学ぶオブジェクト指向〜デザインパターン編〜

Last updated at Posted at 2026-01-15

名前を知れば会話ができる

1. はじめに:なぜデザインパターンを学ぶのか

ここまで、オブジェクト指向の基礎・OO原則・SOLID原則を学んできました。
実は、このシリーズの中で、すでにいくつかの「デザインパターン」を使っていたことをご存知でしょうか?

デザインパターンとは、先人たちが発見した「よくある問題への解決策」に名前をつけたものです。

OO原則編で作った技の設計を覚えていますか?

class Pokemon {
  private moves: Move[] = [];
  
  learnMove(move: Move): void { ... }
  useMove(index: number, target: Pokemon): void {
    const move = this.moves[index];
    move.execute(this, target);
  }
}

技を独立したオブジェクトとして持ち、実行時に切り替える。
この設計にはStrategyパターンという名前がついています。

デザインパターンを学ぶメリットは何でしょうか?

  • 車輪の再発明を避けられる:よくある問題には、すでに良い解決策がある
  • チームでの共通言語になる:「ここはStrategyで」の一言で設計意図が伝わる
  • コードの意図が明確になる:パターン名がドキュメント代わりになる

今回は、ポケモンの設計を題材に、4つの代表的なパターンを学んでいきましょう。

2. Strategy パターン:技の切り替え

「アルゴリズムをカプセル化し、実行時に切り替え可能にする」

まずは、すでに使っていたStrategyパターンから始めましょう。

OO原則編で、技をメソッドからオブジェクトに切り出しました。

// Before: 技がメソッドとして固定
class Pikachu extends Pokemon {
  thunderbolt(): void {
    console.log("10まんボルト!");
  }
}

// After: 技が独立したオブジェクト
interface Move {
  name: string;
  execute(user: Pokemon, target: Pokemon): void;
}

class Thunderbolt implements Move {
  name = "10まんボルト";
  
  execute(user: Pokemon, target: Pokemon): void {
    console.log(`${user.name}の10まんボルト!`);
  }
}

そして、ポケモンは技を「持ち」、実行時に選択します。

class Pokemon {
  private moves: Move[] = [];

  learnMove(move: Move): void {
    this.moves.push(move);
  }

  useMove(index: number, target: Pokemon): void {
    const move = this.moves[index];
    move.execute(this, target);
  }
}
const pikachu = new Pokemon("ピカチュウ", [electricType], 25, 100);
pikachu.learnMove(new Thunderbolt());
pikachu.learnMove(new QuickAttack());
pikachu.learnMove(new IronTail());

// 実行時に技を選択
pikachu.useMove(0, opponent);  // 10まんボルト!
pikachu.useMove(1, opponent);  // でんこうせっか!
pikachu.useMove(2, opponent);  // アイアンテール!

これがStrategyパターンです。

  • Strategy(戦略)Moveインターフェース
  • ConcreteStrategy(具体的な戦略)ThunderboltQuickAttackなど
  • Context(利用者)Pokemonクラス

Strategyパターンを使うと、アルゴリズム(技の効果)を独立させて、実行時に切り替えることができます。
新しい技を追加してもPokemonクラスを変更する必要がありません。

OO原則編で「変化する部分をカプセル化する」と学びましたが、それを実現するための具体的な設計パターンがStrategyだったのです。

3. Factory パターン:ポケモンの生成

「オブジェクトの生成を専用のクラスに任せる」

ポケモンの生成方法はいくつかあります。

  • 野生でエンカウント(レベルや技がランダム)
  • タマゴから孵化(遺伝技、個体値の遺伝)
  • 化石から復元(特定の種族のみ)

それぞれ生成ロジックが異なります。
これを呼び出し側で毎回書いていると、どうなるでしょうか?

// ❌ 生成ロジックがあちこちに散らばる

// 野生ポケモンを生成(草むらの処理)
const pokemon1 = new Pokemon("ピカチュウ", [electricType], 0, 0);
pokemon1.level = Math.floor(Math.random() * 5) + 3;  // Lv.3〜7
pokemon1.hp = 35 + pokemon1.level;
pokemon1.learnMove(new ThunderShock());

// 野生ポケモンを生成(別の場所でも同じようなコード...)
const pokemon2 = new Pokemon("ピカチュウ", [electricType], 0, 0);
pokemon2.level = Math.floor(Math.random() * 5) + 3;
pokemon2.hp = 35 + pokemon2.level;
pokemon2.learnMove(new ThunderShock());

// タマゴから孵化
const pokemon3 = new Pokemon("ピチュー", [electricType], 1, 36);
pokemon3.learnMove(new ThunderShock());
pokemon3.learnMove(new Charm());  // 遺伝技

同じような生成ロジックがあちこちにコピーされています。
レベルの範囲を変えたくなったら、全ての箇所を修正する必要があります。

解決策:Factoryに生成を任せる

class PokemonFactory {
  // 野生ポケモンを生成
  static createWild(
    species: string,
    types: Type[],
    minLevel: number,
    maxLevel: number
  ): Pokemon {
    const level = Math.floor(Math.random() * (maxLevel - minLevel + 1)) + minLevel;
    const pokemon = new Pokemon(species, types, level, 35 + level);
    
    // レベルに応じた技を覚えさせる
    const moves = this.getMovesForLevel(species, level);
    moves.forEach(move => pokemon.learnMove(move));
    
    return pokemon;
  }

  // タマゴから孵化
  static hatchFromEgg(egg: Egg, inheritedMoves: Move[]): Pokemon {
    const pokemon = new Pokemon(egg.species, egg.types, 1, 36);
    
    // 基本技
    pokemon.learnMove(new ThunderShock());
    
    // 遺伝技
    inheritedMoves.forEach(move => pokemon.learnMove(move));
    
    return pokemon;
  }

  private static getMovesForLevel(species: string, level: number): Move[] {
    // 種族とレベルに応じた技リストを返す
  }
}

使う側はシンプルになります。

// 野生ポケモン
const wildPikachu = PokemonFactory.createWild("ピカチュウ", [electricType], 3, 7);

// タマゴから孵化
const pichu = PokemonFactory.hatchFromEgg(egg, [new Charm()]);

生成ロジックがPokemonFactoryに集約されたことで、修正が必要なときは1箇所を変えるだけで済みます。

また、生成方法の違いがメソッド名で明確になります。
createWildなのかhatchFromEggなのか、コードを読むだけで意図がわかります。

4. Observer パターン:状態異常の処理

「状態の変化を、関心のあるオブジェクトに通知する」

バトルでは、毎ターン終了時にさまざまな処理が発生します。

  • 毒状態 → HPが減る
  • やけど状態 → HPが減る
  • たべのこし(持ち物)→ HPが回復する
  • 天候が「すなあらし」→ 一部のタイプ以外はダメージ

これを素直に実装すると、どうなるでしょうか?

// ❌ BattleSystemが全ての状態を知っている
class BattleSystem {
  onTurnEnd(pokemon: Pokemon): void {
    // 状態異常のチェック
    if (pokemon.status === "poison") {
      const damage = Math.floor(pokemon.maxHp / 8);
      pokemon.receiveDamage(damage);
      console.log("毒のダメージを受けた!");
    }
    
    if (pokemon.status === "burn") {
      const damage = Math.floor(pokemon.maxHp / 16);
      pokemon.receiveDamage(damage);
      console.log("やけどのダメージを受けた!");
    }

    // 持ち物のチェック
    if (pokemon.item === "たべのこし") {
      const heal = Math.floor(pokemon.maxHp / 16);
      pokemon.heal(heal);
      console.log("たべのこしでHPが回復した!");
    }

    // 天候のチェック
    if (this.weather === "sandstorm") {
      if (!pokemon.types.includes(rockType) && 
          !pokemon.types.includes(groundType) && 
          !pokemon.types.includes(steelType)) {
        const damage = Math.floor(pokemon.maxHp / 16);
        pokemon.receiveDamage(damage);
        console.log("すなあらしのダメージを受けた!");
      }
    }

    // 新しい状態異常や持ち物が増えるたびにここを修正...
  }
}

BattleSystemがあらゆる状態異常、持ち物、天候について知っている必要があります。
新しい要素が増えるたびに、この巨大なif文を修正しなければなりません。

解決策:Observerパターンで通知する

「ターン終了」というイベントを、関心のあるオブジェクトに通知する設計に変えましょう。

// ターン終了時に呼ばれるインターフェース
interface TurnEndObserver {
  onTurnEnd(pokemon: Pokemon): void;
}

各状態異常や持ち物が、このインターフェースを実装します。

// 毒状態
class PoisonStatus implements TurnEndObserver {
  onTurnEnd(pokemon: Pokemon): void {
    const damage = Math.floor(pokemon.maxHp / 8);
    pokemon.receiveDamage(damage);
    console.log("毒のダメージを受けた!");
  }
}

// やけど状態
class BurnStatus implements TurnEndObserver {
  onTurnEnd(pokemon: Pokemon): void {
    const damage = Math.floor(pokemon.maxHp / 16);
    pokemon.receiveDamage(damage);
    console.log("やけどのダメージを受けた!");
  }
}

// たべのこし
class Leftovers implements TurnEndObserver {
  onTurnEnd(pokemon: Pokemon): void {
    const heal = Math.floor(pokemon.maxHp / 16);
    pokemon.heal(heal);
    console.log("たべのこしでHPが回復した!");
  }
}

// すなあらし
class SandstormWeather implements TurnEndObserver {
  onTurnEnd(pokemon: Pokemon): void {
    const immuneTypes = [rockType, groundType, steelType];
    const isImmune = pokemon.getTypes().some(t => immuneTypes.includes(t));
    
    if (!isImmune) {
      const damage = Math.floor(pokemon.maxHp / 16);
      pokemon.receiveDamage(damage);
      console.log("すなあらしのダメージを受けた!");
    }
  }
}

ポケモンは、自分に適用されているObserverを保持します。

class Pokemon {
  private turnEndObservers: TurnEndObserver[] = [];

  addTurnEndObserver(observer: TurnEndObserver): void {
    this.turnEndObservers.push(observer);
  }

  removeTurnEndObserver(observer: TurnEndObserver): void {
    this.turnEndObservers = this.turnEndObservers.filter(o => o !== observer);
  }

  // ターン終了時に全てのObserverに通知
  notifyTurnEnd(): void {
    for (const observer of this.turnEndObservers) {
      observer.onTurnEnd(this);
    }
  }
}

BattleSystemはシンプルになります。

class BattleSystem {
  onTurnEnd(pokemon: Pokemon): void {
    // 通知するだけ!
    pokemon.notifyTurnEnd();
  }
}
// 使用例
const pikachu = new Pokemon("ピカチュウ", [electricType], 25, 100);

// 毒状態になった
pikachu.addTurnEndObserver(new PoisonStatus());

// たべのこしを持たせた
pikachu.addTurnEndObserver(new Leftovers());

// ターン終了
pikachu.notifyTurnEnd();
// → 毒のダメージを受けた!
// → たべのこしでHPが回復した!

新しい状態異常や持ち物を追加しても、BattleSystemを修正する必要はありません。
TurnEndObserverを実装したクラスを作るだけです。

5. Decorator パターン:持ち物による強化

「オブジェクトに動的に機能を追加する」

ポケモンの持ち物には、ステータスに影響を与えるものがあります。

  • こだわりハチマキ → 攻撃1.5倍
  • こだわりメガネ → 特攻1.5倍
  • しんかのきせき → 防御・特防1.5倍(進化前のみ)

これを素直に実装すると、どうなるでしょうか?

// ❌ Pokemonクラスに全アイテム効果を書く
class Pokemon {
  private item: string | null = null;

  getAttack(): number {
    let attack = this.baseAttack;
    
    if (this.item === "こだわりハチマキ") {
      attack = Math.floor(attack * 1.5);
    }
    if (this.item === "いのちのたま") {
      attack = Math.floor(attack * 1.3);
    }
    if (this.item === "ちからのハチマキ") {
      attack = Math.floor(attack * 1.1);
    }
    // 新しいアイテムが増えるたびにここを修正...
    
    return attack;
  }

  getDefense(): number {
    let defense = this.baseDefense;
    
    if (this.item === "しんかのきせき" && this.canEvolve) {
      defense = Math.floor(defense * 1.5);
    }
    // 新しいアイテムが増えるたびにここも修正...
    
    return defense;
  }
}

持ち物が増えるたびにPokemonクラスの複数のメソッドを修正する必要があります。
単一責任原則にも違反しています。

解決策:Decoratorでステータスをラップする

まず、ステータスを提供するインターフェースを定義します。

interface StatsProvider {
  getAttack(): number;
  getDefense(): number;
  getSpecialAttack(): number;
  getSpecialDefense(): number;
  getSpeed(): number;
}

Pokemonクラスはこのインターフェースを実装し、素のステータスを返します。

class Pokemon implements StatsProvider {
  constructor(
    public name: string,
    private baseAttack: number,
    private baseDefense: number,
    private baseSpecialAttack: number,
    private baseSpecialDefense: number,
    private baseSpeed: number
  ) {}

  getAttack(): number {
    return this.baseAttack;
  }

  getDefense(): number {
    return this.baseDefense;
  }

  getSpecialAttack(): number {
    return this.baseSpecialAttack;
  }

  getSpecialDefense(): number {
    return this.baseSpecialDefense;
  }

  getSpeed(): number {
    return this.baseSpeed;
  }
}

持ち物はStatsProviderをラップして、ステータスを加工します。

// こだわりハチマキ:攻撃1.5倍
class ChoiceBandDecorator implements StatsProvider {
  constructor(private pokemon: StatsProvider) {}

  getAttack(): number {
    return Math.floor(this.pokemon.getAttack() * 1.5);
  }

  // 他のステータスはそのまま委譲
  getDefense(): number {
    return this.pokemon.getDefense();
  }

  getSpecialAttack(): number {
    return this.pokemon.getSpecialAttack();
  }

  getSpecialDefense(): number {
    return this.pokemon.getSpecialDefense();
  }

  getSpeed(): number {
    return this.pokemon.getSpeed();
  }
}

// こだわりメガネ:特攻1.5倍
class ChoiceSpecsDecorator implements StatsProvider {
  constructor(private pokemon: StatsProvider) {}

  getAttack(): number {
    return this.pokemon.getAttack();
  }

  getDefense(): number {
    return this.pokemon.getDefense();
  }

  getSpecialAttack(): number {
    return Math.floor(this.pokemon.getSpecialAttack() * 1.5);
  }

  getSpecialDefense(): number {
    return this.pokemon.getSpecialDefense();
  }

  getSpeed(): number {
    return this.pokemon.getSpeed();
  }
}

// こだわりスカーフ:素早さ1.5倍
class ChoiceScarfDecorator implements StatsProvider {
  constructor(private pokemon: StatsProvider) {}

  getAttack(): number {
    return this.pokemon.getAttack();
  }

  getDefense(): number {
    return this.pokemon.getDefense();
  }

  getSpecialAttack(): number {
    return this.pokemon.getSpecialAttack();
  }

  getSpecialDefense(): number {
    return this.pokemon.getSpecialDefense();
  }

  getSpeed(): number {
    return Math.floor(this.pokemon.getSpeed() * 1.5);
  }
}
// 素のピカチュウ
const pikachu = new Pokemon("ピカチュウ", 55, 40, 50, 50, 90);
console.log(pikachu.getAttack());  // 55
console.log(pikachu.getSpeed());   // 90

// こだわりハチマキを持たせる
const pikachuWithBand: StatsProvider = new ChoiceBandDecorator(pikachu);
console.log(pikachuWithBand.getAttack());  // 82(1.5倍)
console.log(pikachuWithBand.getSpeed());   // 90(変わらず)

// こだわりスカーフを持たせる
const pikachuWithScarf: StatsProvider = new ChoiceScarfDecorator(pikachu);
console.log(pikachuWithScarf.getAttack()); // 55(変わらず)
console.log(pikachuWithScarf.getSpeed());  // 135(1.5倍)

Decoratorパターンのポイントは、元のオブジェクトを変更せずに機能を追加できることです。

Pokemonクラスは素のステータスを返すだけ。
持ち物の効果は各Decoratorクラスが担当します。
新しい持ち物を追加しても、Pokemonクラスを修正する必要はありません。

6. まとめ

今回学んだ4つのパターンを振り返りましょう。

パターン 目的 ポケモンでの例
Strategy アルゴリズムを切り替え可能に 技を独立したオブジェクトとして持つ
Factory 生成ロジックを一元管理 野生エンカウント、タマゴ孵化、化石復元
Observer イベントを関心のあるオブジェクトに通知 ターン終了時の毒ダメージ、持ち物効果
Decorator 動的に機能を追加 持ち物によるステータス強化

デザインパターンは「目的」ではなく「手段」です。
パターンを使うこと自体が目的になってはいけません。

大切なのは、問題を認識し、適切な解決策を選べるようになることです。
今回学んだパターンの名前を知っていれば、同じような問題に出会ったときに「これはStrategyで解決できそうだ」と気づくことができます。

7. シリーズ全体の振り返り

全4回のシリーズを通じて、オブジェクト指向の考え方を学んできました。

学んだこと
基礎編 クラス、インスタンス、カプセル化、継承、ポリモーフィズム
OO原則編 継承よりコンポジション、変化のカプセル化、インターフェース指向
SOLID原則編 単一責任、オープン・クローズド、リスコフの置換、インターフェース分離、依存性逆転
デザインパターン編 Strategy、Factory、Observer、Decorator

基礎編で学んだ概念が、OO原則編で「なぜそうするのか」という原則につながり、SOLID原則編でより具体的な指針になり、デザインパターン編で「こう実装する」という形になりました。

オブジェクト指向は、一度学んで終わりではありません。
実際のコードを書きながら、「この設計で変更に強いか?」「責任が集中していないか?」と問い続けることで、少しずつ身についていきます。

このシリーズが、皆さんの設計力向上の一助となれば幸いです。

シリーズ目次

本記事は「ポケモンで学ぶオブジェクト指向」シリーズの一部です。

  1. 基礎編:ピカチュウをコードで表現してみよう
  2. OO原則編:継承だけでは表現できない世界
  3. SOLID原則編:5つの原則で設計を磨く
  4. デザインパターン編:名前を知れば会話ができる
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?