コーディング · デザインパターン
Factory Method パターンを例でわかりやすく解説
結論
Factory Method パターンは、オブジェクトを作成するメソッドを定義しつつ、どの具象クラスをインスタンス化するかをサブクラスに決めさせる生成パターンです。基底クラスは製品のインターフェイスに対してロジックを書き、new の代わりに createTransport() のようなファクトリメソッドを呼びます。これにより、製品を使うコードを編集せずに、サブクラスを追加するだけで新しい種類の製品を加えられます。
目的
オブジェクトを作成するためのインターフェイスを定義しますが、どのクラスをインスタンス化するかはサブクラスに決めさせます。Factory Method パターンは、クラスがインスタンス化をサブクラスに委ねられるようにします。Gang of Four(GoF)の書籍にある5つの生成パターンの1つで、継承に依存するため、クラスを対象とするのはこのパターンだけです。
解決する問題
通知モジュールを想像してください。メッセージを整形し、送信し、結果をログに記録します。最初はメールの送信だけなので、コードは new EmailChannel() を直接呼んでいます。やがてSMS、さらにプッシュ通知が必要になります。整形やログの手順は変わらないのに、チャネルが増えるたびに同じクラスを編集し、if の分岐を追加することになります。
送信のワークフローは安定していて、変わるのはチャネルの種類だけです。Factory Method パターンは、変化する唯一の判断である「どのクラスを作るか」を、オーバーライド可能な1つのメソッドに移します。
構造と登場要素
| 要素 | 役割 | 例では |
|---|---|---|
| Product | ファクトリメソッドが作成するオブジェクトのインターフェイスです。 | Channel |
| ConcreteProduct | Productのインターフェイスを実装します。 | EmailChannel, SmsChannel |
| Creator | ファクトリメソッドを宣言し、製品を使うロジックを含みます。 | Notifier |
| ConcreteCreator | ファクトリメソッドをオーバーライドして、特定のConcreteProductを返します。 | EmailNotifier, SmsNotifier |
TypeScriptでの実装
interface Channel {
send(to: string, text: string): string;
}
class EmailChannel implements Channel {
send(to: string, text: string): string { return `email to ${to}: ${text}`; }
}
class SmsChannel implements Channel {
send(to: string, text: string): string { return `sms to ${to}: ${text.slice(0, 20)}`; }
}
abstract class Notifier {
// The factory method: subclasses choose the concrete product.
protected abstract createChannel(): Channel;
// Shared logic that works with any Channel.
notify(to: string, text: string): void {
const channel = this.createChannel();
const result = channel.send(to, text.trim());
console.log("[log]", result);
}
}
class EmailNotifier extends Notifier {
protected createChannel(): Channel { return new EmailChannel(); }
}
class SmsNotifier extends Notifier {
protected createChannel(): Channel { return new SmsChannel(); }
}
const notifiers: Notifier[] = [new EmailNotifier(), new SmsNotifier()];
for (const n of notifiers) n.notify("ada", " Your order has shipped today ");
// [log] email to ada: Your order has shipped today
// [log] sms to ada: Your order has shippプッシュ通知を追加するには、PushChannel と PushNotifier を書くだけです。notify メソッドと、その呼び出し元はすべて変わりません。
パターンを適用する前は、生成の判断が共通ロジックの中に埋め込まれていました。
class Notifier {
constructor(private kind: "email" | "sms") {}
notify(to: string, text: string): void {
// every new channel edits this method
const result = this.kind === "email"
? `email to ${to}: ${text}`
: `sms to ${to}: ${text.slice(0, 20)}`;
console.log("[log]", result);
}
}より軽い代替案:ファクトリ関数を渡す
Factory Method パターンは継承を使います。TypeScriptでは、生成用の関数をコンストラクタに渡す合成によって、同じ柔軟性が得られることがよくあります。これなら、製品ごとにサブクラスを作る必要がありません。
interface Channel { send(to: string, text: string): string; }
class Notifier {
constructor(private createChannel: () => Channel) {}
notify(to: string, text: string): void {
console.log("[log]", this.createChannel().send(to, text));
}
}
const push = new Notifier(() => ({ send: (to, text) => `push to ${to}: ${text}` }));
push.notify("ada", "Hello"); // [log] push to ada: Hello各クリエーターが別の理由ですでに専用のクラスを持っている場合や、フレームワークが基底クラスの拡張を求める場合は、サブクラス版を使います。バリエーション間の違いが、作成するオブジェクトだけである場合は、関数版を使います。
使うべきとき、使わないとき
次の場合はFactory Method パターンを使います。
- クラスに安定したアルゴリズムがあるが、どの具象オブジェクトを作る必要があるかを事前に知り得ない場合。
- フレームワークやライブラリを書いていて、利用者がサブクラス化によって独自の製品クラスを差し込めるようにしたい場合。
- ある型の生成ロジックを、
newの呼び出しを散らさずに1か所にまとめたい場合。
次の場合は避けます。
- 製品の型が1つしかなく、2つ目が現れる気配もない場合。直接
newを使うほうが明快です。 - テストを可能にすることだけが理由の場合。依存先をコンストラクタ経由で渡すほうが単純です。
- 他のロジックがなく、製品と1対1で対応するだけの、並行したクリエーター階層を作ることになる場合。
実際の利用例
- Javaのコレクション:
Iterable.iterator()は1回だけ宣言され、各コレクションクラス(ArrayList、HashSetなど)がそれをオーバーライドして、自身のイテレータクラスを返します。GoFの書籍は、ポリモーフィックな反復処理におけるFactory Method パターンのこの使い方を紹介しています。 - JavaScriptのイテラブル:すべてのイテラブルは
[Symbol.iterator]()を実装しており、そのコレクションに合った新しいイテレータを返します。 - UIフレームワークでは、コンポーネント、セル、ビューを作成するメソッドをサブクラスがオーバーライドでき、レイアウトとライフサイクルは基底クラスが制御する構成がよくあります。
関連するパターン
| パターン | 違い |
|---|---|
| Abstract Factory パターン | 複数の生成メソッドを持つオブジェクトを通じて、関連する製品のファミリー全体を作成します。そのメソッドは、ファクトリメソッドとして実装されることが多くあります。 |
| Template Method パターン | Factory Method パターンは、上の notify が createChannel を呼ぶように、テンプレートメソッドから呼び出されることがよくあります。 |
| Prototype パターン | クリエーターをサブクラス化する代わりに、インスタンスをコピーしてオブジェクトを作るため、並行するクリエーター階層が不要です。 |
| Builder パターン | どのクラスを作るかではなく、1つの複雑なオブジェクトを段階的に構築することに焦点を当てます。 |
よくある質問
Factory Method パターンとAbstract Factory パターンの違いは何ですか?
Factory Method パターンは、サブクラスがオーバーライドして1つの製品を作る単一のメソッドです。Abstract Factory パターンは、一緒に使うべき関連製品のファミリーを生み出す、複数の生成メソッドを持つオブジェクトです。
単純なファクトリ関数はFactory Method パターンと同じですか?
厳密には違います。内部にswitchを持つ createShape(kind) のような関数は、シンプルファクトリと呼ばれることが多くあります。GoFのFactory Method パターンは、サブクラスが生成メソッドをオーバーライドすることに依存しますが、どちらもクライアントから new を隠します。
Factory Method パターンは開放閉鎖の原則に従っていますか?
はい。新しい製品クラスと新しいクリエーターのサブクラスを追加するだけで製品を追加でき、クリエーターの既存のロジックは変更しません。
ファクトリメソッドは抽象メソッドでなければなりませんか?
いいえ。基底クラスが標準の製品を返す既定の実装を提供し、違うものが必要なときだけサブクラスがオーバーライドしてもかまいません。