Skilore

Strategy パターン

アルゴリズムのファミリーを定義し、それぞれを **カプセル化** して交換可能にする振る舞いパターン。実行時にアルゴリズムを切り替えられ、条件分岐の爆発を防止してOpen/Closed Principle を遵守する。

81 分で読めます40,017 文字

Strategy パターン

アルゴリズムのファミリーを定義し、それぞれを カプセル化 して交換可能にする振る舞いパターン。実行時にアルゴリズムを切り替えられ、条件分岐の爆発を防止してOpen/Closed Principle を遵守する。


この章で学ぶこと

  1. Strategy パターンの構造と、条件分岐の排除によるOCP準拠の設計手法を理解する
  2. DI(依存性注入)との関係、関数型アプローチでの実現方法、Registry パターンとの組み合わせを習得する
  3. Strategy の過剰適用を避ける判断基準と、Template Method/State パターンとの使い分けを身につける

前提知識

このガイドを読む前に、以下の概念を理解しておくことを推奨します。

前提知識 説明 参照リンク
SOLID 原則(特にOCP) 拡張に開き、修正に閉じる原則 SOLID 原則
インタフェースとポリモーフィズム 異なる実装を統一的に扱う概念 クリーンコード
依存性注入(DI) 外部から依存オブジェクトを注入する手法 DI/IoC
関数(第一級オブジェクト) 関数を値として扱い、引数や返り値にする概念 関数型パターン

1. Strategy パターンとは何か

1.1 解決する問題

ソフトウェアでは、同じ種類の処理に複数のアルゴリズムが存在する場面が多い。

  • 料金計算: 通常/プレミアム/学生/シニア
  • ソート: 名前順/日付順/価格順/関連度順
  • 認証: パスワード/OAuth/SAML/APIキー
  • 圧縮: gzip/zstd/lz4/brotli

これらを if/elseswitch で分岐すると、アルゴリズムが増えるたびに条件分岐が肥大化し、既存コードの変更が必要になる(OCP 違反)。

BEFORE(条件分岐の肥大化):
function calculate(type, price) {
  if (type === "regular") return price;
  else if (type === "premium") return price * 0.9;
  else if (type === "student") return price * 0.7;
  else if (type === "senior") return price * 0.8;
  else if (type === "vip") return price * 0.6;    // 追加1
  else if (type === "family") return price * 0.75; // 追加2
  // ... 新しい種類のたびにこの関数を修正 -> OCP 違反
}

AFTER(Strategy パターン):
strategies.get(type).calculate(price);
// 新しい型は register するだけ -> OCP 準拠

1.2 パターンの意図

GoF の定義:

Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use it.

日本語訳:

アルゴリズムのファミリーを定義し、それぞれをカプセル化して交換可能にする。Strategy パターンにより、アルゴリズムをクライアントから独立して変更できる。

1.3 WHY: なぜ Strategy パターンが必要なのか

根本的な理由は アルゴリズムの選択と実行を分離する ことにある。

  1. Open/Closed Principle: 新しいアルゴリズムの追加が既存コードの変更なしに行える
  2. Single Responsibility Principle: 各アルゴリズムが独立したクラス/関数として存在し、個別にテスト可能
  3. 実行時の切り替え: 同じ Context で異なるアルゴリズムを動的に切り替えられる
  4. テスタビリティ: Strategy をモック/スタブに差し替えることでテストが容易

2. Strategy の構造

2.1 クラス図

+-------------+       +-------------------+
|   Context   |------>|   Strategy        |
+-------------+       |   (interface)     |
| - strategy  |       +-------------------+
| + execute() |       | + execute(data)   |
+-------------+       +-------------------+
                              ^
                       _______|_______
                      |               |
               +------------+  +------------+
               | StrategyA  |  | StrategyB  |
               +------------+  +------------+
               | + execute() |  | + execute() |
               +------------+  +------------+

2.2 構成要素の役割

構成要素 役割 責務
Strategy (Interface) アルゴリズムの共通契約 メソッドシグネチャを定義
ConcreteStrategy 具体的なアルゴリズム実装 Strategy インタフェースに従って実装
Context Strategy を使用する側 Strategy の参照を保持し、委譲する
Client Context と Strategy を組み立て 具体的な Strategy を Context に注入

2.3 処理シーケンス

Client            Context              Strategy
  |                  |                     |
  |-- new Context(strategyA) -->|          |
  |                  |-- setStrategy(A) -->|
  |                  |                     |
  |-- execute() ---->|                     |
  |                  |-- execute(data) --->|  StrategyA
  |                  |<--- result ---------|
  |<-- result -------|                     |
  |                  |                     |
  |-- setStrategy(B)->|                    |
  |                  |                     |
  |-- execute() ---->|                     |
  |                  |-- execute(data) --->|  StrategyB
  |                  |<--- result ---------|
  |<-- result -------|                     |

3. コード例

コード例 1: 料金計算 Strategy(TypeScript)

// pricing-strategy.ts — Strategy パターンの基本形
interface PricingStrategy {
  readonly name: string;
  calculate(basePrice: number): number;
  getDescription(): string;
}
 
class RegularPricing implements PricingStrategy {
  readonly name = "regular";
  calculate(basePrice: number): number {
    return basePrice;
  }
  getDescription(): string {
    return "通常価格(割引なし)";
  }
}
 
class PremiumPricing implements PricingStrategy {
  readonly name = "premium";
  calculate(basePrice: number): number {
    return Math.round(basePrice * 0.9); // 10%割引
  }
  getDescription(): string {
    return "プレミアム会員価格(10%OFF)";
  }
}
 
class StudentPricing implements PricingStrategy {
  readonly name = "student";
  calculate(basePrice: number): number {
    return Math.round(basePrice * 0.7); // 30%割引
  }
  getDescription(): string {
    return "学生価格(30%OFF)";
  }
}
 
class SeniorPricing implements PricingStrategy {
  readonly name = "senior";
  calculate(basePrice: number): number {
    return Math.round(basePrice * 0.8); // 20%割引
  }
  getDescription(): string {
    return "シニア価格(20%OFF)";
  }
}
 
