コーディング · デザインパターン
Facade パターンを例でわかりやすく解説
結論
Facade パターンは、複雑なサブシステムにある一連のインターフェイスに対して、1つの単純で高レベルなインターフェイスを提供する構造パターンです。在庫、決済、配送、メールの各サービスを正しい順序で呼び出す代わりに、クライアントコードは shop.placeOrder() のような1つのメソッドを呼び、Facadeがサブシステムのクラスを調整します。サブシステムは存在し続け、必要なら直接使えます。Facadeは、よくあるケースを簡単にするだけです。
目的
サブシステムにある一連のインターフェイスに対して、統一されたインターフェイスを提供します。Facade パターンは、サブシステムを使いやすくする、より高レベルのインターフェイスを定義します。Gang of Four(GoF)の書籍にある7つの構造パターンの1つです。
解決する問題
オンラインショップを想像してください。注文するには複数のサービスが必要です。在庫の確認と確保、カードへの請求、決済が失敗した場合の在庫の解放、出荷の作成、確認メールの送信です。各サービスには独自のAPIと独自のルールがあります。
Webのチェックアウト、モバイルAPI、管理ツールなど、すべての呼び出し元がこれらの手順を自分で実行すると、同じ手順があちこちにコピーされます。コピーごとに順序を間違えたり、ロールバックを忘れたりするおそれがあり、すべての呼び出し元が5つのサービスに依存します。1つのサービスが変わると、すべての呼び出し元が変わらなければなりません。Facadeは手順を1か所にまとめ、呼び出し元に1つのメソッドを与えます。
構造と登場要素
| 要素 | 役割 | 例では |
|---|---|---|
| Facade | どのサブシステムのクラスがリクエストを処理するかを知っていて、正しい順序でそれらに委譲します。 | OrderFacade |
| サブシステムのクラス | 実際の作業を行います。Facadeの存在は知りません。 | Inventory, Payments, Shipping, Mailer |
| Client | よくある作業にはFacadeを使い、特別なケースではサブシステムのクラスを直接使うこともあります。 | チェックアウトのコード |
TypeScriptでの実装
// Subsystem classes, each with its own API.
class Inventory {
private stock = new Map([["book", 2]]);
reserve(sku: string): boolean {
const n = this.stock.get(sku) ?? 0;
if (n === 0) return false;
this.stock.set(sku, n - 1);
return true;
}
release(sku: string): void { this.stock.set(sku, (this.stock.get(sku) ?? 0) + 1); }
}
class Payments {
charge(card: string, cents: number): boolean { return card.startsWith("4") && cents > 0; }
}
class Shipping {
createShipment(sku: string, address: string): string { return `SHIP-${sku}-${address.length}`; }
}
class Mailer {
send(to: string, text: string): void { console.log(`mail to ${to}: ${text}`); }
}Facadeは、1つのメソッドの背後で、ワークフロー全体を正しい順序で実行します。
// Facade: one method that runs the whole workflow in the right order.
class OrderFacade {
constructor(private inventory = new Inventory(), private payments = new Payments(),
private shipping = new Shipping(), private mailer = new Mailer()) {}
placeOrder(sku: string, card: string, cents: number, email: string, address: string): string {
if (!this.inventory.reserve(sku)) return "out of stock";
if (!this.payments.charge(card, cents)) {
this.inventory.release(sku); // roll back the reservation
return "payment declined";
}
const shipmentId = this.shipping.createShipment(sku, address);
this.mailer.send(email, `Order confirmed, shipment ${shipmentId}`);
return "ok";
}
}
const shop = new OrderFacade();
console.log(shop.placeOrder("book", "4111", 1999, "[email protected]", "1 Main St"));
// mail to [email protected]: Order confirmed, shipment SHIP-book-9
// ok
console.log(shop.placeOrder("book", "5500", 1999, "[email protected]", "2 Side St")); // payment declined呼び出し元は、1つのメソッドを持つ1つのクラスに依存するだけになりました。ロールバックのルールはちょうど1か所にあります。サブシステムのクラスも引き続き利用でき、在庫を解放するだけでよい管理ツールは、Inventory を直接呼び出せます。
使うべきとき、使わないとき
次の場合はFacade パターンを使います。
- サブシステムが複雑で、ほとんどのクライアントが、その機能のうちの単純でよく使う一部だけを必要とする場合。
- クライアントコードと多くのサブシステムのクラスの間の依存関係の数を減らしたい場合。
- システムを層に整理し、各層への入口としてFacadeを置きたい場合。
- 大きなサードパーティのライブラリをラップし、コードの他の部分には必要な少数の操作だけを使わせたい場合。
次の場合は避けるか、使い方を限定します。
- サブシステムがすでに単純な場合。各呼び出しを1対1で転送するだけのFacadeは、何も隠さずに層を増やすだけです。
- Facadeが、システム中の無関係なメソッドやビジネスルールを集め始めた場合。これは「神オブジェクト」の問題で、1つのクラスが多くを知りすぎ、やりすぎています。焦点を絞った複数のFacadeに分割しましょう。
実際の利用例
- Node.js:
fs.readFile()は、ファイルを開き、最後まで読み、閉じる処理を1回の呼び出しで行います。低レベルのfs.open()、fs.read()、fs.close()も引き続き利用できます。 - jQueryは、DOMや当時のブラウザー間の差異の前に、短く一貫した1つのAPIを置くことで、大きく普及しました。
- GoFの書籍の例はコンパイラです。1つの
Compilerクラスが、スキャナー、パーサー、コードジェネレーターなどのサブシステムのクラスを、単にコードをコンパイルしたいプログラムから隠します。 - バックエンドアプリケーションのサービス層やAPIゲートウェイは、多くの内部サービスに対するFacadeとして機能します。
関連するパターン
| パターン | Facade パターンとの違い |
|---|---|
| Adapter パターン | 既存のインターフェイスを別の既存のインターフェイスに合わせます。Facadeは、多くのクラスに対して新しいより単純なインターフェイスを定義します。 |
| Mediator パターン | こちらも調整を集中させますが、サブシステムのオブジェクトがMediatorを知っていて、それを介して通信します。Facadeでは、サブシステムのクラスはFacadeの存在を知りません。 |
| Abstract Factory パターン | Facadeと併用して、プラットフォームに依存しない方法でサブシステムのオブジェクトを作成できます。 |
| Singleton パターン | サブシステムへの入口は通常1つあれば足りるため、Facadeはしばしば単一のインスタンスになります。 |
| Proxy パターン | 1つのオブジェクトと同じインターフェイスを持ち、それへのアクセスを制御します。Facadeは、多くのオブジェクトに対して、異なるより単純なインターフェイスを持ちます。 |
よくある質問
Facade パターンとAdapter パターンの違いは何ですか?
Adapterは、既存のインターフェイスを、クライアントがすでに期待している別のインターフェイスに変換します。Facadeは、サブシステム全体を使いやすくするために、新しいより単純なインターフェイスを考案します。
Facadeはサブシステムを完全に隠しますか?
いいえ。Facadeはよくある作業を簡単にしますが、より細かい制御が必要なクライアントは、サブシステムのクラスを直接使えます。
1つのシステムに複数のFacadeを置けますか?
はい。大きなシステムでは、サブシステムごと、またはユースケースごとに1つのFacadeを置くことが多く、各Facadeを小さく、焦点の絞られたものに保てます。
サービス層はFacadeですか?
多くの場合はそうです。リポジトリ、外部API、通知を1回の呼び出しの背後で調整するサービスメソッドは、それを呼び出すコードに対してFacadeの役割を果たします。