プログラムをあとから直したり足したりしても壊れにくくする考え方が「SOLID原則」です。
SOLIDは5つの短いルールの頭文字で、初心者でも少しずつ実践しながら身につけられる実用的な指針です。
この記事では意味と覚え方をやさしく解説します。
SOLID原則とは
プログラミング初心者向けのやさしい定義
SOLID原則は、クラスやメソッドの作り方を迷わないように導いてくれる5つの設計ルールです。
難しい数学や専門用語を覚えなくても、日常の例えに置きかえると理解しやすく、コードが大きくなっても見通しを保てます。
役割を1つにすることや変えずに足す工夫など、現場でよく起きる問題に対するシンプルな解決策のセットだと思って大丈夫です。
オブジェクト指向プログラミングで役立つ理由
オブジェクト指向は「モノの責任」と「モノ同士の関係」を考えるので、SOLID原則ととても相性が良いです。
なぜ役立つかを短くまとめます。
- 変更に強いコードになる: 要件が変わっても影響範囲が小さく、安心して直せます。
- 再利用しやすくなる: クラスの役割がはっきりし、別の場面でもそのまま使えます。
- テストしやすい: 部品が小さく分かれているので、個別に動作確認しやすいです。
5つの原則の全体像
SOLIDはSingle、Open-Closed、Liskov、Interface、Dependencyの5単語の頭文字です。
まずは全体を地図のように眺めましょう。
| 文字 | 原則(英語) | 日本語の意味 | 1行フレーズ | 日常イメージ |
|---|---|---|---|---|
| S | Single Responsibility Principle | 単一責任の原則 | 1つのクラスは1つの理由だけで変わる | 役割が1つの引き出し |
| O | Open-Closed Principle | 開放/閉鎖の原則 | 既存コードは変えずに機能を足す | コンセントにアダプタを足す |
| L | Liskov Substitution Principle | 置換の原則 | 親の代わりに子を使っても期待が変わらない | 同じ規格の電池の入れ替え |
| I | Interface Segregation Principle | インターフェース分離の原則 | 使わないメソッドは押しつけない | 必要なボタンだけのリモコン |
| D | Dependency Inversion Principle | 依存関係逆転の原則 | 具体ではなく抽象に依存する | 規格に合わせて機器をつなぐ |
「開放/閉鎖」の言い回しはややこしいですが「拡張に開いている、修正に閉じている」と覚えると楽です。
5つの意味(SOLID)をやさしく解説
S
どんな考え方
単一責任の原則は「1つのクラスは1つのことに集中する」考え方です。
そのクラスが変更される理由が1つだけなら、修正するときに他へ波及しにくくなります。
ありがちな失敗
表示も保存も通知も1つのクラスが全部やると、どれか1つを直すたびに他も壊れやすくなります。
役割ごとに分けると見通しが良くなります。
ミニ例(Python)
レポートの作成と出力を分けるだけで責任がクリアになります。
class Report:
def __init__(self, data):
self.data = data
def summarize(self):
return f"件数: {len(self.data)}" # まとめる責任
class ReportPrinter:
def print(self, report: Report):
print(report.summarize()) # 出力する責任
O
どんな考え方
開放/閉鎖の原則は「振る舞いは足せるが、既存コードはできるだけ触らない」ように設計する考え方です。
足しやすく直しにくい設計が理想です。
ありがちな失敗
新しい種類が増えるたびにif文を追加して既存の関数を書き換えるのは、壊すリスクを毎回増やす行為です。
共通の約束を作り、クラスを足して対応しましょう。
ミニ例(Python)
図形の面積合計を計算するクラスは、図形の追加に対して変更不要にできます。
import math
class Shape:
def area(self) -> float:
raise NotImplementedError()
class Rectangle(Shape):
def __init__(self, w, h):
self.w, self.h = w, h
def area(self):
return self.w * self.h
class Circle(Shape):
def __init__(self, r):
self.r = r
def area(self):
return math.pi * self.r * self.r
class AreaCalculator:
def total(self, shapes: list[Shape]) -> float:
return sum(s.area() for s in shapes) # ここは固定
# Triangleを足してもAreaCalculatorは変更不要
L
どんな考え方
置換の原則は「親クラスの代わりに子クラスを置いても、使う側の期待が変わらない」ことを保証する考え方です。
使う側が驚かないことが大切です。
ありがちな失敗
「鳥は飛ぶ」という親に対して「ペンギンは飛ばない」と例外を投げる子を作ると、使う側は困ります。
最初の約束自体を分けて考えるのがコツです。
ミニ例(Python)
飛べる鳥と飛べない鳥を別の約束に分けると、使う側の期待がぶれません。
class Bird:
def walk(self): pass
class FlyingBird(Bird):
def fly(self): pass
class Sparrow(FlyingBird):
def fly(self): print("バサバサ")
class Penguin(Bird):
def swim(self): print("スイスイ")
def travel_to(tree_top: FlyingBird):
tree_top.fly() # FlyingBirdだけ受け取れば安全
I
どんな考え方
インターフェース分離の原則は「使わないメソッドを押しつけない」ことです。
小さく分かれた約束により、必要な分だけ実装します。
ありがちな失敗
印刷・スキャン・FAXをひとまとめにした太った約束を作ると、印刷だけしたい機械もスキャンやFAXの空実装が必要になってしまいます。
ミニ例(Python)
役割ごとに小さな約束を作ると、実装がシンプルになります。
class Printer:
def print(self, text: str): pass
class Scanner:
def scan(self) -> str: pass
class SimplePrinter(Printer):
def print(self, text: str):
print(text)
class MultiFunctionMachine(Printer, Scanner):
def print(self, text: str): print(text)
def scan(self) -> str: return "scanned"
D
どんな考え方
依存関係逆転の原則は「具体的なクラスではなく、抽象的な約束に依存する」設計にすることです。
これにより差し替えが簡単になります。
ありがちな失敗
支払い処理が特定サービスのクラスを直接作って使っていると、別サービスに切り替えるだけで大量の修正が必要になります。
抽象に合わせて作れば差し替えは注入だけで済みます。
ミニ例(Python)
支払いサービスは「支払えるもの」という約束に依存し、具体の実装は外から渡します。
class PaymentGateway:
def pay(self, amount: int): raise NotImplementedError()
class StripeGateway(PaymentGateway):
def pay(self, amount: int): print(f"Stripeで{amount}円")
class PayPalGateway(PaymentGateway):
def pay(self, amount: int): print(f"PayPalで{amount}円")
class CheckoutService:
def __init__(self, gateway: PaymentGateway):
self.gateway = gateway # 抽象に依存
def checkout(self, amount: int):
self.gateway.pay(amount)
# 差し替えはここだけ
service = CheckoutService(StripeGateway())
service.checkout(1200)
SOLID原則の覚え方
SOLIDの語呂とイメージで暗記
「SOLIDは固い」という語感を利用して「壊れにくい設計」と結びつけると覚えやすいです。
Sは引き出し1個、Oはコンセントとアダプタ、Lは電池の規格、Iは必要なボタンだけのリモコン、Dは規格のプラグを思い出すと記憶が安定します。
Oの「開放/閉鎖」は「拡張はOK、修正はNG」と口に出して覚えましょう。
1行フレーズで覚えるコツ
短いフレーズを繰り返し唱えると実装中にすぐ思い出せます。
- S: 変わる理由は1つだけ
- O: 足す、でも変えない
- L: 親の約束は守る
- I: 使わない約束は渡さない
- D: 抽象に寄りかかる
かんたんなコード例でイメージ
「注入で差し替える」を体験すると、OとDが一気に腹落ちします。
以下は出力先を差し替えられる挨拶クラスです。
class Output:
def write(self, text: str): raise NotImplementedError()
class ConsoleOutput(Output):
def write(self, text: str): print(text)
class Greeter:
# D: Outputという抽象に依存
def __init__(self, out: Output):
self.out = out
# S: Greeterは挨拶文を作って出力するだけ
def hello(self, name: str):
self.out.write(f"Hello, {name}!")
# O: 新しい出力先を追加してもGreeterは変更不要
g = Greeter(ConsoleOutput())
g.hello("SOLID")
ファイル出力やWeb出力を新クラスで足せば、Greeterはそのまま使えます。
初心者の学び方と練習ステップ
小さなプログラムでSOLIDを試す
まずは小さな題材で各原則を1つずつ試すのが上達の近道です。
例えば、メモ帳アプリで「保存」「表示」「検索」を別クラスに分けてSを体験し、検索方法を足せるようにOを試す、といった具合に進めます。
規模が小さいほど、変更の効果が見えやすく学びやすいです。
リファクタリングで設計の違いを知る
最初はあえてベタ書きで作り、あとからSOLIDの観点で直すと、良い設計の価値が体感できます。
直す前後で何が変わったかを表で比べると理解が深まります。
| 観点 | 直す前 | 直した後 |
|---|---|---|
| 責任の数 | 1つのクラスに多い | 役割ごとに分離 |
| 変更の容易さ | 影響範囲が読めない | 変更範囲が小さい |
| テストのしやすさ | まとめてしか試せない | 部品ごとに試せる |
よくあるつまずきと回避法
「原則を守ること」自体が目的化すると複雑になりがちです。
以下の罠に注意しましょう。
- 分けすぎ問題: 小さなクラスを作りすぎて関係が複雑になることがあります。最初は大雑把に作り、必要になったら分けます。
- 早すぎる抽象化: まだ1種類しかないのに抽象で包むと分かりにくくなります。似たものが2つ出てから抽象化しても間に合います。
- if地獄の温存: Oを意識せずifを増やし続けると破綻します。共通の約束を作って足す方向へ切り替えましょう。
- テスト不足: 小さく分けたなら小さくテストしましょう。テストしやすさは設計の良さのサインです。
まとめ
SOLID原則は「壊れにくく、直しやすく、足しやすい」コードに近づくための5つの実践ルールです。
Sで役割をはっきりさせ、Oで変更を最小化し、Lで驚きをなくし、Iで約束を小さく保ち、Dで差し替えを容易にします。
まずは小さな題材で1つずつ試し、良い実感を積み重ねていきましょう。
慣れてくると、日常の設計判断の多くがこの5つに自然と収まることに気づくはずです。
