TypeScript | 関数・クラス・ジェネリクス:クラス設計 – Factoryパターン

TypeScript TypeScript
スポンサーリンク

ゴール:「new の場所を“工場”に集めて、作り方のルールを1カ所に閉じ込める感覚をつかむ」

Factory(ファクトリ)パターンは一言でいうと、

「オブジェクトの new をあちこちで書かず、“作る専門のクラス/関数”にまとめる設計」

です。

もっと砕くと、

  • 「どのクラスを new するか」
  • 「どういう初期値で作るか」

といった“作り方の判断”を、
バラバラに書かずに 1カ所に集約する ための考え方です。

ここを理解すると、

「とりあえずその場で new」から
「作り方をデザインする」側に一歩進めます。


まずは「その場で new してしまう」コードから見る

直接 new していると何がつらくなるか

例えば、通知を送るクラスを考えます。

class EmailNotifier {
  send(message: string): void {
    console.log("メール送信:", message);
  }
}

class LineNotifier {
  send(message: string): void {
    console.log("LINE送信:", message);
  }
}
TypeScript

これを使うコードが、あちこちにこう書かれているとします。

function notifyUser(type: "email" | "line", message: string) {
  if (type === "email") {
    const notifier = new EmailNotifier();
    notifier.send(message);
  } else {
    const notifier = new LineNotifier();
    notifier.send(message);
  }
}
TypeScript

一見普通ですが、問題はここです。

  • notifyUser が「どのクラスを new するか」を知ってしまっている
  • もし「SlackNotifier」を追加したくなったら、この関数を直接書き換える必要がある
  • 同じような if (type === ...) new ... が、別の場所にも増えがち

つまり、

「オブジェクトの作り方のロジックが、アプリのあちこちに散らばる」

という状態になりやすいです。


Factory パターンの基本アイデア:「作る役」を分離する

「作る専門の関数(工場)を用意する」

さっきの例を Factory パターンで書き直してみます。

まず、「Notifier を作る工場」を用意します。

interface Notifier {
  send(message: string): void;
}

class EmailNotifier implements Notifier {
  send(message: string): void {
    console.log("メール送信:", message);
  }
}

class LineNotifier implements Notifier {
  send(message: string): void {
    console.log("LINE送信:", message);
  }
}

class NotifierFactory {
  static create(type: "email" | "line"): Notifier {
    if (type === "email") {
      return new EmailNotifier();
    } else {
      return new LineNotifier();
    }
  }
}
TypeScript

使う側はこうなります。

function notifyUser(type: "email" | "line", message: string) {
  const notifier = NotifierFactory.create(type);
  notifier.send(message);
}
TypeScript

ここで起きた変化を言葉にすると、

  • notifyUser は「どのクラスを new するか」を知らなくてよくなった
  • 「作り方の判断」は NotifierFactory に閉じ込められた
  • 新しい種類(Slack など)を追加したいときは、NotifierFactory だけを直せばいい

という状態になっています。

「new の判断を 1 カ所に集める」= Factory パターンの第一歩 です。


重要ポイント:Factory が守ってくれる“3つのこと”

1つ目:呼び出し側を「具体クラス」から切り離す

Factory を使うと、呼び出し側は
「どのクラスを new するか」を知らなくてよくなります。

// 悪い例(直接 new)
const notifier = new EmailNotifier();

// Factory 経由
const notifier = NotifierFactory.create("email");
TypeScript

後者では、呼び出し側は「Notifier をくれる工場」にしか依存していません。

これにより、

  • クラス名を変えたくなっても、Factory の中だけ直せばいい
  • 実装を差し替えても、呼び出し側はそのままでいい

という柔らかい設計になります。

2つ目:作り方のルールを 1 カ所に集約できる

例えば、「本番環境では Email、開発環境ではコンソール出力だけ」
みたいなルールを入れたくなったとします。

Factory がないと、
あちこちの new の場所に条件分岐を書き足すことになります。

Factory があれば、こうです。

class NotifierFactory {
  static create(): Notifier {
    if (process.env.NODE_ENV === "production") {
      return new EmailNotifier();
    } else {
      return new LineNotifier(); // ここでは仮に LINE を開発用とする
    }
  }
}
TypeScript

呼び出し側は、環境のことを一切気にせずにこう書けます。

