📝

オープン・クローズドの原則(OCP)を学んで、インターフェースの使い方が少し分かった気がする

2025/02/13に公開

これまで特に何も考えずにクラスやインターフェースを書いてしまっていたので設計について学んでみた。

オープン・クローズドの原則(OCP: Open-Closed Principle)とは

「ソフトウェアの構成要素(クラス、モジュール、関数など)は拡張に対しては開いていて、修正に対して閉じていなければならない。」
(Bertrand Meyer. Object Oriented Software Construction, Printice Hallm 1988m p.23.)

変更が発生した場合に、既存のコードには修正を加えずに新しくコードを追加するだけで対応できるような設計にすること。

この原則を守ることで、ソフトウェアの拡張が容易になり、変更による影響範囲を最小限に抑えることができる。
例えば、新しい支払い方法を追加する際に既存のコードを修正する必要がないため、バグのリスクが減少し、テストがしやすくなるというメリットもある。

悪い例

https://www.typescriptlang.org/play/?#code/MYGwhgzhAEAKYE8C2BTAdgF1gJwPbBSl22gG8AoaaABzwKnmXQwAoMFqUAuaCDbAJZoA5gBpoYJLgCumHmmlIARimwBKHgDdcAgCZlKVaAIBm0NhxTQAvLegAiYNhS6BGAMJhsu+2oNGjYFw0CFwQFAA6EFxhFgADQHqGQBuGQA6GQGGGQAmGQGqGQB+GQEmGQHMGaAASUkkZTABfQDDFaEA7BkB6U0BVI0ARBkAkhkAY7UAwuUB1BkA-Bh7AfQZAIAY4tQBuQypK6BQQCCtTc3ZOGzt7akRNhF9-AOggkLDI6Ni47e2i0vLZDBr65vbu-qHRianoGbmFvYCMAAs8AB3aBoFAggCi2Dw2HipRWKBmBUarUA9gyAP+1ADv6PUAZgwtPqAbQZAMkMb0mAUqhgpFPIAHoadBAP7ygApXQDR8uRDnwaHRCKESNZQeC4IhUJgcPgecQWO9aOKiNgIjL6BBGCLWI5nK4PF4fOIAIwABn10u5coVJuVwuYLA2W0Q9nEAFYjeMgA

// 支払いを処理するクラス
class PaymentProcessor {
  processPayment(type: string, amount: number): void {
    if (type === "creditCard") {
      console.log(`クレジットカードで ${amount}円 の支払いを完了しました。`);
    } else if (type === "paypay") {
      console.log(`paypayで ${amount}円 の支払いを完了しました。`);
    } else {
      throw new Error(`${type} での支払いは対応していません。`);
    }
  }
}

// 使用例
const processor = new PaymentProcessor();
processor.processPayment("creditCard", 100);
processor.processPayment("paypay", 50);

新しい支払い方法を追加するたびに PaymentProcessor クラスを修正しないといけない。

// 支払いを処理するクラス
class PaymentProcessor {
  processPayment(type: string, amount: number): void {
    if (type === "creditCard") {
      console.log(`クレジットカードで ${amount}円 の支払いを完了しました。`);
    } else if (type === "paypay") {
      console.log(`paypayで ${amount}円 の支払いを完了しました。`);
    // 新しい支払い方法  
    } else if (type === "") {
      
    } else {
      throw new Error(`${type} での支払いは対応していません。`);
    }
  }
}

この設計の問題点

  • 変更が多く発生する
    → 新しい支払い方法を追加するたびに processPayment メソッドを修正しなければならない。
  • 変更が増えるのでバグが入る可能性が高くなる
    → 既存のロジックに影響を与える可能性がある。
  • テストが大変
    → 追加するたびに PaymentProcessor のテストも変更する必要がある。

良い例

