LESSON 30分

ストーリー

高橋アーキテクト
新しい決済方法を追加してほしいという要件が来た。クレジットカード、銀行振込、そして今度は電子マネーだ
あなた
既存のコードを修正すればいいですよね?
高橋アーキテクト
そのたびに既存コードを修正するということは、既にテスト済みの箇所にバグを入れるリスクがあるということだ。OCPを知っていれば、既存コードを変えずに新しい機能を追加できる

OCPとは

Open-Closed Principle(オープン・クローズドの原則): ソフトウェアの構成要素は、拡張に対して開いていて、修正に対して閉じているべきである。

  • Open for extension — 新しい振る舞いを追加できる
  • Closed for modification — 既存のコードを変更しなくてよい

OCP違反の例

// OCP違反:新しい決済方法を追加するたびに既存コードを修正
class PaymentProcessor {
  processPayment(method: string, amount: number): void {
    if (method === 'credit_card') {
      // クレジットカード処理
      console.log(`Credit card payment: ${amount}`);
      // カード番号検証、API呼び出し...
    } else if (method === 'bank_transfer') {
      // 銀行振込処理
      console.log(`Bank transfer: ${amount}`);
      // 口座番号検証、振込依頼...
    } else if (method === 'e_money') {
      // 電子マネー処理 ← 追加のたびにここが増える
      console.log(`E-money payment: ${amount}`);
    }
    // 新しい決済方法が来るたびに else if を追加...
  }
}

問題点:

  • 新しい決済方法の追加で既存のメソッドを修正する必要がある
  • テスト済みの分岐に意図せず影響する可能性がある
  • メソッドが際限なく長くなる

OCPを適用した設計

// インターフェースを定義(拡張ポイント)
interface PaymentMethod {
  readonly name: string;
  validate(details: PaymentDetails): boolean;
  execute(amount: number): PaymentResult;
}

// 既存の実装
class CreditCardPayment implements PaymentMethod {
  readonly name = 'credit_card';

  validate(details: PaymentDetails): boolean {
    // クレジットカード固有のバリデーション
    return details.cardNumber !== undefined && details.cardNumber.length === 16;
  }

  execute(amount: number): PaymentResult {
    // クレジットカードAPIを呼び出す
    return { success: true, transactionId: 'cc-123' };
  }
}

class BankTransferPayment implements PaymentMethod {
  readonly name = 'bank_transfer';

  validate(details: PaymentDetails): boolean {
    return details.accountNumber !== undefined;
  }

  execute(amount: number): PaymentResult {
    return { success: true, transactionId: 'bt-456' };
  }
}

// 新しい決済方法を追加 -- 既存コードの変更不要!
class EMoneyPayment implements PaymentMethod {
  readonly name = 'e_money';

  validate(details: PaymentDetails): boolean {
    return details.emoneyId !== undefined;
  }

  execute(amount: number): PaymentResult {
    return { success: true, transactionId: 'em-789' };
  }
}

// 処理クラスは修正不要(閉じている)
class PaymentProcessor {
  private methods: Map<string, PaymentMethod> = new Map();

  registerMethod(method: PaymentMethod): void {
    this.methods.set(method.name, method);
  }

  processPayment(methodName: string, amount: number, details: PaymentDetails): PaymentResult {
    const method = this.methods.get(methodName);
    if (!method) {
      throw new Error(`Unknown payment method: ${methodName}`);
    }
    if (!method.validate(details)) {
      throw new Error(`Invalid payment details for ${methodName}`);
    }
    return method.execute(amount);
  }
}

OCPを実現するテクニック

1. ポリモーフィズム(最も基本的)

上の決済の例のように、インターフェースと実装クラスで実現します。

2. Strategy パターン

// 割引計算のストラテジー
interface DiscountStrategy {
  calculate(price: number): number;
}

class NoDiscount implements DiscountStrategy {
  calculate(price: number): number { return price; }
}

class PercentageDiscount implements DiscountStrategy {
  constructor(private rate: number) {}
  calculate(price: number): number { return price * (1 - this.rate); }
}

class FixedDiscount implements DiscountStrategy {
  constructor(private amount: number) {}
  calculate(price: number): number { return Math.max(0, price - this.amount); }
}

// 新しい割引方法を追加しても、Order クラスは変更不要
class Order {
  constructor(private discountStrategy: DiscountStrategy) {}

  calculateTotal(items: OrderItem[]): number {
    const subtotal = items.reduce((sum, item) => sum + item.price, 0);
    return this.discountStrategy.calculate(subtotal);
  }
}

3. デコレーターパターン

interface Logger {
  log(message: string): void;
}

class ConsoleLogger implements Logger {
  log(message: string): void {
    console.log(message);
  }
}

// 既存のLoggerを変更せずに機能追加
class TimestampLogger implements Logger {
  constructor(private inner: Logger) {}

  log(message: string): void {
    this.inner.log(`[${new Date().toISOString()}] ${message}`);
  }
}

OCPの適用基準

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

「OCPを全てのコードに適用する必要はない。“変化が予想される箇所”に適用するのがポイントだ。一度も変更されないコードを過度に抽象化するのは、逆に複雑さを増やすだけだ」

状況判断
頻繁に種類が追加されるOCP を適用すべき
ビジネスルールが変わりやすいOCP を適用すべき
安定していて変更の予定がない過度な抽象化は不要
2回目の変更が来たときリファクタリングで OCP に

まとめ

ポイント内容
OCPの定義拡張に開き、修正に閉じる
実現方法ポリモーフィズム、Strategy、Decorator
適用基準変化が予想される箇所に適用
効果新機能追加時に既存コードを壊さない

チェックリスト

  • OCPの定義を自分の言葉で説明できる
  • OCP違反のコードを認識できる
  • ポリモーフィズムを使ってOCPを適用できる

次のステップへ

次は SOLID の L — リスコフの置換原則(LSP)を学びます。「親クラスを使っている場所で、子クラスに入れ替えても正しく動く」とはどういうことか、理解しましょう。


推定読了時間: 30分