Python | OOP:単一責任の原則

Python Python
スポンサーリンク

概要(単一責任の原則は「そのクラスを変える理由をひとつに絞る」)

単一責任の原則(Single Responsibility Principle, SRP)は、「クラス(モジュール)が変更される理由はひとつだけであるべき」という指針です。役割が混ざると、ある変更が別の機能を壊しやすくなり、修正のたびに影響範囲が広がります。責務を分ければ、変更は局所化され、読みやすさ・テスト容易性・拡張性が大きく向上します。


直感で掴む(悪い例から良い例へ)

悪い例:1クラスが「保存・検証・通知」を全部やる

class UserService:
    def __init__(self, session, mailer):
        self.session = session
        self.mailer = mailer

    def register(self, name: str, email: str) -> dict:
        if not email or "@" not in email:               # 検証(バリデーション)
            raise ValueError("メールが不正")
        user = {"id": "1", "name": name, "email": email}
        self.session.save_user(user)                    # 永続化(保存)
        self.mailer.send(email, "ようこそ", "登録完了")  # 通知(メール送信)
        return user
Python

「検証」「保存」「通知」はそれぞれ別の変更理由を持ちます。どれかの仕様変更が、他の挙動に思わぬ副作用を起こしがちです。

良い例:責務ごとに分割し、サービスは“流れ”だけに集中

class UserValidator:
    def validate(self, name: str, email: str) -> None:
        if not name.strip():
            raise ValueError("名前が空")
        if not email or "@" not in email:
            raise ValueError("メールが不正")

class UserRepository:
    def __init__(self, session):
        self.session = session
    def save(self, user: dict) -> None:
        self.session.save_user(user)

class Notifier:
    def __init__(self, mailer):
        self.mailer = mailer
    def welcome(self, email: str) -> None:
        self.mailer.send(email, "ようこそ", "登録完了")

class UserService:
    def __init__(self, validator: UserValidator, repo: UserRepository, notifier: Notifier):
        self.validator = validator
        self.repo = repo
        self.notifier = notifier

    def register(self, name: str, email: str) -> dict:
        self.validator.validate(name, email)         # 検証(変更理由:入力仕様)
        user = {"id": "1", "name": name, "email": email}
        self.repo.save(user)                          # 保存(変更理由:DB仕様)
        self.notifier.welcome(email)                  # 通知(変更理由:コミュニケーション仕様)
        return user
Python

各クラスが単一の責務を持ち、変更理由が明確です。サービスは「順序と連携」に専念し、個別の仕様変更は担当クラスだけ修正すれば済みます。


実務での分割基準(変更理由で切る・入出力と変換を分ける)

変更理由で切る

  • 入力仕様が変わる: 検証クラスに集約。
  • 保存先やスキーマが変わる: リポジトリ(永続化)で受け止める。
  • 通知先や文面が変わる: 通知クラスで表現する。
  • ビジネス規則が変わる: 規則(Rule/Strategy)を分離して持つ。

入出力と変換の分離

  • 入出力(I/O): DBやHTTPなどの境界は“外部と話す責務”に限定。
  • 変換(Pureロジック): データ整形・計算などは副作用のない関数やクラスへ分離。 これでテストが軽くなり、I/Oをモックで差し替えてもロジックの検証は純粋に行えます。

境界の引き方を先に決める

  • 公開面(インターフェース): 利用者が必要とする最小のメソッドだけを出す。
  • 内部詳細: 部品に委譲し、外部へ漏らさない。戻り値・例外は“意味”が一貫するように揃える。

重要ポイントの深掘り(責務の定義、凝集度・結合度、テスト容易性)

責務の定義を言語化する

  • 責務の一文: 「このクラスはXを行い、Yは行わない」。この一文でズレが見えるようにします。
  • 境界の明記: 「入力検証と保存は分ける」「通知は永続化の中で行わない」など、やらないことを決める。

凝集度と結合度

  • 高凝集: 関連の強いロジックをひとつのクラスへ集める(責務が明確)。
  • 低結合: クラス同士の依存を小さくし、インターフェースに依存させる(差し替え可能)。 SRPは「高凝集・低結合」を支える基盤です。

