オープンクローズドの原則は「ソフトウェアの構成要素は拡張に対して開かれ、修正に対して閉じられているべきである」という設計原則です。ただしこの原則には、1988年にバートランド・メイヤーが定義した版と、1996年にロバート・C・マーチンが定義し直した版という中身の異なる2つがあります。しかも、いま最も広く引用されている一文はメイヤー自身の文言ではありません。この記事では出所の整理から始めて、2つの定義の差、型分岐という典型的な違反コードの直し方、そして適用してはいけない場面の判断基準までを扱います。
まとめ
- 原典は Bertrand Meyer『Object-Oriented Software Construction』(1988年)。マーチンが提唱した原則ではない。
- 広く引用される「拡張に開き、修正に閉じる」の一文は、マーチンが1996年にメイヤーを言い換えたもの。原文に
To paraphrase himと明記されている。 - メイヤー版は実装継承で拡張する定義、マーチン版は抽象基底クラスと実行時ポリモーフィズムで差し替える定義。SOLIDの「O」は後者を指す。
- 違反の典型は型による分岐。種類が増えるたび、同じ判定を持つ複数の関数を書き換えることになる。
- マーチン自身が「閉じることは完全にはできないので戦略的でなければならない」と書いている。閉じる軸を読み違えた抽象化は負債になる。
この5点のうち出所の整理がいちばん誤解を招いている部分なので、定義の中身から順に見ていきます。
オープンクローズドの原則の定義と「開く」「閉じる」の対象
英語でよく引用されるのは “software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.” という一文です。ところがこれは、マーチンが1996年の記事でメイヤーを紹介する際に付けた言い換えで、原文には To paraphrase him(彼を言い換えると)という前置きが明示されています。メイヤー自身の言葉は次のものです。
「A class is closed, since it may be compiled, stored in a library, baselined, and used by client classes. But it is also open, since any new class may use it as parent, adding new features.」(クラスはコンパイルされ、ライブラリに格納され、ベースラインとして確定し、クライアントクラスから使われているという意味で閉じている。同時に、任意の新しいクラスがそれを親として使い、新しい機能を加えられるという意味で開いている)
言い換えのほうが記憶しやすいため定着しましたが、メイヤーの原文は「閉じている=すでに配って使われている」という運用上の状態を指していました。この差が、次章で見る2つの版の分岐点になります。
「開く」「閉じる」が指しているもの
「開く」の対象は振る舞いの拡張、「閉じる」の対象は既存ソースコードの修正です。混同されやすいのは、閉じるのが「変更全般」ではなく「特定の種類の変更」だという点です。決済手段の追加に対して閉じた設計にしても、税率計算の仕様変更に対しては開いたままかもしれません。何に対して閉じるのかを指定しない限り、この原則を満たしているかどうかは評価できません。
別名・英語表記・略称(開放閉鎖の原則/開閉原則/OCP原則)
日本語では「開放閉鎖の原則」「開放/閉鎖原則」「開閉原則」と訳され、日本語版Wikipediaの項目名は「開放/閉鎖原則」が採用されています。英語は Open-Closed Principle、略称は OCP で、日本語記事では「OCP原則」という重ね表記も見かけます。中黒を入れた「オープン・クローズドの原則」も同義です。なお OCP は制御工学の最適制御問題(Optimal Control Problem)の略でもあるため、英語で検索するときは principle を添えると絞り込めます。
メイヤー版(1988年)とマーチン版(1996年)の違い
同じ一文を掲げていても、実現手段として想定しているものが両者で異なります。この差を知らないまま古い解説と新しい解説を並べて読むと、継承すべきなのかインターフェースを切るべきなのかで矛盾した結論になります。
| 観点 | メイヤー版(1988年) | マーチン版(1996年) |
|---|---|---|
| 出典 | Object-Oriented Software Construction | C++ Report 1996年1月号 |
| 拡張の手段 | 実装継承 | 抽象基底クラスの実装 |
| 閉じている状態 | すでに配布・利用されている | 抽象メソッドの並びが不変 |
| 利用側の参照先 | 親クラス | 抽象型 |
| SOLIDでの参照 | ほぼ参照されない | SOLIDの「O」はこちら |
表の「閉じている状態」の差が、そのまま実装方針を分けます。
メイヤー版:配布済みのクラスを、親クラスとして拡張する
メイヤーの定義では、モジュールが open であるとは “still available for extension”(まだ拡張可能である)状態、closed であるとは “available for use by other modules”(他のモジュールから利用可能になっている)状態を指します。closed の前提として、仕様がよく定義されて安定しており、実装が情報隠蔽されていることが求められます。つまり「コンパイルしてライブラリに置き、すでにクライアントが呼んでいる」段階に入ったものが closed です。
実現手段は実装継承です。いったん配布したクラスはそのまま凍結し、拡張が必要になったら子クラスを派生させてフィールドやメソッドを足します。この版は親クラスが具象クラスとして実装を持つことを前提にしており、抽象クラスを必須としていません。1988年当時のEiffelの言語機能を土台にした定義で、継承のアンチパターンと委譲との使い分けが広く議論される前の設計観に立っています。「継承より合成」という現在の慣習とは前提が違う、という点だけ押さえておけば十分です。
マーチン版:抽象基底クラスを固定し、派生クラスを差し替える
マーチンが1996年1月号の C++ Report で示した版では、拡張点は継承階層ではなく抽象です。原文の表現は abstract base class で、当時のC++に interface キーワードが無かったための語ですが、現代の言語ではインターフェースがそのまま対応します。不変にするのは抽象メソッドの並びだけで、実装は兄弟関係にある複数のクラスが個別に持ちます。利用側は抽象型の変数を通して呼び出すため、実行時にどの実装が入っているかを知りません。この差し替えを成立させているのがポリモフィズム(多態性)です。
新しい振る舞いを足すときは実装クラスを1つ増やすだけで済み、既存の実装クラスにも利用側にも触れません。この構造は上位モジュールを抽象に依存させるという意味で依存性逆転の原則と同じ仕組みに乗っており、マーチン版OCPを実装レベルに落とすと、多くの場合それは依存性逆転の原則(DIP)の適用そのものになります。現在SOLIDの文脈で語られるのはこちらなので、実務ではマーチン版を基準にして問題ありません。
SOLIDの「O」が指すのはどちらか
マーチンは2000年の論文 “Design Principles and Design Patterns” で複数の設計原則をまとめて提示しました。これらを SOLID という頭字語に整理したのはマイケル・フェザーズで、2004年ごろのことです。「O」が参照しているのはマーチン版の解釈であり、メイヤー版の実装継承モデルがSOLIDの文脈で引かれることはまずありません。オープンクローズドの原則はSOLIDのために作られたのではなく、先に存在した原則が後からSOLIDへ組み込まれた、という順序になります。
違反コードと修正例(Python)
違反の形はほぼ1つに集約されます。型を見て分岐する処理です。マーチンが1996年の記事で挙げた例も、図形の種類で分岐する描画関数でした。
型で分岐する違反例
import math
class Shape:
def __init__(self, kind, **params):
self.kind = kind
self.params = params
def area(shape):
if shape.kind == "circle":
return math.pi * shape.params["r"] ** 2
elif shape.kind == "rectangle":
return shape.params["w"] * shape.params["h"]
raise ValueError(shape.kind)
def label(shape):
if shape.kind == "circle":
return "円"
elif shape.kind == "rectangle":
return "長方形"
raise ValueError(shape.kind)
三角形を追加するには area と label の両方に分岐を足す必要があります。修正に対して閉じていません。しかも同じ kind による判定は、描画・検証・シリアライズと処理が増えるほど別の関数へ複製されていきます。1種類の追加が複数箇所の修正を呼ぶこの伝播が、オープンクローズドの原則に違反したときの具体的な損失です。
抽象基底クラスで閉じた設計に直す
import math
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self) -> float:
...
@abstractmethod
def label(self) -> str:
...
class Circle(Shape):
def __init__(self, r):
self.r = r
def area(self) -> float:
return math.pi * self.r ** 2
def label(self) -> str:
return "円"
class Rectangle(Shape):
def __init__(self, w, h):
self.w = w
self.h = h
def area(self) -> float:
return self.w * self.h
def label(self) -> str:
return "長方形"
# 追加はこのクラスだけ。既存クラスと利用側は無修正
class Triangle(Shape):
def __init__(self, b, h):
self.b = b
self.h = h
def area(self) -> float:
return 0.5 * self.b * self.h
def label(self) -> str:
return "三角形"
def report(shapes):
for s in shapes:
print(s.label(), s.area())
差分は Triangle クラスの追加だけで、report にも既存の2クラスにも手が入りません。判定の目安は単純です。新しい種類を1つ足したときの変更行が、新規ファイルの中だけに収まるか。既存ファイルに1行でも足す必要があるなら、その軸に対しては閉じていません。この判定は言語に依存しないので、抽象型を持つ言語であればTypeScriptでもJavaでもGoでも同じ形に書けます。
オープンクローズドの原則を適用すべきでない場面
この原則は無条件に適用するものではありません。マーチン自身が1996年の原文で、まず「どれほど閉じたモジュールでも、閉じられていない種類の変更が必ず存在する」と断ったうえで、こう続けています。”Since closure cannot be complete, it must be strategic. That is, the designer must choose the kinds of changes against which to close his design. This takes a certain amount of prescience derived from experience.”(閉じることは完全にはできないので、戦略的でなければならない。設計者はどの種類の変更に対して設計を閉じるかを選ばねばならず、それには経験に由来するある程度の先読みが要る)。ここでの closure は関数閉包のクロージャではなく「閉じること」を指します。
先読みが要るという条件が付いている以上、その領域の経験が無い段階で拡張点を決めるのは設計判断ではなく賭けです。線引きは次の点に置きます。
- 同種の変更が2回目に来るまで閉じない:1回目は素直に既存コードを修正する。2回目で初めて変更の軸が観測されるので、そこで抽象を切る。
- 実装が1つしかないインターフェースは拡張点として数えない:差し替え候補が存在しないなら、増えたのは間接参照1段だけ。テスト用のモックが目的なら、OCPではなくテスト容易性を理由に正当化する。
- 変更頻度がゼロに近い領域では適用しない:法令で固定された計算式や外部仕様で決まった変換など、変わらないと分かっているものを拡張可能にしても、読む手間が増えるだけになる。
観測されていない変更に備えて抽象を先に置くのは投機的一般化と呼ばれるアンチパターンで、YAGNI(ヤグニ)原則が明確に禁じている行為でもあります。オープンクローズドの原則とYAGNIは対立して見えますが、実際には「閉じる軸が観測されたら閉じる、されていないなら閉じない」という同じ判断基準の表と裏です。抽象化の量ではなくタイミングを問題にしている点で、両者は一致しています。
他のSOLID原則・デザインパターンとの接続
適用の順序は、変更理由でクラスを分ける(SRP)、分かれた境界のうち変更が繰り返される側に抽象を置く(OCP)、上位モジュールをその抽象に依存させる(DIP)となります。SRPを飛ばしてOCPから入ると、何を境界にすべきかが決まらないまま抽象だけが増えるので、分割の粒度で迷う場合はSRP違反と過剰分割の見極めを先に押さえてください。派生クラスが親の契約を破ればポリモーフィズムごと壊れるため、リスコフの置換原則はOCPの前提条件にあたります。
実装パターンとしては、振る舞いを抽象へ切り出して差し替えるStrategyパターンが最も直接的な適用例です。生成処理を閉じたい場合はFactory系、既存クラスの前後に処理を足したい場合はDecoratorが対応し、全体の見取り図はGoFのデザインパターン23種類の一覧で確認できます。層の境界にこの原則を効かせる設計に踏み込むなら、クリーンアーキテクチャの4層構造と依存性逆転が次の到達点です。
よくある質問
オープンクローズドの原則は日本語で何と呼びますか?
「開放閉鎖の原則」「開放/閉鎖原則」「開閉原則」がいずれも同じ原則の訳語です。日本語版Wikipediaの項目名は「開放/閉鎖原則」が採用されています。英語は Open-Closed Principle、略称は OCP、日本語記事では「OCP原則」と重ねて書かれることもあります。
オープンクローズドの原則は誰が提唱したのですか?
バートランド・メイヤーが1988年の著書『Object-Oriented Software Construction』で示したのが最初です。ロバート・C・マーチンは1996年1月号の C++ Report で抽象を前提とする形に定義し直した人物であり、原則そのものの提唱者ではありません。この記事はマーチンの連載「Engineering Notebook」の第1回にあたります。SOLIDという頭字語を付けたのはさらに別人のマイケル・フェザーズで、2004年ごろです。
「拡張に開き、修正に閉じる」はメイヤーの言葉ではないのですか?
違います。この定式はマーチンが1996年の記事でメイヤーを紹介する際に添えた言い換えで、原文にも To paraphrase him と書かれています。メイヤー自身は、クラスがコンパイルされライブラリに置かれクライアントから使われている状態を closed、任意の新しいクラスが親として使い機能を加えられる状態を open と説明していました。
単一責任の原則(SRP)との違いは何ですか?
SRPは「どこで分けるか」、OCPは「分けた後どう拡張するか」を扱います。SRPが変更理由の異なるコードを別クラスへ分離する基準であるのに対し、OCPは分離した境界のどちら側を抽象にして固定するかを決める原則です。適用順序はSRPが先になります。
すべてのクラスに最初からインターフェースを用意すべきですか?
いいえ。実装クラスが1つしかないインターフェースは差し替えの余地が無いため拡張点として機能せず、呼び出し経路を1段深くするだけに終わります。同じ種類の変更が2度目に発生した時点で抽象を切る運用にすると、必要な場所にだけ拡張点が残ります。