JavaScriptファクトリパターン入門:クラス版と関数版を比較する
GoFのFactoryパターンは「どのクラスをインスタンス化するかを呼び出し側に知らせない」設計です。 JavaScriptに持ち込むと、クラスを使わないファクトリ関数という形に自然と落ち着きます。 この記事ではGoF的なクラス版と、JavaScriptらしい関数版を比較します。
Factoryパターンとは
オブジェクトの生成処理を1か所にまとめ、呼び出し側がどのクラスを使うか意識しなくてよい状態を作るパターンです。
典型的な用途: - 種別(type)に応じて異なるオブジェクトを返す - 生成ロジックが複雑で呼び出し側に書きたくない - テスト時にモックオブジェクトに差し替えやすくする
クラス版(GoFの教科書的実装)
JavaやC++で示される実装をJavaScriptに移植するとこうなります。
class EmailNotification {
send(message) {
console.log(`Email: ${message}`);
}
}
class PushNotification {
send(message) {
console.log(`Push: ${message}`);
}
}
class SlackNotification {
send(message) {
console.log(`Slack: ${message}`);
}
}
class NotificationFactory {
create(type) {
if (type === 'email') return new EmailNotification();
if (type === 'push') return new PushNotification();
if (type === 'slack') return new SlackNotification();
throw new Error(`Unknown type: ${type}`);
}
}
const factory = new NotificationFactory();
const notification = factory.create('email');
notification.send('パスワードが変更されました');
クラスの継承構造を前提にしているため、種別を追加するたびにクラスの定義とファクトリの分岐の両方を更新する必要があります。
関数版(JavaScriptらしい実装)
JavaScriptでは「オブジェクトを返す関数」がそのままファクトリです。 クラスも継承も不要です。
const createNotification = (type) => {
const handlers = {
email: (message) => console.log(`Email: ${message}`),
push: (message) => console.log(`Push: ${message}`),
slack: (message) => console.log(`Slack: ${message}`),
};
const send = handlers[type];
if (!send) throw new Error(`Unknown type: ${type}`);
return { send };
};
const notification = createNotification('slack');
notification.send('デプロイが完了しました');
種別を追加したい場合は handlers オブジェクトにエントリを追加するだけです。
実用例:APIクライアントの切り替え
環境(開発・本番)やユーザーのプランに応じてクライアントを切り替える場面でよく使います。
const createApiClient = (env) => {
if (env === 'development') {
return {
get: async (path) => {
console.log(`[mock] GET ${path}`);
return { data: null };
},
};
}
return {
get: async (path) => {
const res = await fetch(`https://api.example.com${path}`);
return res.json();
},
};
};
const client = createApiClient(process.env.NODE_ENV);
const data = await client.get('/users');
呼び出し側は createApiClient() の戻り値に .get() があることだけ知っていればよく、
モックか本物かを意識しません。テスト時にもモック差し替えが簡単です。
2つの実装を比較する
| 比較項目 | クラス版 | 関数版 |
|---|---|---|
| 実装量 | 多い(クラス定義 + ファクトリクラス) | 少ない(関数1つ) |
| 種別追加 | クラス定義とif文の両方を修正 | オブジェクトにエントリ追加のみ |
| 継承・多態性 | 利用する | 不要 |
| テストのしやすさ | モッククラスの作成が必要 | 関数の差し替えで対応 |
| TypeScriptとの相性 | インターフェースで型を統一できる | ユニオン型・型ガードで対応 |
| 使う場面 | NestJSなどDIフレームワーク内 | フロントエンド・素のNode.js |
TypeScriptで型を付ける
関数版はTypeScriptとも相性がよいです。
type NotificationType = 'email' | 'push' | 'slack';
interface Notification {
send: (message: string) => void;
}
const createNotification = (type: NotificationType): Notification => {
const handlers: Record<NotificationType, Notification> = {
email: { send: (msg) => console.log(`Email: ${msg}`) },
push: { send: (msg) => console.log(`Push: ${msg}`) },
slack: { send: (msg) => console.log(`Slack: ${msg}`) },
};
return handlers[type];
};
NotificationType に種別を追加すると handlers の実装漏れをコンパイル時に検出できます。
まとめ
- GoFのFactoryはクラス継承を前提とした設計
- JavaScriptでは「オブジェクトを返す関数」がそのままファクトリになる
- 関数版は実装量が少なく、種別の追加・テストの差し替えが簡単
- クラス版はNestJSなどDIフレームワークを使う場面で活きる
- TypeScriptと組み合わせると型で実装漏れを検出できる