ストーリー
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
| 観点 | Strategy | Observer |
|---|---|---|
| 目的 | アルゴリズムの切り替え | 状態変化の通知 |
| 関係 | 1対1 | 1対多 |
| タイミング | 実行前に設定 | イベント発生時に反応 |
| 方向 | 呼び出し側→ストラテジー | Subject→Observer |
高橋アーキテクトのアドバイス:
「Strategy は”どうやるかを選ぶ”、Observer は”何が起きたかを伝える”。この2つは現代のフレームワークのいたるところで使われている。React の state 管理も Observer の一種だし、ミドルウェアの切り替えは Strategy の応用だ」
まとめ
| ポイント | 内容 |
|---|---|
| Strategy | アルゴリズムをオブジェクトとして切り替え可能にする |
| Observer | 状態変化を複数のオブジェクトに自動通知する |
| Strategy の効果 | 条件分岐をポリモーフィズムに置き換え |
| Observer の効果 | 疎結合な1対多の通知メカニズム |
チェックリスト
- Strategy パターンで条件分岐を排除できる
- Observer パターンでイベント通知を実装できる
- 2つのパターンの使い分けを判断できる
次のステップへ
次は Command と Template Method パターンを学びます。操作のオブジェクト化と、処理の骨格の定義です。
推定読了時間: 40分