OCP(Open/Closed Principle)はSOLIDの2番目の原則であり、ソフトウェアエンティティは拡張に対してオープンであるべきだが、修正に対してはクローズドであるべきと定めています。1988年にBertrand Meyerによって提唱されたこの原則は、既存のコードを変更することなく新しい機能を追加することを可能にします。Robert Martinの著書 Clean Architecture(2017) によると、オープン性の原則は抽象化とポリモーフィズムを通じて実装され、回帰エラーのリスクを最小限に抑えます。
重要ポイント
OCP(オープン・クローズドの原則) — 拡張に対するオープン性と修正に対するクローズ性の原則。クラス、モジュール、関数は、ソースコードを変更せずに新しい振る舞いを追加できるように設計されるべきです。拡張は継承、コンポジション、またはインターフェース実装の置換を通じて達成されます。
Bertrand Meyerは著書Object-Oriented Software Construction(1988)で初めて継承を通じてOCPを説明しました:基底クラスは変更されず、サブクラスがその振る舞いを拡張します。Robert Martinによって提案されたOCPの現代的な解釈は、ポリモーフィズムとインターフェースに基づいています:継承の代わりに抽象的な契約が使用されます。
アプローチ間の違いは重要です。継承は基底クラスと派生クラスの間に強い結合を生み出します。インターフェースとコンポジションは柔軟性を提供します:クライアントコードを変更せずに実装を交換できます。現代のOCPは継承ではなく抽象化についてです。
ポリモーフィックOCPは契約を定義するために抽象クラスまたはインターフェースを使用します。クライアントコードは具体的な実装を知らずに抽象化と連携します。新しい機能は同じインターフェースを実装する新しいクラスを作成することで追加されます — 既存のコードを一切変更せずに。これによりシステムは変更に強く、拡張に対して予測可能になります。
モバイル開発では、このアプローチは広く普及しています:Strategyパターンは単一のインターフェースを通じてアルゴリズム(画像圧縮、キャッシュ、認証)を交換することを可能にします。新しい戦略を追加しても、それを使用するコードを変更する必要はありません。
OCPの実装は変更可能な振る舞いを抽象化に分離することから始まります。コード内にswitch構文やif-elseチェーンがオブジェクトの型をチェックしている場合 — それはOCPを適用する合図です。各条件分岐は拡張時に新しい分岐を追加する必要が生じる可能性があります。
OCPに基づくリファクタリングプロセスは3つのステップで構成されます:変更可能な側面(拡張できるもの)を特定し、それをインターフェースまたは抽象クラスに分離し、クライアントコードを具象クラスの代わりに抽象化と連携するよう書き換えます。これ以降、新しい機能はクライアントを変更せずに追加されます。
重要な補足:修正に対するクローズ性は絶対的なものではありません。要件の変更が抽象化自体や契約に影響を与える場合 — 変更は避けられません。OCPは実装の変更から保護するものであり、契約の変更からではありません。優れた設計は契約が安定しており、実装が可変であることを前提とします。
アーキテクチャのOCP互換性を評価する際には、拡張ポイントを見ることが有用です。開発者が新しい型のためにif-elseやswitchを追加する各ポイントは抽象化の候補です。OCPに従って設計されたシステムは予測可能な拡張ポイントを持ちます:「新しい型を追加するにはこのインターフェースを実装してください」というドキュメント付きのインターフェースです。Androidでは、ViewModelProvider.Factoryと組み合わせたFactoryパターンが明確な例です — 新しいViewModel型を追加しても既存のファクトリを変更する必要はありません。
最も効果的なパターンモバイル開発でOCPを遵守するためのものには、Strategy、Template Method、Decorator、Factoryがあります。それぞれが異なるオブジェクト指向設計メカニズムを通じて、既存のコードを変更せずに振る舞いを拡張する問題を解決します。
Strategyは共通のインターフェースを通じてアルゴリズムを即座に交換することを可能にします。iOS開発では、アニメーションやフォーム検証に戦略が使用されます。Template Methodは基底クラスでアルゴリズムの骨格を定義し、サブクラスがステップをオーバーライドします — 共通の構造を持つが内容が異なる画面に適しています。
Decoratorはオブジェクトのクラスを変更せずに動的に振る舞いを追加します。Androidでは、DecoratorはRepositoryをキャッシュやログ層でラップするために使用されます。Factory Methodはインターフェースを通じてオブジェクトを作成し、サブクラスがどのクラスをインスタンス化するかを決定できるようにします — OCP互換の依存関係作成の基盤です。
パターンの選択は拡張される振る舞いの安定性に依存します。Strategyはアルゴリズムが完全に置き換えられる場合に最適です。Template Method — 構造は固定だがステップが可変の場合。Decorator — 拡張がクライアントに対して透過的であるべき場合。AndroidとiOSのほとんどのシナリオでは、Strategy + 依存性注入で十分です。
これらのパターンをOCPなしで適用することは技術的に可能ですが、意味を失います。なぜ抽象化の追加レベルを導入するのかを正当化するのはOCPです:既存のコードを書き換えずにシステムが成長できるようにするためです。
支払い処理のAndroidの例を考えてみましょう。OCPなしでは、新しい支払いシステムごとにハンドラークラスの変更が必要です。OCPを使用すれば、既存のコードを変更せずに新しいインターフェース実装が追加されます。
// OCP違反:新しいシステム追加時にswitchの修正が必要
class BadPaymentProcessor {
fun process(type: String) {
when (type) {
"card" -> // カード処理
"paypal" -> // PayPal処理
}
}
}
// OCP互換の設計
interface PaymentMethod {
fun pay(amount: Double)
}
class CardPayment : PaymentMethod {
override fun pay(amount: Double) { }
}
class PayPalPayment : PaymentMethod {
override fun pay(amount: Double) { }
}
// 新しいシステム — 新しいクラス、既存のコードを変更せずに
class ApplePayPayment : PaymentMethod {
override fun pay(amount: Double) { }
}
テキストフィールド検証のiOSの例は、Swiftプロトコルを通じて同じロジックを示しています:
// OCP互換の検証
protocol ValidationRule {
func validate(_ input: String) -> Bool
}
struct EmailRule: ValidationRule {
func validate(_ input: String) -> Bool {
return input.contains("@")
}
}
struct PhoneRule: ValidationRule {
func validate(_ input: String) -> Bool {
return input.count == 11
}
}
// 新しいルールを追加してもバリデータコードの変更は不要
struct PasswordRule: ValidationRule {
func validate(_ input: String) -> Bool {
return input.count >= 8
}
}
これらの例におけるOCPの主な利点:ApplePayやPasswordRuleを追加しても既存のクラスを変更する必要がありません。コードは水平方向に拡張されます — 古いファイルを変更するのではなく、新しいファイルを通じて。これにより回帰のリスクが軽減され、新機能の実装が加速されます。
最も一般的な違反はオブジェクトの型に基づくswitchやwhen構文です。新しい型が追加されるたびに、コード内のすべてのそれらのswitchを見つけて新しい分岐を追加する必要があります。見逃されたswitchはコンパイル時に検出が難しいランタイムバグです。
モバイル開発では、enum値に依存するメソッドを持つ巨大なenumクラスを使用する際にOCPが違反されます。新しいenum要素を追加するには、プロジェクト全体のすべてのswitchを変更する必要があります。代替策はインターフェースを通じたポリモーフィズムで、各型が自身の振る舞いを実装します。
もう一つの典型的な違反はGod Adapterです:if-elseを通じて異なるセル型を処理するRecyclerView.Adapter(Android)やUITableViewDataSource(iOS)。新しいセル型ごとにアダプターの拡張が必要です。解決策は共通のbindメソッドを持つポリモーフィックなViewHolderで、各セル型が自身のレンダリングを担当します。
予防策には以下が含まれます:ポリモーフィズムを優先した型ベースのswitchの回避、インターフェースを通じた依存性の注入、設定に応じたオブジェクト作成のためのFactoryパターンの使用。「型によるスイッチ」の有無をコード分析することは、OCP指向チームにおけるコードレビューの必須部分です。
既存のOCP違反のリファクタリングはReplace Conditional with Polymorphismを通じて行われます:各条件分岐が共通のインターフェースを実装する個別のクラスになります。クライアントコードはインターフェースと連携するよう書き換えられ、具体的な実装はファクトリまたはDIコンテナを通じて提供されます。
OCPとポリモーフィズムがすべての拡張問題を解決するわけではないことを理解することが重要です。アーキテクチャが誤って選択された場合、新しい機能を追加するには実装だけでなく契約も変更する必要があります。優れたアーキテクチャは拡張の方向性を予測し、まさにそれらのポイントに抽象化を配置します。OCPへの投資は、プロジェクトの寿命が長く、特定のモジュールの要件が頻繁に変更されるほど効果を発揮します。
よくある質問
いいえ。OCPは同じ抽象化に関連する新しい機能を追加する際に既存のコードを変更することを禁止しています。契約の変更、バグ修正、リファクタリングはOCP違反ではありません — この原則は拡張時のカスケード変更から保護します。
StrategyはOCPの直接的な実装です。戦略インターフェースが契約を定義し、クライアントは抽象化に依存し、具体的な戦略が可変的な振る舞いを実装します。新しい戦略を追加してもクライアントを変更する必要はありません — これが修正に対するクローズ性を備えた拡張に対するオープン性です。
はい、継承とTemplate Methodを通じて可能です:基底クラスがアルゴリズムの骨格を定義し、サブクラスがステップをオーバーライドします。ただし、継承は強い結合を生み出し、インターフェースよりも柔軟性に欠けます。現代の開発では、インターフェースとコンポジションがOCPを実装する好ましい方法と考えられています。
OCP互換のコードはテストを簡素化します:各インターフェース実装は独立してテストされます。クライアントコードはモック実装でテストされ、特定の振る舞いに縛られることなくロジックを検証できます。システムの拡張に際して既存のテストを書き換える必要はありません。
いいえ。OCPは機能拡張が予測可能な場合に正当化されます。拡張が計画されていない安定したコードには、追加の抽象化は過剰です。YAGNI(You Ain't Gonna Need It)はOCPに対する良いバランスです:抽象化は振る舞いの2番目のバリアントが現れたときに導入され、先制的には導入されません。
まとめ
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。