はじめに
この記事は、SOLID原則のまとめ の続きです。
今回は、SOLID原則のひとつである Open-Closed Principle(開放閉鎖の原則)について、歴史的背景から実務での適用パターンまで、コード例を交えながら解説します。
Open-Closed Principle(OCP)とは
OCP は 「ソフトウェアの構成要素(クラス、モジュール、関数など)は、拡張に対して開放され、修正に対して閉鎖されるべきである」 という原則です。
もう少し噛み砕くと、新しい機能を追加するときに既存のコードを修正せずに済むように設計しよう、ということです。
歴史的背景: Meyer と Martin の2つの解釈
OCP には歴史的に2つの解釈があります。
Meyer の OCP(1988年)
Bertrand Meyer が1988年の著書 Object-Oriented Software Construction で最初に提唱しました。Meyer のアプローチでは、継承(implementation inheritance) によって OCP を実現します。基底クラスは完成してライブラリに格納され「閉鎖」されますが、サブクラスを作ることで「拡張」が可能になるという考え方です。
Martin の OCP(1996年〜)
Robert C. Martin は、継承による実現には限界があることを指摘し、インターフェース(抽象)と合成(composition) によって OCP を実現するアプローチを提唱しました。これが現在広く採用されている解釈です。
具体的な実装を直接参照するのではなく、抽象(インターフェースや抽象クラス)に依存することで、新しい実装を追加しても既存コードの修正が不要になります。
OCP に違反した例
例1: 具体クラスへの直接依存
支払いサービス(PaymentService)を例に考えます。最初はカード支払いだけだったため、以下のように実装したとします。
<?php
class PaymentService {
public function __construct(CardPayment $cardPayment){
$this->cardPayment = $cardPayment;
}
public function pay()
{
$this->cardPayment->pay();
}
}
class CardPayment {
public function pay(){
echo("<p>pay with card</p>");
}
}
// カード支払い
$cardPayment = new CardPayment();
$payment = new PaymentService($cardPayment);
$payment->pay();
// 銀行支払いを追加したい場合 → PaymentService 自体の修正が必要になる
このコードでは、銀行支払いを追加する際に PaymentService クラス自体を修正する必要があります。これは OCP の「修正に対して閉鎖されるべき」に違反しています。
例2: 条件分岐の肥大化
OCP 違反のもう1つの典型パターンが、if-else や switch 文の肥大化です。
<?php
class DiscountCalculator {
public function calculate(string $customerType, int $amount): int {
if ($customerType === 'regular') {
return $amount;
} elseif ($customerType === 'premium') {
return (int)($amount * 0.9);
} elseif ($customerType === 'vip') {
return (int)($amount * 0.8);
}
// 新しい顧客タイプを追加するたびに、ここに分岐を追加する必要がある
return $amount;
}
}
新しい顧客タイプが増えるたびに DiscountCalculator クラスを修正する必要があり、修正に対して閉じていません。このような条件分岐の肥大化は、OCP 違反の典型的なサインです。
OCP に準拠した例
例1の改善: インターフェースによる抽象化
インターフェースを導入して、支払い方法を抽象化します。
<?php
class PaymentService {
public function __construct(PaymentMethod $paymentMethod){
$this->paymentMethod = $paymentMethod;
}
public function pay()
{
$this->paymentMethod->pay();
}
}
interface PaymentMethod {
public function pay();
}
class CardPayment implements PaymentMethod {
public function pay(){
echo("<p>pay with card</p>");
}
}
class BankPayment implements PaymentMethod {
public function pay(){
echo("<p>pay with bank</p>");
}
}
// カード支払い
$cardPayment = new CardPayment();
$payment = new PaymentService($cardPayment);
$payment->pay();
// 銀行支払い
$bankPayment = new BankPayment();
$payment = new PaymentService($bankPayment);
$payment->pay();
// アプリ支払いなど、新しい支払い方法を追加しても PaymentService の修正は不要
PaymentMethod インターフェースを導入したことで、新しい支払い方法(アプリ支払いなど)を追加する際に PaymentService を修正する必要がなくなりました。
例2の改善: Strategy パターンの適用
条件分岐の肥大化は、Strategy パターン で解消できます。
<?php
interface DiscountStrategy {
public function apply(int $amount): int;
}
class RegularDiscount implements DiscountStrategy {
public function apply(int $amount): int {
return $amount;
}
}
class PremiumDiscount implements DiscountStrategy {
public function apply(int $amount): int {
return (int)($amount * 0.9);
}
}
class VipDiscount implements DiscountStrategy {
public function apply(int $amount): int {
return (int)($amount * 0.8);
}
}
class DiscountCalculator {
public function calculate(DiscountStrategy $strategy, int $amount): int {
return $strategy->apply($amount);
}
}
// 使用例
$calculator = new DiscountCalculator();
echo $calculator->calculate(new PremiumDiscount(), 10000); // 9000
echo $calculator->calculate(new VipDiscount(), 10000); // 8000
// 新しい割引タイプを追加しても DiscountCalculator の修正は不要
Strategy パターンを使うことで、割引ロジックの追加が「クラスの追加」で完結し、既存コードの修正が不要になります。GoF のデザインパターンの多くは、OCP を実現するための具体的な手法と言えます。
OCP を適用する際の注意点
ホットスポットを見極める
OCP を効果的に適用するためには、どこが変更されやすいか(ホットスポット) を見極めることが重要です。すべての箇所に対して OCP を適用しようとすると、不要な抽象化が増えてコードが複雑になります。
変更が起きる可能性が高い箇所を優先的に抽象化し、それ以外はシンプルに保つのが現実的です。
最初から完璧を目指さない
初回の実装で OCP を完璧に適用することは難しく、また必ずしも必要ではありません。まずはシンプルに実装し、実際に変更が発生したタイミングでリファクタリングして OCP に準拠させるというアプローチが実務では効果的です。
他の SOLID原則との関係
OCP は他の SOLID原則と密接に関連しています。
- DIP(依存性逆転の原則) に準拠することで、自然と OCP にも準拠する設計になります
- LSP(リスコフの置換原則) は、OCP で導入した抽象の置換可能性を保証します
- SRP(単一責務の原則) で責務を適切に分離することが、OCP 適用の前提になります
まとめ
OCP は「既存コードを修正せずに機能を拡張できるようにする」という原則です。インターフェースによる抽象化や Strategy パターンなどのデザインパターンを活用することで、変更に強い設計を実現できます。ただし、すべてを抽象化するのではなく、変更が起きやすいホットスポットを見極めて適用することが大切です。