ゴール:「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);
}
}
TypeScriptFactory を差し替えられる設計にしておけば、
- 本番:
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);
}
}
TypeScriptFactory はこう書けます。
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 パターンの気持ちよさが、かなりリアルに体感できるはずです。
