0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ポケモンで学ぶオブジェクト指向〜OO原則編〜

Last updated at Posted at 2026-01-15

継承だけでは表現できない世界

1.はじめに:基礎編の設計を振り返る

前回、オブジェクト指向の基礎として、クラス・インスタンス・カプセル化・継承・ポリモーフィズムを学びました。

abstract class Pokemon {
  constructor(
    public name: string,
    protected level: number,
    protected hp: number
  ) {}

  abstract cry(): void;
}

class Pikachu extends Pokemon {
  constructor(level: number) {
    super("ピカチュウ", level, 35 + level);
  }

  cry(): void {
    console.log("ピカチュウ!");
  }

  thunderbolt(): void {
    console.log("10まんボルト!");
  }
}

一見よさそうですが、この設計にはいくつか問題が隠れています。
前回の最後に触れた「タイプ」と「技」の問題を、改めて考えてみましょう。

2. 継承の限界:タイプをどう表現する?

ポケモンにはタイプがあります。
ピカチュウはでんきタイプ、コイキングはみずタイプ。
これを継承で表現しようとすると、どうなるでしょうか?

// タイプごとに親クラスを作る?
class ElectricPokemon extends Pokemon {}
class WaterPokemon extends Pokemon {}
class FlyingPokemon extends Pokemon {}

class Pikachu extends ElectricPokemon {}  // ピカチュウは電気タイプ
class Koiking extends WaterPokemon {}     // コイキングは水タイプ

単一タイプならうまくいきそうです。
ではギャラドス(みず・ひこう)はどうでしょうか?

// 多重継承? → TypeScriptではできない
class Gyarados extends WaterPokemon, FlyingPokemon {}  // ❌ エラー!

// 複合タイプ専用のクラスを作る?
class WaterFlyingPokemon extends Pokemon {}
class Gyarados extends WaterFlyingPokemon {}  // 動くけど...

複合タイプ用のクラスを作れば動きます。
でも、タイプの組み合わせは100通り以上。
FireFlyingPokemonWaterGroundPokemonGrassPoisonPokemon...全部作りますか?

これが継承の限界です。


原則①:継承よりコンポジションを好む

ここで役立つのが「継承よりコンポジション」という原則です。

コンポジションとは何でしょうか?

継承は「親クラスを拡張する」という関係です。
一方、コンポジションは「他のオブジェクトを部品として持つ」という関係です。

// 継承:ピカチュウ「は」電気タイプポケモン「である」
class Pikachu extends ElectricPokemon {}

// コンポジション:ピカチュウ「は」電気タイプ「を持っている」
class Pikachu {
  private type: Type = electricType;
}

継承が「is-a(〜である)」の関係なら、コンポジションは「has-a(〜を持っている)」の関係です。

タイプの問題を、コンポジションで解決してみましょう。
まず、タイプを独立したオブジェクトとして定義します。

interface Type {
  name: string;
  // 相性計算(将来の拡張用)
  getEffectiveness(target: Type): number;
}

// 各タイプを作る
const electricType: Type = {
  name: "でんき",
  getEffectiveness(target: Type): number {
    if (target.name === "みず") return 2.0;   // 効果バツグン
    if (target.name === "でんき") return 0.5; // いまひとつ
    return 1.0;
  }
};

const waterType: Type = {
  name: "みず",
  getEffectiveness(target: Type): number {
    if (target.name === "ほのお") return 2.0;
    if (target.name === "みず") return 0.5;
    return 1.0;
  }
};

const flyingType: Type = {
  name: "ひこう",
  getEffectiveness(target: Type): number {
    if (target.name === "くさ") return 2.0;
    if (target.name === "でんき") return 0.5;
    return 1.0;
  }
};

次に、ポケモンがタイプを「持つ」設計に変えます。

class Pokemon {
  private types: Type[];

  constructor(
    public name: string,
    types: Type[],
    protected level: number,
    protected hp: number
  ) {
    this.types = types;
  }

  getTypes(): Type[] {
    return [...this.types];
  }
}

これで複合タイプも自由自在です。

// 単一タイプ
const pikachu = new Pokemon("ピカチュウ", [electricType], 25, 100);

// 複合タイプも同じように作れる!
const gyarados = new Pokemon("ギャラドス", [waterType, flyingType], 30, 150);
const charizard = new Pokemon("リザードン", [fireType, flyingType], 36, 140);

継承で100個以上のクラスを作る必要はなくなりました!

3. 変化する部分をカプセル化する:技の設計

次は技の問題です。
基礎編では、技をメソッドとして定義していました。

class Pikachu extends Pokemon {
  thunderbolt(): void {
    console.log("10まんボルト!");
  }
}

この設計の問題点は何でしょうか?

実際のゲームでは、同じピカチュウでも個体によって覚えている技は違います。
サトシのピカチュウは「10まんボルト」「アイアンテール」「でんこうせっか」を覚えていますが、野生のピカチュウは「でんきショック」しか使えないかもしれません。

メソッドとして定義すると、全てのピカチュウが同じ技を持つことになってしまいます。


原則②:変化する部分をカプセル化する

「変化する部分」を見つけて、それを独立させましょう。
ここで変化するのは「技」です。
技を独立したオブジェクトとして切り出します。

interface Move {
  name: string;
  power: number;
  type: Type;
  execute(user: Pokemon, target: Pokemon): void;
}

具体的な技は、このインターフェースを実装して作ります。

class Thunderbolt implements Move {
  name = "10まんボルト";
  power = 90;
  type = electricType;

  execute(user: Pokemon, target: Pokemon): void {
    console.log(`${user.name}の10まんボルト!`);
    // ダメージ計算ロジック...
  }
}

