ストーリー
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分