LESSON 40分

ストーリー

高橋アーキテクト
今日は振る舞いパターンの2つの代表格を学ぼう
高橋アーキテクト
Strategyは”やり方を切り替える”パターン。同じ問題に対して複数のアルゴリズムを差し替え可能にする。Observerは”変化を知らせる”パターン。あるオブジェクトの状態が変わったとき、関連するオブジェクトに自動的に通知する

Strategy パターン

目的

アルゴリズムをインターフェースとして定義し、実行時に切り替え可能にする。条件分岐でアルゴリズムを選択する代わりに、ポリモーフィズムで解決する。

パターンなしの問題

// 問題:アルゴリズムの追加で既存コードが肥大化
class ShippingCalculator {
  calculate(weight: number, distance: number, method: string): number {
    if (method === 'standard') {
      return weight * 10 + distance * 5;
    } else if (method === 'express') {
      return weight * 20 + distance * 10 + 500;
    } else if (method === 'overnight') {
      return weight * 30 + distance * 15 + 1000;
    } else if (method === 'drone') {
      // 新しい配送方法を追加するたびにここが増える
      return weight * 50 + 2000;
    }
    throw new Error(`Unknown method: ${method}`);
  }
}

Strategy の実装

// Strategy インターフェース
interface ShippingStrategy {
  readonly name: string;
  calculate(weight: number, distance: number): number;
}

// 具体的なストラテジー
class StandardShipping implements ShippingStrategy {
  readonly name = 'standard';
  calculate(weight: number, distance: number): number {
    return weight * 10 + distance * 5;
  }
}

class ExpressShipping implements ShippingStrategy {
  readonly name = 'express';
  calculate(weight: number, distance: number): number {
    return weight * 20 + distance * 10 + 500;
  }
}

class OvernightShipping implements ShippingStrategy {
  readonly name = 'overnight';
  calculate(weight: number, distance: number): number {
    return weight * 30 + distance * 15 + 1000;
  }
}

// 新しい配送方法の追加:クラスを追加するだけ
class DroneShipping implements ShippingStrategy {
  readonly name = 'drone';
  calculate(weight: number, distance: number): number {
    return weight * 50 + 2000;
  }
}

// コンテキスト:ストラテジーを使う側
class ShippingCalculator {
  constructor(private strategy: ShippingStrategy) {}

  setStrategy(strategy: ShippingStrategy): void {
    this.strategy = strategy;
  }

  calculate(weight: number, distance: number): number {
    return this.strategy.calculate(weight, distance);
  }
}

// 使い方
const calculator = new ShippingCalculator(new StandardShipping());
console.log(calculator.calculate(5, 100)); // 標準配送

calculator.setStrategy(new ExpressShipping());
console.log(calculator.calculate(5, 100)); // 速達に切り替え

Strategy の実践例:ソート

interface SortStrategy<T> {
  sort(items: T[]): T[];
}

class PriceSortAsc implements SortStrategy<Product> {
  sort(items: Product[]): Product[] {
    return [...items].sort((a, b) => a.price - b.price);
  }
}

class PriceSortDesc implements SortStrategy<Product> {
  sort(items: Product[]): Product[] {
    return [...items].sort((a, b) => b.price - a.price);
  }
}

class NameSort implements SortStrategy<Product> {
  sort(items: Product[]): Product[] {
    return [...items].sort((a, b) => a.name.localeCompare(b.name));
  }
}

class PopularitySort implements SortStrategy<Product> {
  sort(items: Product[]): Product[] {
    return [...items].sort((a, b) => b.salesCount - a.salesCount);
  }
}

class ProductList {
  constructor(
    private products: Product[],
    private sortStrategy: SortStrategy<Product>
  ) {}

  setSortStrategy(strategy: SortStrategy<Product>): void {
    this.sortStrategy = strategy;
  }

  getSorted(): Product[] {
    return this.sortStrategy.sort(this.products);
  }
}

Observer パターン

目的

オブジェクトの状態が変化したときに、依存するすべてのオブジェクトに自動的に通知する。1対多の依存関係を疎結合に実現する。

パターンなしの問題

// 問題:直接呼び出しで密結合
class ShoppingCart {
  addItem(item: CartItem): void {
    this.items.push(item);

    // 通知先が増えるたびにここを修正
    this.uiComponent.updateCartBadge(this.items.length);
    this.analyticsService.trackAddToCart(item);
    this.recommendationEngine.updateSuggestions(item);
    this.inventoryService.decrementPreview(item.id);
  }
}

