LESSON 40分

ストーリー

高橋アーキテクト
ECサイトで”注文確定”ボタンを押すと、在庫チェック、決済処理、配送手配、メール送信が一気に動く。でも呼び出し側がこの全部を知っている必要があると思う?
あなた
それは…大変ですね
高橋アーキテクト
Facadeは、複雑なサブシステムにシンプルな窓口を提供するパターンだ。そしてCompositeは、フォルダとファイルの関係のように、部分と全体を同じように扱えるパターンだ

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

観点FacadeComposite
目的複雑さを隠蔽する部分と全体を統一する
構造フラットな委譲木構造(再帰)
使用場面サブシステム群の統合階層構造の操作

高橋アーキテクトのアドバイス:

「Facadeは”複雑さから守る盾”。Compositeは”部分と全体を同じに扱える魔法”。どちらも日常的によく使うパターンだ」


まとめ

ポイント内容
Facade複雑なサブシステムにシンプルな窓口を提供
Composite木構造を統一インターフェースで操作
Facade の効果呼び出し側の複雑さを大幅に削減
Composite の効果部分と全体を同じように扱える

チェックリスト

  • Facade パターンで複雑な処理をシンプルにできる
  • Composite パターンで木構造を設計できる
  • 2つのパターンの違いを説明できる

次のステップへ

次は振る舞いパターンの Strategy と Observer を学びます。アルゴリズムの交換とイベント通知の仕組みです。


推定読了時間: 40分