ストーリー
Facade パターン
目的
複雑なサブシステムに対して、統一されたシンプルなインターフェースを提供する。
パターンなしの問題
// 呼び出し側がすべてのサブシステムを知っている
class OrderController {
async placeOrder(orderData: OrderData): Promise<void> {
const inventory = new InventoryService();
const payment = new PaymentService();
const shipping = new ShippingService();
const notification = new NotificationService();
const loyalty = new LoyaltyService();
// 在庫チェック
for (const item of orderData.items) {
const available = await inventory.checkStock(item.productId, item.quantity);
if (!available) throw new Error(`Out of stock: ${item.productId}`);
}
// 在庫確保
for (const item of orderData.items) {
await inventory.reserveStock(item.productId, item.quantity);
}
// 決済
const paymentResult = await payment.charge(orderData.userId, orderData.total);
if (!paymentResult.success) {
// 在庫確保を戻す
for (const item of orderData.items) {
await inventory.releaseStock(item.productId, item.quantity);
}
throw new Error('Payment failed');
}
// 配送手配
await shipping.createShipment(orderData);
// メール送信
await notification.sendOrderConfirmation(orderData.userId, orderData);
// ポイント付与
await loyalty.addPoints(orderData.userId, Math.floor(orderData.total * 0.01));
}
}
Facade の実装
// Facade:複雑な処理をシンプルなインターフェースに
class OrderFacade {
constructor(
private inventory: InventoryService,
private payment: PaymentService,
private shipping: ShippingService,
private notification: NotificationService,
private loyalty: LoyaltyService
) {}
async placeOrder(orderData: OrderData): Promise<OrderResult> {
await this.checkAndReserveInventory(orderData.items);
try {
const paymentResult = await this.processPayment(orderData);
await this.arrangeShipping(orderData);
await this.notifyCustomer(orderData);
await this.rewardCustomer(orderData);
return { success: true, orderId: paymentResult.transactionId };
} catch (error) {
await this.rollbackInventory(orderData.items);
throw error;
}
}
private async checkAndReserveInventory(items: OrderItem[]): Promise<void> {
for (const item of items) {
const available = await this.inventory.checkStock(item.productId, item.quantity);
if (!available) throw new Error(`Out of stock: ${item.productId}`);
}
for (const item of items) {
await this.inventory.reserveStock(item.productId, item.quantity);
}
}
private async processPayment(order: OrderData): Promise<PaymentResult> {
const result = await this.payment.charge(order.userId, order.total);
if (!result.success) throw new Error('Payment failed');
return result;
}
private async arrangeShipping(order: OrderData): Promise<void> {
await this.shipping.createShipment(order);
}
private async notifyCustomer(order: OrderData): Promise<void> {
await this.notification.sendOrderConfirmation(order.userId, order);
}
private async rewardCustomer(order: OrderData): Promise<void> {
const points = Math.floor(order.total * 0.01);
await this.loyalty.addPoints(order.userId, points);
}
private async rollbackInventory(items: OrderItem[]): Promise<void> {
for (const item of items) {
await this.inventory.releaseStock(item.productId, item.quantity);
}
}
}
// 呼び出し側はシンプル
class OrderController {
constructor(private orderFacade: OrderFacade) {}
async placeOrder(orderData: OrderData): Promise<OrderResult> {
return this.orderFacade.placeOrder(orderData);
}
}
Composite パターン
目的
部分と全体を同じインターフェースで扱えるようにする。木構造(ツリー構造)を統一的に操作できる。
ユースケース:メニューシステム
// 共通インターフェース(部分と全体を統一)
interface MenuComponent {
getName(): string;
getPrice(): number;
print(indent?: number): string;
}
// 葉(Leaf):個々のメニューアイテム
class MenuItem implements MenuComponent {
constructor(
private name: string,
private price: number,
private description: string
) {}
getName(): string { return this.name; }
getPrice(): number { return this.price; }
print(indent: number = 0): string {
const pad = ' '.repeat(indent);
return `${pad}${this.name} - ${this.price}円: ${this.description}`;
}
}
// 複合要素(Composite):メニューカテゴリ(子要素を持てる)
class MenuCategory implements MenuComponent {
private children: MenuComponent[] = [];
constructor(private name: string) {}
add(component: MenuComponent): void {
this.children.push(component);
}
remove(component: MenuComponent): void {
const index = this.children.indexOf(component);
if (index >= 0) this.children.splice(index, 1);
}
getName(): string { return this.name; }
// 全子要素の合計金額
getPrice(): number {
return this.children.reduce((sum, child) => sum + child.getPrice(), 0);
}
print(indent: number = 0): string {
const pad = ' '.repeat(indent);
const header = `${pad}[${this.name}]`;
const childrenStr = this.children
.map(child => child.print(indent + 2))
.join('\n');
return `${header}\n${childrenStr}`;
}
}
// 使い方
const mainMenu = new MenuCategory('Main Menu');
const drinks = new MenuCategory('Drinks');
drinks.add(new MenuItem('Coffee', 400, 'Drip coffee'));
drinks.add(new MenuItem('Tea', 350, 'Green tea'));
drinks.add(new MenuItem('Juice', 300, 'Orange juice'));
const food = new MenuCategory('Food');
const pasta = new MenuCategory('Pasta');
pasta.add(new MenuItem('Carbonara', 1200, 'Creamy pasta'));
pasta.add(new MenuItem('Peperoncino', 900, 'Garlic & chili'));
food.add(pasta);
food.add(new MenuItem('Salad', 600, 'Caesar salad'));
mainMenu.add(drinks);
mainMenu.add(food);
// 全体を統一的に操作
console.log(mainMenu.print());
console.log(`Total: ${mainMenu.getPrice()}円`); // 全メニューの合計
出力例
[Main Menu]
[Drinks]
Coffee - 400円: Drip coffee
Tea - 350円: Green tea
Juice - 300円: Orange juice
[Food]
[Pasta]
Carbonara - 1200円: Creamy pasta
Peperoncino - 900円: Garlic & chili
Salad - 600円: Caesar salad
Total: 3750円
もう1つの例:ファイルシステム
interface FileSystemNode {
getName(): string;
getSize(): number;
list(indent?: number): string;
}
class File implements FileSystemNode {
constructor(private name: string, private size: number) {}
getName(): string { return this.name; }
getSize(): number { return this.size; }
list(indent: number = 0): string {
return `${' '.repeat(indent)}${this.name} (${this.size}KB)`;
}
}
class Directory implements FileSystemNode {
private children: FileSystemNode[] = [];
constructor(private name: string) {}
add(node: FileSystemNode): void { this.children.push(node); }
getName(): string { return this.name; }
getSize(): number {
return this.children.reduce((sum, child) => sum + child.getSize(), 0);
}
list(indent: number = 0): string {
const header = `${' '.repeat(indent)}${this.name}/`;
const items = this.children.map(c => c.list(indent + 2)).join('\n');
return `${header}\n${items}`;
}
}
Facade vs Composite
| 観点 | Facade | Composite |
|---|---|---|
| 目的 | 複雑さを隠蔽する | 部分と全体を統一する |
| 構造 | フラットな委譲 | 木構造(再帰) |
| 使用場面 | サブシステム群の統合 | 階層構造の操作 |
高橋アーキテクトのアドバイス:
「Facadeは”複雑さから守る盾”。Compositeは”部分と全体を同じに扱える魔法”。どちらも日常的によく使うパターンだ」
まとめ
| ポイント | 内容 |
|---|---|
| Facade | 複雑なサブシステムにシンプルな窓口を提供 |
| Composite | 木構造を統一インターフェースで操作 |
| Facade の効果 | 呼び出し側の複雑さを大幅に削減 |
| Composite の効果 | 部分と全体を同じように扱える |
チェックリスト
- Facade パターンで複雑な処理をシンプルにできる
- Composite パターンで木構造を設計できる
- 2つのパターンの違いを説明できる
次のステップへ
次は振る舞いパターンの Strategy と Observer を学びます。アルゴリズムの交換とイベント通知の仕組みです。
推定読了時間: 40分