// Context: ショッピングカート
class ShoppingCart {
  private items: { name: string; price: number; quantity: number }[] = [];
  private pricingStrategy: PricingStrategy;
 
  constructor(pricingStrategy: PricingStrategy = new RegularPricing()) {
    this.pricingStrategy = pricingStrategy;
  }
 
  setPricingStrategy(strategy: PricingStrategy): void {
    this.pricingStrategy = strategy;
    console.log(`Pricing changed to: ${strategy.getDescription()}`);
  }
 
  addItem(name: string, price: number, quantity: number = 1): void {
    this.items.push({ name, price, quantity });
  }
 
  checkout(): { subtotal: number; discount: number; total: number; strategy: string } {
    const subtotal = this.items.reduce((sum, i) => sum + i.price * i.quantity, 0);
    const total = this.pricingStrategy.calculate(subtotal);
    return {
      subtotal,
      discount: subtotal - total,
      total,
      strategy: this.pricingStrategy.name,
    };
  }
}
 
// --- 使用例: 実行時に戦略を切り替え ---
const cart = new ShoppingCart();
cart.addItem("TypeScript Book", 3000);
cart.addItem("Design Patterns Book", 4000);
 
console.log(cart.checkout());
// { subtotal: 7000, discount: 0, total: 7000, strategy: "regular" }
 
cart.setPricingStrategy(new StudentPricing());
// "Pricing changed to: 学生価格(30%OFF)"
 
console.log(cart.checkout());
// { subtotal: 7000, discount: 2100, total: 4900, strategy: "student" }
 
cart.setPricingStrategy(new PremiumPricing());
console.log(cart.checkout());
// { subtotal: 7000, discount: 700, total: 6300, strategy: "premium" }

コード例 2: 関数型 Strategy(TypeScript)

// functional-strategy.ts — クラスを使わず関数で Strategy を実現
interface User {
  name: string;
  age: number;
  email: string;
  createdAt: Date;
  score: number;
}
 
// Strategy を関数型で定義
type SortStrategy<T> = (a: T, b: T) => number;
 
const byName: SortStrategy<User> = (a, b) =>
  a.name.localeCompare(b.name);
 
const byAge: SortStrategy<User> = (a, b) =>
  a.age - b.age;
 
const byCreatedDesc: SortStrategy<User> = (a, b) =>
  b.createdAt.getTime() - a.createdAt.getTime();
 
const byScore: SortStrategy<User> = (a, b) =>
  b.score - a.score;
 
// 合成: 複数のソート条件を組み合わせる
function composeStrategies<T>(...strategies: SortStrategy<T>[]): SortStrategy<T> {
  return (a, b) => {
    for (const strategy of strategies) {
      const result = strategy(a, b);
      if (result !== 0) return result;
    }
    return 0;
  };
}
 
// 反転: 降順にする
function reverse<T>(strategy: SortStrategy<T>): SortStrategy<T> {
  return (a, b) => -strategy(a, b);
}
 
// Context 関数
function sortUsers(users: User[], strategy: SortStrategy<User>): User[] {
  return [...users].sort(strategy);
}
 
// --- 使用例 ---
const users: User[] = [
  { name: "Charlie", age: 30, email: "c@test.com", createdAt: new Date("2024-01"), score: 85 },
  { name: "Alice", age: 25, email: "a@test.com", createdAt: new Date("2024-03"), score: 92 },
  { name: "Bob", age: 30, email: "b@test.com", createdAt: new Date("2024-02"), score: 88 },
];
 
// 単一ソート
console.log(sortUsers(users, byName).map(u => u.name));
// ["Alice", "Bob", "Charlie"]
 
console.log(sortUsers(users, byScore).map(u => u.name));
// ["Alice", "Bob", "Charlie"]
 
// 合成ソート: 年齢順 -> 名前順(同じ年齢の場合)
const byAgeThenName = composeStrategies(byAge, byName);
console.log(sortUsers(users, byAgeThenName).map(u => u.name));
// ["Alice", "Bob", "Charlie"]
 
// 反転: 名前の逆順
console.log(sortUsers(users, reverse(byName)).map(u => u.name));
// ["Charlie", "Bob", "Alice"]

コード例 3: バリデーション Strategy(TypeScript)

// validation-strategy.ts — フォームバリデーション
interface ValidationResult {
  valid: boolean;
  errors: string[];
}
 
interface ValidationStrategy {
  validate(value: string): ValidationResult;
  readonly fieldName: string;
}
 
class EmailValidation implements ValidationStrategy {
  readonly fieldName = "email";
 
  validate(value: string): ValidationResult {
    const errors: string[] = [];
    if (!value) errors.push("Email is required");
    else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
      errors.push("Invalid email format");
    }
    return { valid: errors.length === 0, errors };
  }
}
 
class PasswordValidation implements ValidationStrategy {
  readonly fieldName = "password";
 
  constructor(
    private options: {
      minLength?: number;
      requireUppercase?: boolean;
      requireDigit?: boolean;
      requireSpecial?: boolean;
    } = {}
  ) {}
 
  validate(value: string): ValidationResult {
    const errors: string[] = [];
    const { minLength = 8, requireUppercase = true, requireDigit = true, requireSpecial = false } = this.options;
 
    if (value.length < minLength) errors.push(`Minimum ${minLength} characters`);
    if (requireUppercase && !/[A-Z]/.test(value)) errors.push("Need uppercase letter");
    if (requireDigit && !/[0-9]/.test(value)) errors.push("Need digit");
    if (requireSpecial && !/[!@#$%^&*]/.test(value)) errors.push("Need special character");
 
    return { valid: errors.length === 0, errors };
  }
}
 
class PhoneValidation implements ValidationStrategy {
  readonly fieldName = "phone";
 
  constructor(private country: 'JP' | 'US' = 'JP') {}
 
  validate(value: string): ValidationResult {
    const errors: string[] = [];
    const patterns = {
      JP: /^0\d{1,4}-?\d{1,4}-?\d{4}$/,
      US: /^\+?1?[-.\s]?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}$/,
    };
 
    if (!value) errors.push("Phone number is required");
    else if (!patterns[this.country].test(value)) {
      errors.push(`Invalid ${this.country} phone number format`);
    }
 
    return { valid: errors.length === 0, errors };
  }
}
 
// Context: フォームフィールド
class FormField {
  private strategies: ValidationStrategy[] = [];
 