https://www.typescriptlang.org/play/?#code/PTAEEYDpUelNFUjQIgyE7TQqzaBUGQFgyDsGQJQyGeGQfoZAfhkFWGQcoYjBOhkCSGQLO1BK-wCgBLAOwBcBTAJwDMBDAMadQABX4BPALacOAWU7sAFgHsAJqADejUKAAO3FcIDOxgBT8pKgK4cAXKFbWpAIx4BKBwDcVzNQG5GAF9GRhBQACZoQHdFQGV5QBC3QCsGeGQUakB87UBRiMZBABt+U1AAYW5ONWZ2Iv5uNXFpWXZQZik9XM4ZDmMxSQ72BWV1LR19QxNzSxt7R2c3bk9QHz8h3V1BFVZjFTbIXJUAczMAA0B6hkAbhkAOhkBhhkAJhkBqhiJASYZAcwZQABJNCdt2IMAwxVAsFLUQAx2oAwuUA6gyAPwYwYB9BkAQAyHdyBXQhEI5fKFOqYnoNJotNq9Lp1Xr9VQabS6AxGTimCxWL4OJyuDzeXxk4ardabba7A6HPSSfkSF7vT4cX7-QGgyEw+GI4Yo0LhQGAM8VAGAugE0GQDRDMdAJcMlDRBUJ2I4olG1M23GWI2YXn4XH0xr6ilJDiJDRJ6kCwzWG3Y3GsgnYKm4ZkFxOd6ldjo9ancVt0ymYxkgYfdEY0AF4HfV5OmkaBURSzaY3RxaZN2AyZsyFqz46BE8nU7mBmoU8XxnSOHLkcFFWBAP7ygApXQDR8jlOY1BKVypVqrVHaAs6xOAB3YrTipVGql9hmOU+4yNQWCneLxyr7oSLE53dy8e+kZU0zB8Bn5drnemp8WsxTsqbucdzlSkxhfdtvx3MxwAABmgu8DyPDtgwiN8L0-JCQ2PSQgMCEDzWQ8CxkggBWOD-CAA

// 1. 支払い方法ごとのインターフェースを定義
interface PaymentMethod {
  process(amount: number): void;
}

// 2. 具体的な支払い方法を実装
class CreditCardPayment implements PaymentMethod {
  process(amount: number): void {
    console.log(`クレジットカードで ${amount}円 の支払いを完了しました。`);
  }
}

class PayPayPayment implements PaymentMethod {
  process(amount: number): void {
    console.log(`paypayで ${amount}円 の支払いを完了しました。`);
  }
}

// 支払いを処理するクラス
class PaymentProcessor {
  private paymentMethod: PaymentMethod;

  constructor(paymentMethod: PaymentMethod) {
    this.paymentMethod = paymentMethod;
  }

  processPayment(amount: number): void {
    this.paymentMethod.process(amount);
  }
}

// 使用例
const creditCardPayment = new CreditCardPayment();
const paypayPayment = new PayPayPayment();

const processor1 = new PaymentProcessor(creditCardPayment);
processor1.processPayment(100);

const processor2 = new PaymentProcessor(paypayPayment);
processor2.processPayment(50);

この設計の良い点

  • 新しい支払い方法を追加するのに PaymentProcessor クラスを修正する必要がない
  • PaymentProcessorPaymentMethod インターフェースに依存しており、個別の支払い方法 (CreditCardPayment, PayPayPayment など) には依存していない
    → 新しいクラスを作るだけで拡張でき、既存のコードには影響が出ない
// 新しい支払い方法を追加するときは、新しいクラスを作るだけ
class CashPayment implements PaymentMethod {
  process(amount: number): void {
    console.log(`現金で ${amount}円 の支払いを完了しました。`);
  }
}

const cashPayment = new CashPayment();
const processor3 = new PaymentProcessor(cashPayment);
processor3.processPayment(200);

さらに、この設計では PaymentProcessor が特定の支払い方法に依存せず、
コンストラクタで渡すことで柔軟に変更することができる。
→ モックを使ってテストもしやすくなる。

// テスト用のモック支払いクラス
class MockPayment implements PaymentMethod {
  process(amount: number): void {
    console.log(`テスト用の支払い処理: ${amount}`);
  }
}

// モックを使ってテスト
const mockPayment = new MockPayment();
const testProcessor = new PaymentProcessor(mockPayment);
testProcessor.processPayment(100);

まとめ

インターフェースを活用することで、実装の詳細に依存せずに柔軟な設計ができることが分かった。
オープン・クローズドの原則(OCP)を意識することで、「変更しやすく、拡張しやすいコード」を書くコツを少しつかめた気がする。

Discussion