Observer の実装

// Observer(通知を受ける側)のインターフェース
interface CartObserver {
  onCartUpdated(event: CartEvent): void;
}

// イベントデータ
interface CartEvent {
  type: 'item_added' | 'item_removed' | 'cart_cleared';
  item?: CartItem;
  cartSize: number;
}

// Subject(通知を出す側)
class ShoppingCart {
  private items: CartItem[] = [];
  private observers: CartObserver[] = [];

  subscribe(observer: CartObserver): void {
    this.observers.push(observer);
  }

  unsubscribe(observer: CartObserver): void {
    this.observers = this.observers.filter(o => o !== observer);
  }

  private notify(event: CartEvent): void {
    for (const observer of this.observers) {
      observer.onCartUpdated(event);
    }
  }

  addItem(item: CartItem): void {
    this.items.push(item);
    this.notify({
      type: 'item_added',
      item,
      cartSize: this.items.length,
    });
  }

  removeItem(itemId: string): void {
    const item = this.items.find(i => i.id === itemId);
    this.items = this.items.filter(i => i.id !== itemId);
    this.notify({
      type: 'item_removed',
      item,
      cartSize: this.items.length,
    });
  }

  clear(): void {
    this.items = [];
    this.notify({ type: 'cart_cleared', cartSize: 0 });
  }
}

// 具体的な Observer
class CartBadgeUpdater implements CartObserver {
  onCartUpdated(event: CartEvent): void {
    console.log(`Badge updated: ${event.cartSize} items`);
  }
}

class AnalyticsTracker implements CartObserver {
  onCartUpdated(event: CartEvent): void {
    if (event.type === 'item_added' && event.item) {
      console.log(`Analytics: Added ${event.item.name}`);
    }
  }
}

class RecommendationUpdater implements CartObserver {
  onCartUpdated(event: CartEvent): void {
    if (event.type === 'item_added' && event.item) {
      console.log(`Recommendations updated based on: ${event.item.category}`);
    }
  }
}

// 使い方
const cart = new ShoppingCart();
cart.subscribe(new CartBadgeUpdater());
cart.subscribe(new AnalyticsTracker());
cart.subscribe(new RecommendationUpdater());

cart.addItem({ id: '1', name: 'TypeScript Book', category: 'books', price: 3000 });
// Badge updated: 1 items
// Analytics: Added TypeScript Book
// Recommendations updated based on: books

汎用的な EventEmitter

type EventHandler<T = unknown> = (data: T) => void;

class EventEmitter {
  private handlers: Map<string, EventHandler[]> = new Map();

  on(event: string, handler: EventHandler): void {
    const list = this.handlers.get(event) ?? [];
    list.push(handler);
    this.handlers.set(event, list);
  }

  off(event: string, handler: EventHandler): void {
    const list = this.handlers.get(event) ?? [];
    this.handlers.set(event, list.filter(h => h !== handler));
  }

  emit(event: string, data?: unknown): void {
    const list = this.handlers.get(event) ?? [];
    for (const handler of list) {
      handler(data);
    }
  }
}

// 使い方
const emitter = new EventEmitter();
emitter.on('user:login', (user) => console.log(`Welcome, ${(user as User).name}`));
emitter.on('user:login', (user) => console.log(`Login tracked`));
emitter.emit('user:login', { name: 'Takahashi' });

Strategy vs Observer

観点StrategyObserver
目的アルゴリズムの切り替え状態変化の通知
関係1対11対多
タイミング実行前に設定イベント発生時に反応
方向呼び出し側→ストラテジーSubject→Observer

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

「Strategy は”どうやるかを選ぶ”、Observer は”何が起きたかを伝える”。この2つは現代のフレームワークのいたるところで使われている。React の state 管理も Observer の一種だし、ミドルウェアの切り替えは Strategy の応用だ」


まとめ

ポイント内容
Strategyアルゴリズムをオブジェクトとして切り替え可能にする
Observer状態変化を複数のオブジェクトに自動通知する
Strategy の効果条件分岐をポリモーフィズムに置き換え
Observer の効果疎結合な1対多の通知メカニズム

チェックリスト

  • Strategy パターンで条件分岐を排除できる
  • Observer パターンでイベント通知を実装できる
  • 2つのパターンの使い分けを判断できる

次のステップへ

次は Command と Template Method パターンを学びます。操作のオブジェクト化と、処理の骨格の定義です。


推定読了時間: 40分