SOLID原則とは
SOLID は Robert C. Martin(Uncle Bob)が提唱したオブジェクト指向設計の5つの原則の頭文字です。これらを守ることで、変更に強く・テストしやすく・理解しやすいコードになります。
| 文字 | 原則名 | 一言まとめ |
|---|---|---|
| S | Single Responsibility | クラスの責務は1つだけ |
| O | Open/Closed | 拡張には開かれ、修正には閉じている |
| L | Liskov Substitution | 派生クラスは基底クラスと置換可能 |
| I | Interface Segregation | 使わないメソッドに依存させない |
| D | Dependency Inversion | 抽象に依存し、具体に依存しない |
S — 単一責任の原則(SRP)
クラスを変更する理由は1つだけであるべき
# ❌ 悪い例: ユーザー管理・メール送信・ログ出力が混在
class UserService:
def create_user(self, name, email):
# ユーザーを作成
user = {"name": name, "email": email}
# メール送信(別の責務)
self._send_welcome_email(email)
# ログ出力(別の責務)
print(f"[LOG] User created: {name}")
return user
def _send_welcome_email(self, email):
# SMTPを直接扱う
...
# ✅ 良い例: 責務を分離
class UserRepository:
def create(self, name, email):
return {"name": name, "email": email}
class EmailService:
def send_welcome(self, email):
...
class Logger:
def info(self, message):
print(f"[LOG] {message}")
class UserService:
def __init__(self, repo, email_service, logger):
self.repo = repo
self.email_service = email_service
self.logger = logger
def create_user(self, name, email):
user = self.repo.create(name, email)
self.email_service.send_welcome(email)
self.logger.info(f"User created: {name}")
return user
変更理由が分離されているので、メール送信ロジックを変えても UserRepository に影響しません。
O — 開放/閉鎖の原則(OCP)
ソフトウェアは拡張に対して開かれ、修正に対して閉じているべき
# ❌ 悪い例: 新しい支払い方法を追加するたびにこのクラスを修正する
class PaymentProcessor:
def process(self, method, amount):
if method == "credit_card":
self._charge_credit_card(amount)
elif method == "paypal":
self._charge_paypal(amount)
elif method == "bitcoin": # 追加するたびにここを変更
self._charge_bitcoin(amount)
# ✅ 良い例: 抽象クラスで拡張できるよう設計
from abc import ABC, abstractmethod
class PaymentMethod(ABC):
@abstractmethod
def charge(self, amount: float) -> bool:
...
class CreditCard(PaymentMethod):
def charge(self, amount):
print(f"クレジットカード決済: {amount}円")
return True
class PayPal(PaymentMethod):
def charge(self, amount):
print(f"PayPal決済: {amount}円")
return True
class Bitcoin(PaymentMethod): # 新メソッド追加 = 新クラス追加だけ
def charge(self, amount):
print(f"Bitcoin決済: {amount}円")
return True
class PaymentProcessor: # このクラスは変更しない
def process(self, method: PaymentMethod, amount: float):
return method.charge(amount)
L — リスコフの置換原則(LSP)
派生クラスは基底クラスと置換可能でなければならない
# ❌ 悪い例: 正方形は長方形の派生クラスだが、置換すると挙動が壊れる
class Rectangle:
def __init__(self, w, h):
self.width = w
self.height = h
def area(self):
return self.width * self.height
class Square(Rectangle):
def __init__(self, side):
super().__init__(side, side)
def set_width(self, w): # 正方形は幅だけ変えられない
self.width = w
self.height = w # 両方変えてしまう
def process(rect: Rectangle):
rect.set_width(5)
# 長方形なら height はそのまま → area は 5*h
# 正方形だと height も 5 になる → area は 25(期待と違う)
# ✅ 良い例: 共通の基底を持つが is-a 関係を無理に作らない
class Shape(ABC):
@abstractmethod
def area(self) -> float: ...
class Rectangle(Shape):
def __init__(self, w, h):
self.width = w
self.height = h
def area(self):
return self.width * self.height
class Square(Shape):
def __init__(self, side):
self.side = side
def area(self):
return self.side ** 2
I — インターフェース分離の原則(ISP)
クライアントが使わないメソッドへの依存を強制しない
# ❌ 悪い例: 太りすぎたインターフェース
class Worker(ABC):
@abstractmethod
def work(self): ...
@abstractmethod
def eat(self): ...
@abstractmethod
def sleep(self): ...
class Robot(Worker):
def work(self): print("作業中")
def eat(self): raise NotImplementedError("ロボットは食事しない") # 意味がない
def sleep(self): raise NotImplementedError("ロボットは睡眠しない")
# ✅ 良い例: 細かく分割されたインターフェース
class Workable(ABC):
@abstractmethod
def work(self): ...
class Eatable(ABC):
@abstractmethod
def eat(self): ...
class Human(Workable, Eatable):
def work(self): print("作業中")
def eat(self): print("食事中")
class Robot(Workable): # 必要なものだけ実装
def work(self): print("作業中")
D — 依存性逆転の原則(DIP)
上位モジュールは下位モジュールに依存してはならない。両者とも抽象に依存すべき
# ❌ 悪い例: 上位モジュールが具体クラスに直接依存
class MySQLDatabase:
def save(self, data):
print(f"MySQLに保存: {data}")
class UserService:
def __init__(self):
self.db = MySQLDatabase() # 具体クラスに直接依存
def save_user(self, user):
self.db.save(user)
# ✅ 良い例: 抽象(インターフェース)に依存
class Database(ABC):
@abstractmethod
def save(self, data): ...
class MySQLDatabase(Database):
def save(self, data):
print(f"MySQLに保存: {data}")
class PostgreSQLDatabase(Database):
def save(self, data):
print(f"PostgreSQLに保存: {data}")
class UserService:
def __init__(self, db: Database): # 抽象に依存
self.db = db
def save_user(self, user):
self.db.save(user)
# 使う側がどのDBを使うか決める(DIコンテナや手動注入)
service = UserService(MySQLDatabase())
service = UserService(PostgreSQLDatabase()) # 差し替え自由
これが**依存性注入(Dependency Injection)**です。テスト時はモックDBを注入できます。
まとめ
| 原則 | 違反した場合のリスク | 守るメリット |
|---|---|---|
| SRP | 変更が多くの場所に波及する | 変更の影響範囲が小さい |
| OCP | 機能追加のたびに既存コードを変更する | 既存コードに触れず機能追加できる |
| LSP | 多態性が期待通りに動かない | 安全に派生クラスが使える |
| ISP | 不要な依存でコンパイルエラーや修正が増える | 必要最小限の依存で保守しやすい |
| DIP | テストが困難・変更コストが高い | テスト容易性・差し替え可能性が高まる |
SOLID原則は「すべてに適用すべき絶対ルール」ではなく、設計の判断軸として使うものです。過度な抽象化は逆にコードを複雑にします。