LASSIC Media らしくメディア
SOLID原則とは|5原則の要点とやりすぎの落とし穴
監修・編集責任者:牛尾 昭昌(株式会社LASSIC 執行役員)
この記事の結論
- SOLID原則は、クラスやモジュールの分け方とつなぎ方を示す5つの設計原則で、目指すのは既存のコードを変えずに機能を足せる設計です。
- 開放閉鎖が目的、依存性逆転がその手段、リスコフの置換が差し替えの約束にあたり、インターフェース分離と単一責任が分け方を支えます。
- 当てはめすぎると抽象が増えて読みにくくなるため、変更の履歴と見込みから、分ける理由を説明できる箇所に絞って使います。
※ 本記事は2026年10月時点の公式情報(省庁・公的機関、および製品やサービスを提供する事業者が公開している資料)に基づきます。
SOLID原則は、オブジェクト指向設計でクラスやモジュールをどう分け、どうつなぐかを示した5つの原則の総称です。単一責任、開放閉鎖、リスコフの置換、インターフェース分離、依存性逆転の英語名の頭文字を並べた呼び名で、設計レビューや技術面の打ち合わせでよく引き合いに出されます。一方で、名前だけが独り歩きし、かえって読みにくいコードが増える場面もあります。
本記事では、開発に携わるエンジニアやPMの方に向けて、Robert C. Martinの論文とブログ、リスコフの置換原則の元になったBarbara LiskovとJeannette Wingの論文をもとに、5つの原則の要点と原則どうしの関係を整理します。Pythonの短いコードで違反と改善の形を示し、やりすぎの落とし穴にも触れます。
目次
SOLID原則とは
SOLID原則の考え方の多くは、Robert C. Martinが1990年代から示してきたものです。Martinは2000年の論文「Design Principles and Design Patterns」で、設計が劣化していく兆候として、硬さ(Rigidity)、もろさ(Fragility)、再利用しにくさ(Immobility)、粘り気(Viscosity)の4つを挙げました。*1 小さな変更が依存先へ次々と波及する、直すたびに関係の無い箇所が壊れる、といった状態です。
Martinは、4つの兆候はどれもモジュール間の不適切な依存から生じると見て、依存を管理するクラス設計の原則として開放閉鎖・リスコフの置換・依存性逆転・インターフェース分離の4つを並べました。単一責任の原則はこの論文には入っていません。Martinは2014年のブログで、凝集度と結合度の考え方を1990年代の終わりに1つの原則にまとめて名付けたと振り返っています。*2
| 頭文字 | 原則 | 要約 |
|---|---|---|
| S | 単一責任の原則(SRP) | 同じ理由で変わるものをまとめ、違う理由で変わるものを分ける |
| O | 開放閉鎖の原則(OCP) | モジュールは拡張に対して開き、変更に対して閉じる |
| L | リスコフの置換原則(LSP) | 派生クラスは基底クラスと置き換えられなければならない |
| I | インターフェース分離の原則(ISP) | 利用者ごとの小さなインターフェースは、汎用の大きな1つにまさる |
| D | 依存性逆転の原則(DIP) | 抽象に依存し、具象に依存しない |
5つは互いに支え合う関係にあり、関係は後の節で図にまとめます。まず原則ごとに要点を見ていきます。
単一責任の原則
単一責任の原則は「1つのクラスは1つのことだけをする」と説明されがちですが、Martinの定義は少し違います。Martinは、モジュールが変わる理由は1つだけであるべきだとし、その理由とは変更を求める人、つまり1つの業務を担う人々のまとまりだと説明しています。*2 例は、給与の計算、データベースへの保存、勤務時間の報告を1つの従業員クラスに持たせた設計です。それぞれ財務、業務運営、技術の部門が変更を求めるため、ある部門の依頼で直した箇所が別の部門の機能を壊しかねません。
実務では、業務ルールと画面の処理を混ぜない、SQLと通信の処理を混ぜない、といった形で現れます。どこで業務を区切るかは、ドメイン駆動設計の境界づけられたコンテキストの考え方とも重なります(「ドメイン駆動設計とは」)。この記事では「変更を求める人で区切る」という点だけを押さえておきます。
開放閉鎖の原則
開放閉鎖の原則は、モジュールを拡張に対して開き、変更に対して閉じておくという原則です。Martinは2000年の論文でこれをオブジェクト指向設計の原則のうち最も重要なものと位置づけ、起源をBertrand Meyerの仕事に求めています。モジュールのソースコードを書き換えずに、そのモジュールがすることを変えられるように作る、という意味です。
Martinは2014年のブログで、この原則の代表例としてプラグインの仕組みを挙げました。エディターなどは本体を書き換えずにプラグインで機能を足せます。そのためには、プラグインの依存がすべて本体を向き、本体はプラグインを知らない作りにしておく、と説明しています。*3
身近な例として、売上の明細を出力形式ごとの文字列に変える処理を考えます。変更前は形式を if 文で分けているため、形式を足すたびにこの関数を書き換えます。変更後は形式ごとの関数を登録表に載せ、export() は表を引くだけにしました。
# 変更前:形式が増えるたびに export() を書き換える
def export(rows, fmt):
if fmt == "csv":
return [",".join(map(str, r)) for r in rows]
raise ValueError(f"未対応の形式: {fmt}")
# 変更後:形式は登録で足し、export() は書き換えない
EXPORTERS = {}
def exporter(fmt):
def register(fn):
EXPORTERS[fmt] = fn
return fn
return register
@exporter("csv")
def to_csv(rows):
return [",".join(map(str, r)) for r in rows]
def export(rows, fmt):
return EXPORTERS[fmt](rows)
変更後の形なら、JSON Lines形式を足すときも別のファイルに関数を書いて登録するだけで、export() と既存のCSV出力には手を触れません。実行すると、CSVとJSON Linesの両方を同じ export() で出力できました。変更前の関数に同じ形式を渡すと「未対応の形式」のエラーになります。
ただし、すべての変更に備えるのは現実的ではありません。備える方向を決めずに差し替え口を増やすと、使われない抽象だけが残ります。出力形式のように実際に増えてきた軸に絞るのが扱いやすい線です。
リスコフの置換原則
リスコフの置換原則は、派生クラスは基底クラスと置き換えられなければならない、という原則です。*1 元になったLiskovとWingの1994年の論文は、型Tのオブジェクトについて証明できる性質は、Tの部分型Sのオブジェクトについても成り立つべきだ、という形で部分型の条件を示しました。*4 Martinはこれを契約の言葉に言い直し、派生クラスのメソッドの事前条件は基底クラスより強くなく、事後条件は弱くない、としています。
次の例では、長方形を継承した正方形が、幅を変えるときに高さも変えてしまいます。長方形を受け取る関数は「幅だけが変わる」ことを前提にしているため、正方形を渡すと前提が崩れます。
class Rectangle:
def __init__(self, w, h):
self.w, self.h = w, h
def set_width(self, w):
self.w = w
class Square(Rectangle):
def __init__(self, side):
super().__init__(side, side)
def set_width(self, w): # 正方形を保つため高さも変える
self.w = self.h = w
def widen(r): # 呼び出し側は「幅だけが変わる」と信じている
h = r.h
r.set_width(10)
assert r.w * r.h == 10 * h, "高さまで変わった"
widen(Rectangle(2, 3)) # 通る
widen(Square(3)) # AssertionError: 高さまで変わった
実行すると、長方形では通り、正方形では AssertionError になりました。Martinは円と楕円を題材に同じ問題を示し、違反に気づいた呼び出し側に if/else の分岐が足されがちだと書いています。数学では正方形は長方形の一種でも、呼び出し側が頼っている振る舞いを守れないなら、置き換えはできません。
インターフェース分離の原則
インターフェース分離の原則は、利用者ごとの小さなインターフェースのほうが、汎用の大きな1つのインターフェースよりよい、という原則です。*1 Martinは、多くの利用者を持つクラスにすべての操作を載せた1つの口を用意すると、ある利用者が使う操作を変えただけで、ほかの利用者まで再コンパイルや再配置が要る場合があると説明しています。2020年のブログでは、使わないものに依存しないようインターフェースを小さく保つ、と要約しました。
動的型付けの言語では再コンパイルの問題は小さくなりますが、Martinは影響が無くなるわけではないとしています。*5 差が出やすいのはテストです。次の例の集計関数は、取得と保存をまとめた抽象クラスではなく、取得だけを持つ小さな型に依存しています。
from abc import ABC, abstractmethod
from typing import Protocol
class Storage(ABC): # 取得と保存をまとめた大きな口
@abstractmethod
def fetch(self, month): ...
@abstractmethod
def save(self, month, rows): ...
class SalesReader(Protocol): # 集計が使う操作だけ
def fetch(self, month): ...
def monthly_total(src: SalesReader, month):
return sum(r["amount"] for r in src.fetch(month))
class FakeReader: # テスト用の偽物は fetch だけでよい
def fetch(self, month):
return [{"amount": 1200}, {"amount": 800}]
print(monthly_total(FakeReader(), "2026-09")) # 2000
集計のテストは fetch だけを持つ偽物で済み、結果は2000でした。一方、Storage を継承して fetch だけを書いた偽物は、生成した時点で TypeError になり、使わない save まで求められました。typing.Protocol は、型チェッカーが構造的部分型(静的なダックタイピング)として照合するための仕組みで、継承を宣言しなくても型が合うかを確かめられます。*6 mypy などの型チェッカーをCIで回す前提で使います。
Martinは、利用者のクラス1つごとに専用のインターフェースを作れという意味ではない、とも書いています。利用者を種類で分けて種類ごとにインターフェースを作り、複数の種類が同じ操作を使うなら、その操作を両方に載せればよいという考え方です。
依存性逆転の原則
依存性逆転の原則は、抽象に依存し、具象に依存しない、という原則です。Martinは、手続き型の設計では上位のモジュールが下位に、下位がさらに下位に依存する構造になりやすいと指摘しました。上位のモジュールが扱うのはアプリケーションの方針で、方針は実装の細部にはあまり関心がないのに、なぜ細部のモジュールに直接依存しなければならないのか、という問いです。*1 2020年のブログでは、設計上の境界をまたぐ依存を、細部ではなく上位の抽象へ向けるよう求めています。*5
依存性逆転の原則と依存性注入(DI)は名前が似ていますが、別のものです。前者は依存をどちらへ向けるかという設計の原則で、後者は使う部品を外から渡すという実装の手法です。DIは依存性逆転を実現する手段の1つとしてよく使われます。DIの仕組みやDIコンテナは「依存性注入(DI)とは」で扱っています。
原則どうしの関係
Martinは2000年の論文で、開放閉鎖の原則がオブジェクト指向の設計の目的を示すなら、依存性逆転の原則はその主な仕組みを示す、と述べています。*1 既存のコードを変えずに足せるようにするには、上位の方針が具体的な実装ではなく抽象に依存している必要があるからです。
リスコフの置換原則は、その抽象が約束どおりに差し替えられることを支えます。Martinは、リスコフの置換原則の違反は開放閉鎖の原則の潜在的な違反だとしています。約束を破る実装があると、呼び出し側に型を見分ける分岐が要り、実装を足すたびに既存のコードを書き換えるためです。インターフェース分離の原則は依存する抽象を利用者ごとに小さく保ち、単一責任の原則はそもそも何を1つのまとまりにするかを決める土台になります。
やりすぎの落とし穴と確かめ方
原則は、当てはめすぎると別の問題を生みます。よくあるのは、差し替える予定の無いクラスにまでインターフェースを用意し、処理をたどるたびにファイルを何枚も開く形です。Martin自身も、依存性逆転の原則はあらゆる具象を変わりやすいものと仮定しているが例外もあるとし、C言語の標準ライブラリ string.h のように具体的でも変わりにくいものに依存するのは害にならない、と書いています。*1
単一責任の原則を「クラスは小さいほどよい」と読み、1つの業務の流れを細かなクラスに刻みすぎるのも落とし穴です。Martinの定義は変更を求める人でまとめるものなので、同じ人が同じ理由で変える処理は、むしろ1か所に集めます。インターフェース分離も、前の節で見たとおり、利用者1つごとに口を作ることまでは求めていません。
判断の目安は、変更の履歴と今後の見込みです。同じ箇所が別々の理由で何度も書き換えられる、1か所の修正で無関係なテストが落ちる、といった兆しが出てから分けても遅くはありません。Martinは2020年のブログで、ただ「単純に書け」と言うだけでは導きにならず、単純さを保つには原則に導かれた規律が要る、と述べています。原則は、分ける理由を説明するための言葉として使うと扱いやすくなります。
開発の委託や設計レビューでは、原則の名前が守られているかではなく、分けた理由を説明できるかを確かめます。差し替え口ごとに備えた変更を設計書やプルリクエストに書き残してもらう、実装が1つだけの抽象を一覧にして要否を見直す、といった確認が役立ちます。型の照合と自動テストをCIに組み込んでおくと、置き換えの約束が破られたときに早く気づけます。
まとめ:SOLID原則で確かめておきたい3つの点
確かめておきたい点は3つです。第一に、SOLID原則は変えずに足せる設計のための5つの原則で、開放閉鎖が目的、依存性逆転が主な手段にあたること。第二に、リスコフの置換原則は差し替えの約束を、インターフェース分離と単一責任の原則は抽象とまとまりの分け方を支えること。第三に、当てはめすぎると使われない抽象や細切れのクラスが増えるため、変更の履歴と見込みから、分ける理由を説明できる箇所に絞ることです。
よくある質問
SOLID原則は、マイクロサービスや動的型付けの言語でも意味がありますか
Martinは2020年のブログで、変わる理由の違うコードを混ぜればマイクロサービスでも絡み合う、リスコフの置換原則は継承ではなく部分型の話でダックタイピングにも当てはまる、として今も有効だとしています。クラスを使わない書き方でも、同じ考え方を使えます。
5つの原則は、すべて守らないといけませんか
すべてを常に満たす必要はありません。原則は、変更に強い形へ直すときの判断の手がかりです。変更が集中する箇所や、テストしにくい箇所から当てはめ、変わる見込みの無い部分まで抽象を足さないほうが、読みやすさを保てます。
既存のシステムに後から取り入れられますか
取り入れられます。先に、直す箇所の今の振る舞いを自動テストで押さえてから、依存の向きや分け方を少しずつ整えます。一度に全体を作り直すより、改修の機会ごとに変更の多い箇所から手を入れるほうが、手戻りを抑えやすくなります。
SOLID原則を踏まえた設計・改修のご相談
元請(プライムベンダー)として、既存システムの設計の見直しから開発、保守・運用までご提案します。
Remoguとリラシクなら、設計の見直しや改修の開発に加わるITエンジニアも探せます。
Remoguは、リモート前提で全国から即戦力のITプロ人材を調達するサービスです。リラシクは、扱う求人がすべてリモートワークのITエンジニア専門転職エージェントです。どちらもLASSICが運営しています。
出典
- *1 参考:Robert C. Martin「Design Principles and Design Patterns」(2000年)(https://objectmentor.com/resources/articles/Principles_and_Patterns.pdf)。出典:設計の劣化の4つの兆候、不適切な依存が原因とする記述、開放閉鎖・リスコフの置換・依存性逆転・インターフェース分離の各原則の定義と説明(Circle/Ellipse、事前条件と事後条件、string.h の例外、利用者の種類ごとのインターフェース、OCPとDIPの関係)を参照(2026年10月確認)
- *2 参考:Robert C. Martin「The Single Responsibility Principle」(The Clean Code Blog、2014年5月8日)(https://blog.cleancoder.com/uncle-bob/2014/05/08/SingleReponsibilityPrinciple.html)。出典:変わる理由は1つとする定義、変更を求める人を理由とする説明、Employeeクラスの例、1990年代の終わりに名付けたとする記述を参照(2026年10月確認)
- *3 参考:Robert C. Martin「The Open Closed Principle」(The Clean Code Blog、2014年5月12日)(https://blog.cleancoder.com/uncle-bob/2014/05/12/TheOpenClosedPrinciple.html)。出典:プラグインの仕組みを開放閉鎖の原則の例とし、依存を本体へ向ける説明を参照(2026年10月確認)
- *4 参考:Barbara H. Liskov, Jeannette M. Wing「A Behavioral Notion of Subtyping」(ACM Transactions on Programming Languages and Systems, Vol. 16, No. 6, 1994年11月)(https://www.cs.cmu.edu/~wing/publications/LiskovWing94.pdf)。出典:Subtype Requirement(型Tについて証明できる性質は部分型Sでも成り立つべき)を参照(2026年10月確認)
- *5 参考:Robert C. Martin「Solid Relevance」(The Clean Code Blog、2020年10月18日)(https://blog.cleancoder.com/uncle-bob/2020/10/18/Solid-Relevance.html)。出典:5つの原則の要約、マイクロサービス・部分型・動的型付けの言語への言及、依存を上位の抽象へ向ける説明、単純さには原則に導かれた規律が要るとする記述を参照(2026年10月確認)
- *6 参考:Python Software Foundation「typing — Support for type hints」(https://docs.python.org/3/library/typing.html)。出典:typing.Protocol は構造的部分型(静的なダックタイピング)を扱う型チェッカーで主に使うとする説明を参照(2026年10月確認)