メインコンテンツへ移動
SEMentor
設計 約11分

SOLID原則

保守性の高いオブジェクト指向設計の5原則 — SRP・OCP・LSP・ISP・DIP を実例で学ぶ

SOLID原則とは

SOLID は Robert C. Martin(Uncle Bob)が提唱したオブジェクト指向設計の5つの原則の頭文字です。これらを守ることで、変更に強く・テストしやすく・理解しやすいコードになります。

文字原則名一言まとめ
SSingle Responsibilityクラスの責務は1つだけ
OOpen/Closed拡張には開かれ、修正には閉じている
LLiskov Substitution派生クラスは基底クラスと置換可能
IInterface Segregation使わないメソッドに依存させない
DDependency 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原則は「すべてに適用すべき絶対ルール」ではなく、設計の判断軸として使うものです。過度な抽象化は逆にコードを複雑にします。

このレッスンは未完了です。

次のレッスンへ