const notifier = NotifierFactory.create();
notifier.send("こんにちは");
TypeScript

「作り方のルールを 1 カ所に閉じ込める」
これが Factory の大きな価値です。

3つ目:テストで差し替えやすくなる

Factory を interface と組み合わせると、テストも楽になります。

interface Notifier {
  send(message: string): void;
}
TypeScript

テストでは、こんなダミーを使えます。

class DummyNotifier implements Notifier {
  messages: string[] = [];

  send(message: string): void {
    this.messages.push(message);
  }
}
TypeScript

Factory を差し替えられる設計にしておけば、

  • 本番:NotifierFactory が本物の EmailNotifier を返す
  • テスト:テスト用 Factory が DummyNotifier を返す

といったことが簡単にできます。

「new を直接書かない」=「テスト時に差し替えやすい」
というつながりを、頭の片隅に置いておいてください。


シンプルな Factory 関数版もよく使う

クラスではなく「ただの関数」としての Factory

必ずしもクラスにしなくても、
「作るだけの関数」を Factory として使うことも多いです。

interface User {
  id: number;
  name: string;
  isAdmin: boolean;
}

function createUser(name: string): User {
  return {
    id: Date.now(),
    name,
    isAdmin: false,
  };
}

const u = createUser("Taro");
TypeScript

ここでの createUser は、
「User を作るためのルール」を 1 カ所にまとめた Factory 関数です。

もし「ID の採番方法を変えたい」「デフォルトの isAdmin を true にしたい」
となっても、この関数だけを直せば済みます。

「new ではなく、Factory 関数を通して作る」
というだけでも、設計はかなりスッキリします。


Factory パターンと interface の相性

「どの実装を返すか」は Factory に任せる

interface と組み合わせると、Factory の威力がさらに増します。

interface Storage {
  save(key: string, value: string): void;
}

class LocalStorageImpl implements Storage {
  save(key: string, value: string): void {
    localStorage.setItem(key, value);
  }
}

class MemoryStorageImpl implements Storage {
  private data = new Map<string, string>();

  save(key: string, value: string): void {
    this.data.set(key, value);
  }
}
TypeScript

Factory はこう書けます。

class StorageFactory {
  static create(): Storage {
    if (typeof localStorage !== "undefined") {
      return new LocalStorageImpl();
    } else {
      return new MemoryStorageImpl();
    }
  }
}
TypeScript

使う側は、実装の違いを意識せずにこう書けます。

const storage = StorageFactory.create();
storage.save("token", "abc123");
TypeScript

「どのクラスを返すか」は Factory の中だけが知っていて、
呼び出し側は interface だけを見ている

という構造になっています。


いつ Factory パターンを使うべきか

キーワードは「new の条件分岐が増え始めたら」

Factory を使うか迷ったら、
自分のコードをこう眺めてみてください。

  • あちこちで if (...) new A() else new B() していないか
  • 「作り方のルール」が複数の場所にコピペされていないか
  • テストで「本物のクラスを new したくない」と思っていないか

もし心当たりがあるなら、
そこは Factory パターンの候補です。

最初から「全部 Factory にしよう」とする必要はありません。

「new の条件分岐が 2 回、3 回と出てきたら、Factory にまとめる」

くらいの感覚で十分です。


まとめ:Factory パターンを自分の言葉で説明すると

最後に、あなた自身の言葉でこう整理してみてください。

Factory パターンとは、

  • オブジェクトの new をあちこちで書かず、「作る専門の場所(工場)」に集約すること
  • 「どのクラスを new するか」「どう初期化するか」というルールを 1 カ所に閉じ込めること
  • 呼び出し側は Factory に「ください」と言うだけで、具体クラスを意識しなくてよくなること
  • interface と組み合わせると、「実装の差し替え」「テスト用ダミーの注入」がしやすくなること

今のあなたのコードの中で、

if (type === "xxx") new Xxx()
else if (type === "yyy") new Yyy()

のような場所を 1 カ所だけ探してみてください。

そこを、

  • interface を定義して
  • Factory クラス(または関数)に「作り方」を移し
  • 呼び出し側は Factory 経由でインスタンスをもらう

という形に書き換えてみると、
Factory パターンの気持ちよさが、かなりリアルに体感できるはずです。

タイトルとURLをコピーしました