  constructor(
    private name: string,
    ...strategies: ValidationStrategy[]
  ) {
    this.strategies = strategies;
  }
 
  validate(value: string): ValidationResult {
    const allErrors: string[] = [];
    for (const strategy of this.strategies) {
      const result = strategy.validate(value);
      allErrors.push(...result.errors);
    }
    return { valid: allErrors.length === 0, errors: allErrors };
  }
}
 
// --- 使用例 ---
const emailField = new FormField("email", new EmailValidation());
console.log(emailField.validate("test@example.com"));
// { valid: true, errors: [] }
 
console.log(emailField.validate("invalid-email"));
// { valid: false, errors: ["Invalid email format"] }
 
const passwordField = new FormField(
  "password",
  new PasswordValidation({ minLength: 10, requireSpecial: true })
);
console.log(passwordField.validate("short"));
// { valid: false, errors: ["Minimum 10 characters", "Need uppercase letter", "Need digit", "Need special character"] }
 
console.log(passwordField.validate("MyP@ssw0rd!!"));
// { valid: true, errors: [] }

コード例 4: Python ── Protocol ベースの Strategy

# compression_strategy.py — Python Protocol による Strategy
from typing import Protocol
import gzip
import time
 
 
class CompressionStrategy(Protocol):
    """圧縮戦略のプロトコル(インタフェース)"""
    @property
    def name(self) -> str: ...
    def compress(self, data: bytes) -> bytes: ...
    def decompress(self, data: bytes) -> bytes: ...
 
 
class GzipCompression:
    name = "gzip"
 
    def compress(self, data: bytes) -> bytes:
        return gzip.compress(data)
 
    def decompress(self, data: bytes) -> bytes:
        return gzip.decompress(data)
 
 
class NoCompression:
    name = "none"
 
    def compress(self, data: bytes) -> bytes:
        return data
 
    def decompress(self, data: bytes) -> bytes:
        return data
 
 
class FileProcessor:
    """Context: ファイル処理器"""
    def __init__(self, compression: CompressionStrategy):
        self._compression = compression
 
    def set_compression(self, compression: CompressionStrategy) -> None:
        self._compression = compression
        print(f"Compression changed to: {compression.name}")
 
    def save(self, data: bytes, path: str) -> dict:
        start = time.time()
        compressed = self._compression.compress(data)
        elapsed = time.time() - start
 
        with open(path, "wb") as f:
            f.write(compressed)
 
        ratio = len(compressed) / len(data) * 100
        return {
            "original_size": len(data),
            "compressed_size": len(compressed),
            "ratio": f"{ratio:.1f}%",
            "time_ms": f"{elapsed * 1000:.2f}",
            "algorithm": self._compression.name,
        }
 
    def load(self, path: str) -> bytes:
        with open(path, "rb") as f:
            compressed = f.read()
        return self._compression.decompress(compressed)
 
 
# --- 使用例 ---
data = b"Hello " * 1000  # 6000 bytes の繰り返しデータ
 
processor = FileProcessor(GzipCompression())
result = processor.save(data, "/tmp/data.gz")
print(result)
# {"original_size": 6000, "compressed_size": ~40, "ratio": "0.7%", ...}
 
processor.set_compression(NoCompression())
result = processor.save(data, "/tmp/data.raw")
print(result)
# {"original_size": 6000, "compressed_size": 6000, "ratio": "100.0%", ...}

コード例 5: Strategy の動的選択(Registry パターン)

// strategy-registry.ts — Registry + Strategy
class StrategyRegistry<T> {
  private strategies = new Map<string, T>();
  private defaultKey: string | null = null;
 
  register(name: string, strategy: T, isDefault: boolean = false): this {
    this.strategies.set(name, strategy);
    if (isDefault) this.defaultKey = name;
    return this;
  }
 
  get(name: string): T {
    const strategy = this.strategies.get(name);
    if (strategy) return strategy;
 
    // デフォルト戦略があれば返す
    if (this.defaultKey) {
      return this.strategies.get(this.defaultKey)!;
    }
 
    throw new Error(`Strategy "${name}" not found. Available: ${this.getAvailableNames().join(', ')}`);
  }
 
  has(name: string): boolean {
    return this.strategies.has(name);
  }
 
  getAvailableNames(): string[] {
    return [...this.strategies.keys()];
  }
}
 
// --- 料金計算の Registry ---
const pricingRegistry = new StrategyRegistry<PricingStrategy>();
pricingRegistry
  .register("regular", new RegularPricing(), true) // デフォルト
  .register("premium", new PremiumPricing())
  .register("student", new StudentPricing())
  .register("senior", new SeniorPricing());
 
// APIリクエストから動的に選択
function handleCheckout(req: { membershipType: string; items: any[] }) {
  const strategy = pricingRegistry.get(req.membershipType);
  const cart = new ShoppingCart(strategy);
  // ...
}
 
// 利用可能な戦略の一覧
console.log(pricingRegistry.getAvailableNames());
// ["regular", "premium", "student", "senior"]

コード例 6: HTTP リトライ Strategy

// retry-strategy.ts — リトライアルゴリズムの Strategy
interface RetryStrategy {
  readonly name: string;
  getDelay(attempt: number, baseDelay: number): number;
  shouldRetry(attempt: number, maxAttempts: number, error: Error): boolean;
}
 
class LinearRetry implements RetryStrategy {
  readonly name = "linear";
  getDelay(attempt: number, baseDelay: number): number {
    return baseDelay * attempt;
  }
  shouldRetry(attempt: number, maxAttempts: number): boolean {
    return attempt < maxAttempts;
  }
}
 
class ExponentialBackoff implements RetryStrategy {
  readonly name = "exponential";
  getDelay(attempt: number, baseDelay: number): number {
    return baseDelay * Math.pow(2, attempt - 1);
  }
  shouldRetry(attempt: number, maxAttempts: number): boolean {
    return attempt < maxAttempts;
  }
}
 
class ExponentialWithJitter implements RetryStrategy {
  readonly name = "exponential-jitter";
  getDelay(attempt: number, baseDelay: number): number {
    const exponentialDelay = baseDelay * Math.pow(2, attempt - 1);
    const jitter = Math.random() * exponentialDelay;
    return Math.floor(jitter);
  }
  shouldRetry(attempt: number, maxAttempts: number, error: Error): boolean {
    // 4xx エラーはリトライしない(クライアントエラー)
    if ('statusCode' in error && (error as any).statusCode >= 400 && (error as any).statusCode < 500) {
      return false;
    }
    return attempt < maxAttempts;
  }
}
 
// Context: HTTP クライアント
class ResilientHttpClient {
  constructor(
    private retryStrategy: RetryStrategy,
    private maxAttempts: number = 3,
    private baseDelay: number = 1000
  ) {}
 