テスト容易性が指標になる

  • 単体テストが独立: 1クラスのテストが他の境界を必要としないなら、責務が適切に分離されています。
  • 失敗パスの再現が容易: 入力検証や例外処理を個別に試せるなら、設計は健全です。

現実での折り合い(粒度のバランス・ユースケース単位・まとめ役)

粒度は“変更の頻度”で決める

頻繁に変わる部分(文面、ルール、フォーマット)は独立させ、滅多に変わらないもの(単純なDTO)はまとめて構いません。分割しすぎても管理コストが上がるため、現場の変更理由に合わせてバランスします。

ユースケース単位のサービス

「ユーザー登録」「支払い」「レポート出力」などユースケースごとに“まとめ役”のサービスを置き、部品を組み合わせて流れを定義します。サービス自身は“決めるだけ・委譲するだけ”で、ロジックは部品へ。

ファサードで外部公開を簡単に

内部が分割されても、外部へはシンプルな窓口(ファサード)を提供します。SRPを守りながら、利用者にはわかりやすいAPIを約束できます。


例題(実務に近い分割でSRPを体感する)

検証・正規化・保存・通知の分離

class Normalizer:
    def user(self, name: str, email: str) -> dict:
        return {"name": name.strip(), "email": (email or "").strip()}

class UserValidator:
    def user(self, data: dict) -> None:
        if not data["name"]:
            raise ValueError("名前が空")
        if "@" not in data["email"]:
            raise ValueError("メールが不正")

class UserRepository:
    def __init__(self, session): self.session = session
    def save(self, user: dict) -> dict:
        uid = self.session.save_user(user)
        return {**user, "id": uid}

class Notifier:
    def __init__(self, mailer): self.mailer = mailer
    def welcome(self, email: str) -> None:
        self.mailer.send(email, "ようこそ", "登録完了")

class UserRegistrationService:
    def __init__(self, normalizer: Normalizer, validator: UserValidator,
                 repo: UserRepository, notifier: Notifier):
        self.normalizer = normalizer
        self.validator = validator
        self.repo = repo
        self.notifier = notifier

    def register(self, name: str, email: str) -> dict:
        data = self.normalizer.user(name, email)  # 正規化(フォーマットの変更理由をここへ)
        self.validator.user(data)                 # 検証(入力仕様の変更理由をここへ)
        saved = self.repo.save(data)              # 保存(DB仕様の変更理由をここへ)
        self.notifier.welcome(saved["email"])     # 通知(コミュニケーション仕様の変更理由をここへ)
        return saved
Python

一連の流れは保ちつつ、各責務を独立させることで、どこが変わっても他へ波及しにくく、テストも部品単位で楽に行えます。

価格計算:ルールを戦略として分離(SRP+Strategy)

class TaxRule:
    def __init__(self, rate: float): self.rate = rate
    def apply(self, amount: float) -> float: return amount * (1 + self.rate)

class DiscountRule:
    def __init__(self, off: float): self.off = off
    def apply(self, amount: float) -> float: return max(0.0, amount - self.off)

class PriceService:
    def __init__(self, tax: TaxRule, discount: DiscountRule):
        self.tax = tax
        self.discount = discount

    def total(self, base: float) -> float:
        after_disc = self.discount.apply(base)
        return self.tax.apply(after_disc)
Python

「税」「割引」はそれぞれ単一責任。組み合わせはサービスが担い、どちらかが変わってももう片方へ副作用が及びません。


まとめ(単一責任は“変更理由の分離”でコードを強くする)

単一責任の原則は、クラスを「ひとつの変更理由」に絞り、他の理由を別クラスへ分離することです。検証・正規化・永続化・通知・ビジネス規則をそれぞれ独立させ、サービスは“流れを決めて委譲する”役に徹する。公開面は最小に、テストは部品単位で軽く、現場の変更頻度に合わせて粒度を調整する。これを徹底すれば、初心者でも「壊れにくく、拡張しやすく、テストが速い」OOP設計を自然に身につけられます。

タイトルとURLをコピーしました