📝
オープン・クローズドの原則(OCP)を学んで、インターフェースの使い方が少し分かった気がする
これまで特に何も考えずにクラスやインターフェースを書いてしまっていたので設計について学んでみた。
オープン・クローズドの原則(OCP: Open-Closed Principle)とは
「ソフトウェアの構成要素(クラス、モジュール、関数など)は拡張に対しては開いていて、修正に対して閉じていなければならない。」
(Bertrand Meyer. Object Oriented Software Construction, Printice Hallm 1988m p.23.)
変更が発生した場合に、既存のコードには修正を加えずに新しくコードを追加するだけで対応できるような設計にすること。
この原則を守ることで、ソフトウェアの拡張が容易になり、変更による影響範囲を最小限に抑えることができる。
例えば、新しい支払い方法を追加する際に既存のコードを修正する必要がないため、バグのリスクが減少し、テストがしやすくなるというメリットもある。
悪い例
// 支払いを処理するクラス
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
のテストも変更する必要がある。
良い例
// 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
クラスを修正する必要がない -
PaymentProcessor
はPaymentMethod
インターフェースに依存しており、個別の支払い方法 (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