class QuickAttack implements Move {
  name = "でんこうせっか";
  power = 40;
  type = normalType;

  execute(user: Pokemon, target: Pokemon): void {
    console.log(`${user.name}のでんこうせっか!`);
  }
}

class ThunderShock implements Move {
  name = "でんきショック";
  power = 40;
  type = electricType;

  execute(user: Pokemon, target: Pokemon): void {
    console.log(`${user.name}のでんきショック!`);
  }
}

ポケモンは技を「持つ」ようになります。

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

  constructor(
    public name: string,
    types: Type[],
    protected level: number,
    protected hp: number
  ) {
    this.types = types;
  }

  // 技を覚える
  learnMove(move: Move): void {
    if (this.moves.length >= 4) {
      console.log("技は4つまでしか覚えられない!");
      return;
    }
    this.moves.push(move);
    console.log(`${this.name}${move.name}を覚えた!`);
  }

  // 技を使う
  useMove(index: number, target: Pokemon): void {
    const move = this.moves[index];
    if (!move) {
      console.log("その技は覚えていない!");
      return;
    }
    move.execute(this, target);
  }

  // 覚えている技を確認
  getMoves(): Move[] {
    return [...this.moves];
  }
}

これで、個体ごとに異なる技構成が実現できます。

// サトシのピカチュウ
const satoshiPikachu = new Pokemon("ピカチュウ", [electricType], 50, 120);
satoshiPikachu.learnMove(new Thunderbolt());
satoshiPikachu.learnMove(new QuickAttack());
satoshiPikachu.learnMove(new IronTail());

// 野生のピカチュウ(弱い技だけ)
const wildPikachu = new Pokemon("ピカチュウ", [electricType], 5, 35);
wildPikachu.learnMove(new ThunderShock());

4. インターフェースに対してプログラミングする

タイプと技の設計を通じて、もう一つの重要な原則が見えてきます。

原則③:インターフェースに対してプログラミングする

PokemonクラスのuseMoveメソッドをもう一度見てみましょう。

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

  // 技を使う
  useMove(index: number, target: Pokemon): void {
    const move = this.moves[index];
    if (!move) {
      console.log("その技は覚えていない!");
      return;
    }
    move.execute(this, target);
  }
}

このメソッドは、技が「10まんボルト」なのか「でんこうせっか」なのかを知りません。
知っているのは「Moveインターフェースにexecuteメソッドがある」ということだけです。

// Pokemonクラスは具体的な技クラスを知らない
move.execute(this, target);  // Thunderbolt? QuickAttack? 知らなくても動く

これが「インターフェースに対してプログラミングする」ということです。

具体的なThunderboltクラスやQuickAttackクラスではなく、Moveインターフェースに依存することで、新しい技を追加してもPokemonクラスを変更する必要がありません。

// 新しい技を追加
class VoltTackle implements Move {
  name = "ボルテッカー";
  power = 120;
  type = electricType;

  execute(user: Pokemon, target: Pokemon): void {
    console.log(`${user.name}のボルテッカー!`);
    // 反動ダメージの計算...
  }
}

// Pokemonクラスは一切変更なしで新しい技が使える
satoshiPikachu.learnMove(new VoltTackle());
satoshiPikachu.useMove(3, gyarados); // ボルテッカー!

もしPokemonクラスが具体的な技クラスに依存していたらどうなるでしょうか?

// ❌ 悪い例:具体的なクラスに依存
class Pokemon {
  useMove(move: Thunderbolt | QuickAttack | VoltTackle, target: Pokemon): void {
    if (move instanceof Thunderbolt) {
      console.log("10まんボルト!");
      // Thunderbolt固有の処理...
    } else if (move instanceof QuickAttack) {
      console.log("でんこうせっか!");
      // QuickAttack固有の処理...
    } else if (move instanceof VoltTackle) {
      console.log("ボルテッカー!");
      // VoltTackle固有の処理...
    }
    // 新しい技を追加するたびにここを修正...
  }
}

技が増えるたびにPokemonクラスを修正することになります。
これでは拡張性がありません。

インターフェースに依存することで、Pokemonクラスは「技の使い方」という抽象的な知識だけを持ち、具体的な技の詳細はMove実装クラスに任せることができます。

5. Before/After比較

今回の変更を振り返ってみましょう。

観点 Before(基礎編) After(OO原則適用後)
タイプ 継承で表現 → 複合タイプで破綻 コンポジションで柔軟に表現
クラスのメソッドとして固定 独立したオブジェクトで個体ごとに設定可能
拡張性 新タイプ・新技追加でクラス修正が必要 既存コード変更なしで追加可能

6. まとめ

今回学んだ3つの原則を整理します。

継承よりコンポジションを好む

「is-a(〜である)」より「has-a(〜を持っている)」で考える。
タイプを継承ではなく、ポケモンが「持つ」ものとして設計することで、複合タイプを柔軟に表現できました。

変化する部分をカプセル化する

変化しやすい部分を見つけて独立させる。
技をメソッドからオブジェクトに切り出すことで、個体ごとに異なる技構成を実現できました。

インターフェースに対してプログラミングする

具体的なクラスではなく、インターフェースに依存する。
Moveインターフェースを使うことで、新しい技を追加しても既存コードを変更せずに済みました。


次回予告

今回の設計、だいぶ良くなりました。
でも、まだ改善の余地があります。

「新しい技を追加するたびに、ダメージ計算のコードを毎回書くの?」

「でんじは(変化技)を作ろうとしたら、powerに何を入れればいい?攻撃技と変化技、同じインターフェースで本当にいいの?」

次回は「SOLID原則編」を通じて、さらに堅牢で拡張しやすい設計を目指します。

お楽しみに!

シリーズ目次

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

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?