  async request(url: string, options?: RequestInit): Promise<Response> {
    let lastError: Error | null = null;
 
    for (let attempt = 1; attempt <= this.maxAttempts; attempt++) {
      try {
        const response = await fetch(url, options);
        if (response.ok) return response;
        throw Object.assign(new Error(`HTTP ${response.status}`), { statusCode: response.status });
      } catch (error) {
        lastError = error as Error;
        console.log(`[${this.retryStrategy.name}] Attempt ${attempt} failed: ${lastError.message}`);
 
        if (!this.retryStrategy.shouldRetry(attempt, this.maxAttempts, lastError)) {
          break;
        }
 
        const delay = this.retryStrategy.getDelay(attempt, this.baseDelay);
        console.log(`Retrying in ${delay}ms...`);
        await new Promise(resolve => setTimeout(resolve, delay));
      }
    }
 
    throw lastError;
  }
}
 
// --- 使用例 ---
// 開発環境: リニアリトライ(予測しやすい)
const devClient = new ResilientHttpClient(new LinearRetry(), 3, 500);
 
// 本番環境: 指数バックオフ + ジッター(サーバー負荷分散)
const prodClient = new ResilientHttpClient(new ExponentialWithJitter(), 5, 1000);

コード例 7: ロギング Strategy

// logging-strategy.ts — ログ出力先の Strategy
interface LogEntry {
  level: 'debug' | 'info' | 'warn' | 'error';
  message: string;
  timestamp: Date;
  context?: Record<string, unknown>;
}
 
interface LoggingStrategy {
  log(entry: LogEntry): void;
  flush?(): Promise<void>;
}
 
class ConsoleLogging implements LoggingStrategy {
  log(entry: LogEntry): void {
    const prefix = `[${entry.timestamp.toISOString()}] [${entry.level.toUpperCase()}]`;
    const ctx = entry.context ? ` ${JSON.stringify(entry.context)}` : '';
    console.log(`${prefix} ${entry.message}${ctx}`);
  }
}
 
class JsonFileLogging implements LoggingStrategy {
  private buffer: string[] = [];
 
  constructor(private filePath: string, private bufferSize: number = 10) {}
 
  log(entry: LogEntry): void {
    this.buffer.push(JSON.stringify(entry));
    if (this.buffer.length >= this.bufferSize) {
      this.flush();
    }
  }
 
  async flush(): Promise<void> {
    if (this.buffer.length === 0) return;
    const data = this.buffer.join('\n') + '\n';
    this.buffer = [];
    console.log(`[JsonFileLogging] Flushed ${data.split('\n').length - 1} entries to ${this.filePath}`);
  }
}
 
class MultiLogging implements LoggingStrategy {
  constructor(private strategies: LoggingStrategy[]) {}
 
  log(entry: LogEntry): void {
    this.strategies.forEach(s => s.log(entry));
  }
 
  async flush(): Promise<void> {
    await Promise.all(
      this.strategies
        .filter(s => s.flush)
        .map(s => s.flush!())
    );
  }
}
 
// Context: Logger
class Logger {
  constructor(private strategy: LoggingStrategy) {}
 
  setStrategy(strategy: LoggingStrategy): void {
    this.strategy = strategy;
  }
 
  private createEntry(level: LogEntry['level'], message: string, context?: Record<string, unknown>): LogEntry {
    return { level, message, timestamp: new Date(), context };
  }
 
  debug(message: string, context?: Record<string, unknown>): void {
    this.strategy.log(this.createEntry('debug', message, context));
  }
 
  info(message: string, context?: Record<string, unknown>): void {
    this.strategy.log(this.createEntry('info', message, context));
  }
 
  warn(message: string, context?: Record<string, unknown>): void {
    this.strategy.log(this.createEntry('warn', message, context));
  }
 
  error(message: string, context?: Record<string, unknown>): void {
    this.strategy.log(this.createEntry('error', message, context));
  }
}
 
// --- 使用例 ---
// 開発環境: コンソールのみ
const devLogger = new Logger(new ConsoleLogging());
devLogger.info("Server started", { port: 3000 });
 
// 本番環境: コンソール + JSON ファイル
const prodLogger = new Logger(new MultiLogging([
  new ConsoleLogging(),
  new JsonFileLogging("/var/log/app.jsonl"),
]));
prodLogger.error("Database connection failed", { host: "db.example.com" });

4. if/else の排除

Strategy パターンの最も実践的な価値は、条件分岐の排除である。

BEFORE (条件分岐の肥大化):

  function calculate(type: string, price: number): number {
    if (type === "regular") return price;
    else if (type === "premium") return price * 0.9;
    else if (type === "student") return price * 0.7;
    else if (type === "senior") return price * 0.8;
    // ... 追加のたびにこの関数を変更 -> OCP 違反
  }

  問題:
  1. 関数の肥大化
  2. テストの組み合わせ爆発
  3. 新しい型の追加で既存コードを変更

AFTER (Strategy パターン):

  // 各戦略は独立したクラス/関数
  strategies.get(type).calculate(price);

  利点:
  1. 各戦略が独立してテスト可能
  2. 新しい戦略は register するだけ
  3. 既存のコードは変更不要

判断フロー:
  条件分岐は3つ以上か? ----No----> if/else で十分
    |
   Yes
    |
  将来の追加が見込まれるか? --No----> switch + enum で可読性確保
    |
   Yes
    |
  Strategy パターンを導入

5. 比較表

比較表 1: Strategy vs State vs Command vs Template Method

観点 Strategy State Command Template Method
目的 アルゴリズム交換 状態依存の振る舞い 操作のカプセル化 アルゴリズムの骨格定義
交換タイミング クライアントが決定 内部状態で自動遷移 キュー/履歴に保存 コンパイル時
関係 has-a(委譲) has-a(委譲) has-a(委譲) is-a(継承)
柔軟性 高い(実行時交換) 高い 低い(継承で固定)
Undo なし なし あり なし
典型的な数 少数〜中 有限個の状態 多数のコマンド 1つの骨格
テスト 個別にテスト容易 状態ごとにテスト コマンドごとにテスト サブクラスごとにテスト

比較表 2: クラス Strategy vs 関数 Strategy

観点 クラスベース 関数ベース
状態保持 フィールドで可能 クロージャで可能
設定パラメータ コンストラクタで注入 高階関数で注入
テスト インスタンス化して実行 直接呼び出し
コード量 多い(class, implements) 少ない(関数リテラル)
型安全性 高い(インタフェース強制) 中(型エイリアスに依存)
DI フレームワーク 対応しやすい 対応にくい場合あり
適用場面 複雑な戦略、状態を持つ 単純な変換、ソート

比較表 3: Strategy の適用判断

状況 推奨アプローチ 理由
バリエーションが2つ 三項演算子/if-else Strategy は過剰設計
バリエーションが3〜5 switch/enum または Strategy 将来の追加を考慮して判断
バリエーションが6以上 Strategy + Registry 条件分岐の管理が困難
実行時に切り替え必要 Strategy 主目的に合致
アルゴリズムが複雑 クラス Strategy 状態とロジックのカプセル化
アルゴリズムが単純 関数 Strategy 軽量で十分

6. アンチパターン

アンチパターン 1: 戦略が2つしかないのに Strategy パターン

// NG: 過剰設計(YAGNI 違反)
interface GreetingStrategy {
  greet(name: string): string;
}
class FormalGreeting implements GreetingStrategy {
  greet(name: string) { return `Dear ${name}`; }
}
class CasualGreeting implements GreetingStrategy {
  greet(name: string) { return `Hi ${name}`; }
}
 
// このためだけにインタフェース + 2クラスは過剰
// 三項演算子で十分:
 
// OK: シンプルに書く
const greet = (name: string, formal: boolean) =>
  formal ? `Dear ${name}` : `Hi ${name}`;

改善: バリエーションが3つ以上、または将来の追加が見込まれる場合にのみ Strategy パターンを導入する。YAGNI(You Aren't Gonna Need It)の原則を忘れない。

アンチパターン 2: Context が Strategy の内部を知っている

// NG: Context が Strategy の具象型をチェック
class Context {
  execute(): void {
    if (this.strategy instanceof StrategyA) {
      // StrategyA 固有の前処理
      this.prepareForA();
    }
    if (this.strategy instanceof StrategyB) {
      // StrategyB 固有の前処理
      this.prepareForB();
    }
    this.strategy.execute();
  }
}
// 問題: Strategy を追加するたびに Context も変更が必要 -> OCP 違反
 
// OK: Context は Strategy インタフェースのみに依存
class Context {
  execute(): void {
    // 前処理は Strategy 内部に閉じ込める
    this.strategy.execute();
  }
}
 
// Strategy 側で前処理を含める
class StrategyA implements Strategy {
  execute(): void {
    this.prepare(); // 固有の前処理
    this.doWork();  // 本処理
  }
}

アンチパターン 3: Strategy の粒度が不適切

// NG: 1つの Strategy に複数の無関係な責務
interface AllInOneStrategy {
  calculatePrice(price: number): number;
  formatOutput(data: any): string;
  validateInput(input: string): boolean;
  sendNotification(message: string): void;
}
// 問題: 料金計算を変えたいだけなのに、全てのメソッドを実装する必要がある
 
// OK: 責務ごとに Strategy を分割
interface PricingStrategy {
  calculate(price: number): number;
}
 
interface FormattingStrategy {
  format(data: any): string;
}
 
interface ValidationStrategy {
  validate(input: string): ValidationResult;
}
 
// Context は必要な Strategy だけを使う
class OrderService {
  constructor(
    private pricing: PricingStrategy,
    private formatting: FormattingStrategy,
  ) {}
}

アンチパターン 4: Strategy の切り替えがスレッドセーフでない

// NG: マルチスレッド環境で Strategy の切り替えが競合する
class PaymentProcessor {
  private strategy: PaymentStrategy;
 
  setStrategy(strategy: PaymentStrategy): void {
    this.strategy = strategy; // スレッドAが書き換え中にスレッドBが読む可能性
  }
 
  process(order: Order): PaymentResult {
    return this.strategy.process(order); // どのStrategyが使われるか不定
  }
}
 
// OK: Strategy を引数で渡すか、イミュータブルなContextを使う
class PaymentProcessor {
  // 方法1: Strategyを引数として受け取る(状態を持たない)
  process(order: Order, strategy: PaymentStrategy): PaymentResult {
    return strategy.process(order);
  }
}
 
// 方法2: イミュータブルなContext(Strategyの変更時は新インスタンスを生成)
class PaymentProcessor {
  constructor(private readonly strategy: PaymentStrategy) {}
 
  withStrategy(strategy: PaymentStrategy): PaymentProcessor {
    return new PaymentProcessor(strategy);
  }
 
  process(order: Order): PaymentResult {
    return this.strategy.process(order);
  }
}

改善: マルチスレッド環境では、(1) Strategy を引数で渡す、(2) Context をイミュータブルにする、(3) スレッドローカルストレージを使う、のいずれかで安全性を確保する。


7. 実践演習

演習 1: 基礎 ── テキスト変換 Strategy

課題: テキスト変換を Strategy パターンで実装せよ。

要件:

  1. TextTransformer インタフェース: transform(text: string): string
  2. 具体 Strategy: UpperCase, LowerCase, CamelCase, SnakeCase, KebabCase
  3. TextProcessor Context: Strategy を使ってテキストを変換

テストケース:

const processor = new TextProcessor(new UpperCase());
console.log(processor.process("hello world")); // "HELLO WORLD"
 
processor.setStrategy(new CamelCase());
console.log(processor.process("hello world")); // "helloWorld"
 
processor.setStrategy(new SnakeCase());
console.log(processor.process("hello world")); // "hello_world"
 
processor.setStrategy(new KebabCase());
console.log(processor.process("hello world")); // "hello-world"

期待される出力: 上記コメントの通り。


演習 2: 応用 ── 動的 Strategy Registry

課題: Strategy Registry を実装し、設定ファイルやAPIパラメータから動的に Strategy を選択できるシステムを構築せよ。

要件:

  1. StrategyRegistry<T> クラス: Strategy の登録と取得
  2. デフォルト Strategy のサポート
  3. 利用可能な Strategy の一覧取得
  4. 実行時の Strategy 追加(プラグイン対応)
  5. Shipping(送料計算)の具体例で実装

テストケース:

const registry = new StrategyRegistry<ShippingStrategy>();
registry
  .register("standard", new StandardShipping(), true)
  .register("express", new ExpressShipping())
  .register("same-day", new SameDayShipping());
 
console.log(registry.getAvailableNames());
// ["standard", "express", "same-day"]
 
const strategy = registry.get("express");
console.log(strategy.calculate(1000, 2.5)); // 送料計算
 
// 未登録の名前 -> デフォルト戦略
const fallback = registry.get("unknown");
console.log(fallback === registry.get("standard")); // true

期待される出力: 上記コメントの通り。


演習 3: 発展 ── 合成可能な Strategy パイプライン

課題: 複数の Strategy を組み合わせて、パイプライン的に処理を適用できるフレームワークを構築せよ。

要件:

  1. TransformPipeline<T> クラス: 複数の変換 Strategy をチェーン
  2. addStep(strategy): パイプラインにステップを追加
  3. execute(input): パイプラインを順次実行
  4. addConditional(predicate, strategy): 条件付き Strategy の適用
  5. 画像処理のパイプラインで具体例を実装

テストケース:

interface ImageData {
  width: number;
  height: number;
  format: string;
  quality: number;
}
 
const pipeline = new TransformPipeline<ImageData>()
  .addStep(new ResizeStrategy(800, 600))
  .addConditional(
    img => img.format === 'png',
    new ConvertToJpeg()
  )
  .addStep(new CompressStrategy(85));
 
const result = pipeline.execute({
  width: 1920, height: 1080, format: 'png', quality: 100
});
console.log(result);
// { width: 800, height: 600, format: 'jpg', quality: 85 }

期待される出力: 上記コメントの通り。


トラブルシューティング

よくあるエラーと解決策

エラー 原因 解決策
初期化エラー 設定ファイルの不備 設定ファイルのパスと形式を確認
タイムアウト ネットワーク遅延/リソース不足 タイムアウト値の調整、リトライ処理の追加
メモリ不足 データ量の増大 バッチ処理の導入、ページネーションの実装
権限エラー アクセス権限の不足 実行ユーザーの権限確認、設定の見直し
データ不整合 並行処理の競合 ロック機構の導入、トランザクション管理

デバッグの手順

  1. エラーメッセージの確認: スタックトレースを読み、発生箇所を特定する
  2. 再現手順の確立: 最小限のコードでエラーを再現する
  3. 仮説の立案: 考えられる原因をリストアップする
  4. 段階的な検証: ログ出力やデバッガを使って仮説を検証する
  5. 修正と回帰テスト: 修正後、関連する箇所のテストも実行する
# デバッグ用ユーティリティ
import logging
import traceback
from functools import wraps
 
# ロガーの設定
logging.basicConfig(
    level=logging.DEBUG,
    format='%(asctime)s [%(levelname)s] %(name)s: %(message)s'
)
logger = logging.getLogger(__name__)
 
def debug_decorator(func):
    """関数の入出力をログ出力するデコレータ"""
    @wraps(func)
    def wrapper(*args, **kwargs):
        logger.debug(f"呼び出し: {func.__name__}(args={args}, kwargs={kwargs})")
        try:
            result = func(*args, **kwargs)
            logger.debug(f"戻り値: {func.__name__} -> {result}")
            return result
        except Exception as e:
            logger.error(f"例外発生: {func.__name__}: {e}")
            logger.error(traceback.format_exc())
            raise
    return wrapper
 
@debug_decorator
def process_data(items):
    """データ処理(デバッグ対象)"""
    if not items:
        raise ValueError("空のデータ")
    return [item * 2 for item in items]

パフォーマンス問題の診断

パフォーマンス問題が発生した場合の診断手順:

  1. ボトルネックの特定: プロファイリングツールで計測
  2. メモリ使用量の確認: メモリリークの有無をチェック
  3. I/O待ちの確認: ディスクやネットワークI/Oの状況を確認
  4. 同時接続数の確認: コネクションプールの状態を確認
問題の種類 診断ツール 対策
CPU負荷 cProfile, py-spy アルゴリズム改善、並列化
メモリリーク tracemalloc, objgraph 参照の適切な解放
I/Oボトルネック strace, iostat 非同期I/O、キャッシュ
DB遅延 EXPLAIN, slow query log インデックス、クエリ最適化

設計判断ガイド

選択基準マトリクス

技術選択を行う際の判断基準を以下にまとめます。

判断基準 重視する場合 妥協できる場合
パフォーマンス リアルタイム処理、大規模データ 管理画面、バッチ処理
保守性 長期運用、チーム開発 プロトタイプ、短期プロジェクト
スケーラビリティ 成長が見込まれるサービス 社内ツール、固定ユーザー
セキュリティ 個人情報、金融データ 公開データ、社内利用
開発速度 MVP、市場投入スピード 品質重視、ミッションクリティカル

アーキテクチャパターンの選択

アーキテクチャ選択フロー
① チーム規模は?
├─ 小規模(1-5人)→ モノリス
└─ 大規模(10人+)→ ②へ
② デプロイ頻度は?
├─ 週1回以下 → モノリス + モジュール分割
└─ 毎日/複数回 → ③へ
③ チーム間の独立性は?
├─ 高い → マイクロサービス
└─ 中程度 → モジュラーモノリス

トレードオフの分析

技術的な判断には必ずトレードオフが伴います。以下の観点で分析を行いましょう:

1. 短期 vs 長期のコスト

  • 短期的に速い方法が長期的には技術的負債になることがある
  • 逆に、過剰な設計は短期的なコストが高く、プロジェクトの遅延を招く

2. 一貫性 vs 柔軟性

  • 統一された技術スタックは学習コストが低い
  • 多様な技術の採用は適材適所が可能だが、運用コストが増加

3. 抽象化のレベル

  • 高い抽象化は再利用性が高いが、デバッグが困難になる場合がある
  • 低い抽象化は直感的だが、コードの重複が発生しやすい
# 設計判断の記録テンプレート
class ArchitectureDecisionRecord:
    """ADR (Architecture Decision Record) の作成"""
 
    def __init__(self, title: str):
        self.title = title
        self.context = ""
        self.decision = ""
        self.consequences = []
        self.alternatives = []
 
    def set_context(self, context: str):
        """背景と課題の記述"""
        self.context = context
        return self
 
    def set_decision(self, decision: str):
        """決定内容の記述"""
        self.decision = decision
        return self
 
    def add_consequence(self, consequence: str, positive: bool = True):
        """結果の追加"""
        self.consequences.append({
            'description': consequence,
            'type': 'positive' if positive else 'negative'
        })
        return self
 
    def add_alternative(self, name: str, reason_rejected: str):
        """却下した代替案の追加"""
        self.alternatives.append({
            'name': name,
            'reason_rejected': reason_rejected
        })
        return self
 
    def to_markdown(self) -> str:
        """Markdown形式で出力"""
        md = f"# ADR: {self.title}\n\n"
        md += f"## 背景\n{self.context}\n\n"
        md += f"## 決定\n{self.decision}\n\n"
        md += "## 結果\n"
        for c in self.consequences:
            icon = "✅" if c['type'] == 'positive' else "⚠️"
            md += f"- {icon} {c['description']}\n"
        md += "\n## 却下した代替案\n"
        for a in self.alternatives:
            md += f"- **{a['name']}**: {a['reason_rejected']}\n"
        return md

実務での適用シナリオ

シナリオ1: スタートアップでのMVP開発

状況: 限られたリソースで素早くプロダクトをリリースする必要がある

アプローチ:

  • シンプルなアーキテクチャを選択
  • 必要最小限の機能に集中
  • 自動テストはクリティカルパスのみ
  • モニタリングは早期から導入

学んだ教訓:

  • 完璧を求めすぎない(YAGNI原則)
  • ユーザーフィードバックを早期に取得
  • 技術的負債は意識的に管理する

シナリオ2: レガシーシステムのモダナイゼーション

状況: 10年以上運用されているシステムを段階的に刷新する

アプローチ:

  • Strangler Fig パターンで段階的に移行
  • 既存のテストがない場合はCharacterization Testを先に作成
  • APIゲートウェイで新旧システムを共存
  • データ移行は段階的に実施
フェーズ 作業内容 期間目安 リスク
1. 調査 現状分析、依存関係の把握 2-4週間
2. 基盤 CI/CD構築、テスト環境 4-6週間
3. 移行開始 周辺機能から順次移行 3-6ヶ月
4. コア移行 中核機能の移行 6-12ヶ月
5. 完了 旧システム廃止 2-4週間

シナリオ3: 大規模チームでの開発

状況: 50人以上のエンジニアが同一プロダクトを開発する

アプローチ:

  • ドメイン駆動設計で境界を明確化
  • チームごとにオーナーシップを設定
  • 共通ライブラリはInner Source方式で管理
  • APIファーストで設計し、チーム間の依存を最小化
# チーム間のAPI契約定義
from dataclasses import dataclass
from typing import List, Optional
from enum import Enum
 
class Priority(Enum):
    LOW = "low"
    MEDIUM = "medium"
    HIGH = "high"
    CRITICAL = "critical"
 
@dataclass
class APIContract:
    """チーム間のAPI契約"""
    endpoint: str
    method: str
    owner_team: str
    consumers: List[str]
    sla_ms: int  # レスポンスタイムSLA
    priority: Priority
 
    def validate_sla(self, actual_ms: int) -> bool:
        """SLA準拠の確認"""
        return actual_ms <= self.sla_ms
 
    def to_openapi(self) -> dict:
        """OpenAPI形式で出力"""
        return {
            'path': self.endpoint,
            'method': self.method,
            'x-owner': self.owner_team,
            'x-consumers': self.consumers,
            'x-sla-ms': self.sla_ms
        }
 
# 使用例
contracts = [
    APIContract(
        endpoint="/api/v1/users",
        method="GET",
        owner_team="user-team",
        consumers=["order-team", "notification-team"],
        sla_ms=200,
        priority=Priority.HIGH
    ),
    APIContract(
        endpoint="/api/v1/orders",
        method="POST",
        owner_team="order-team",
        consumers=["payment-team", "inventory-team"],
        sla_ms=500,
        priority=Priority.CRITICAL
    )
]

シナリオ4: パフォーマンスクリティカルなシステム

状況: ミリ秒単位のレスポンスが求められるシステム

最適化ポイント:

  1. キャッシュ戦略(L1: インメモリ、L2: Redis、L3: CDN)
  2. 非同期処理の活用
  3. コネクションプーリング
  4. クエリ最適化とインデックス設計
最適化手法 効果 実装コスト 適用場面
インメモリキャッシュ 頻繁にアクセスされるデータ
CDN 静的コンテンツ
非同期処理 I/O待ちが多い処理
DB最適化 クエリが遅い場合
コード最適化 低-中 CPU律速の場合

8. FAQ

Q1: Strategy と DI は同じですか?

DI(依存性注入)は依存の注入メカニズム、Strategy はアルゴリズム交換のパターンです。DI は Strategy を実現する手段として使えますが、Strategy は DI なしでも実装可能です。DI コンテナ(InversifyJS, tsyringe 等)を使うと、設定ファイルから Strategy を自動的に注入できて便利ですが、必須ではありません。

Q2: JavaScript では関数を渡すだけで Strategy は実現できますか?

はい。コールバック関数は Strategy パターンの軽量な実装です。Array.sort(compareFn) が典型例です。ただし、複雑な状態を持つ戦略やパラメータ設定が必要な戦略にはクラスが適しています。「関数1つで済むならクラスは不要、設定やテストの都合でクラスが必要なら使う」が実用的な判断基準です。

Q3: Strategy と Template Method の違いは?

Strategy は委譲(has-a)でアルゴリズム全体を交換します。Template Method は継承(is-a)でアルゴリズムの一部をオーバーライドします。Strategy の方が柔軟性が高く、現代のプログラミングでは推奨されます。GoF 自身も「委譲を継承より優先せよ」と述べています。

Q4: Strategy をいつ導入すべきですか?

以下の条件を満たす場合に導入を検討してください: (1) 同じ処理に3つ以上のバリエーションがある、(2) 将来新しいバリエーションの追加が見込まれる、(3) 実行時にアルゴリズムを切り替える必要がある、(4) アルゴリズムのテストを個別に行いたい。逆に、バリエーションが2つで将来の追加もないなら if-else で十分です。

Q5: Strategy とポリモーフィズムの関係は?

Strategy パターンはポリモーフィズムの応用です。共通のインタフェースを通じて異なる実装を統一的に扱うという点で、ポリモーフィズムそのものです。OOP ではインタフェース/抽象クラスで、関数型では関数型(type alias)で実現します。

Q6: Strategy パターンのテストはどう書くべきか?

テストは3つの層に分けて書くのが効果的です。

  1. 各 Strategy の単体テスト: Strategy ごとに入力と期待出力を検証する。Strategy は独立したクラス/関数なのでモック不要でテストしやすい。
  2. Context のテスト: モック Strategy を注入して、Context が Strategy を正しく呼び出しているかを検証する。ここでは Strategy の実装詳細には踏み込まない。
  3. 統合テスト: 実際の Strategy と Context を組み合わせて、エンドツーエンドの動作を確認する。
// 1. Strategy の単体テスト
describe('ExpressShipping', () => {
  it('5kg以下の荷物に速達料金を適用する', () => {
    const strategy = new ExpressShipping();
    expect(strategy.calculate(1000, 3.0)).toBe(1800); // 基本料 + 速達加算
  });
});
 
// 2. Context のテスト(モック使用)
describe('ShippingCalculator', () => {
  it('設定された Strategy に計算を委譲する', () => {
    const mockStrategy: ShippingStrategy = {
      calculate: jest.fn().mockReturnValue(500),
    };
    const calculator = new ShippingCalculator(mockStrategy);
    const result = calculator.calculateShipping(1000, 2.0);
    expect(mockStrategy.calculate).toHaveBeenCalledWith(1000, 2.0);
    expect(result).toBe(500);
  });
});
 
// 3. 統合テスト
describe('ShippingCalculator + StandardShipping', () => {
  it('実際の送料計算が正しい', () => {
    const calculator = new ShippingCalculator(new StandardShipping());
    expect(calculator.calculateShipping(1000, 2.0)).toBe(600);
  });
});

Q7: Strategy パターンとデコレータパターンの使い分けは?

Strategy は「アルゴリズムの交換」、デコレータは「機能の追加・装飾」が目的です。Strategy ではある時点で1つの戦略が選択されて実行されますが、デコレータは複数のラッパーを重ねて機能を拡張します。

判断基準:

  • 「A または B を実行する」→ Strategy(排他的選択)
  • 「A に加えて B も実行する」→ Decorator(累積的追加)

実務では両者を組み合わせることも多く、例えばログ出力 Strategy をキャッシュ Decorator で包むといった設計が有効です。


FAQ

Q1: このトピックを学ぶ上で最も重要なポイントは何ですか?

実践的な経験を積むことが最も重要です。理論だけでなく、実際にコードを書いて動作を確認することで理解が深まります。

Q2: 初心者がよく陥る間違いは何ですか?

基礎を飛ばして応用に進むことです。このガイドで説明している基本概念をしっかり理解してから、次のステップに進むことをお勧めします。

Q3: 実務ではどのように活用されていますか?

このトピックの知識は、日常的な開発業務で頻繁に活用されます。特にコードレビューやアーキテクチャ設計の際に重要になります。


9. まとめ

項目 ポイント
目的 アルゴリズムをカプセル化して交換可能にする
OCP 新しい戦略を追加しても既存コード変更不要
実装方式 クラスベース(状態あり)/ 関数ベース(軽量)
Registry 動的選択と拡張性を両立する補助パターン
関数型 関数を渡すだけでも実現可能(Array.sort等)
判断基準 3+バリエーション or 将来の拡張が見込まれる場合に導入
注意 Context は Strategy の具象型を知らない設計にする
粒度 1つの Strategy に1つの責務(ISP の遵守)

次に読むべきガイド


参考文献

  1. Gamma, E., Helm, R., Johnson, R., Vlissides, J. (1994). Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley. -- Strategy パターンの原典。
  2. Freeman, E., Robson, E. (2020). Head First Design Patterns (2nd Edition). O'Reilly Media. -- Strategy パターンを最初に扱い、設計原則との関連を丁寧に解説。
  3. Refactoring.Guru -- Strategy. https://refactoring.guru/design-patterns/strategy -- 図解と多言語実装例。
  4. Martin, R.C. (2003). Agile Software Development: Principles, Patterns, and Practices. Prentice Hall. -- OCP と Strategy の関係。
  5. Fowler, M. (1999). Refactoring: Improving the Design of Existing Code. Addison-Wesley. -- 「条件分岐をポリモーフィズムに置き換える」リファクタリング。