Java | オブジェクト指向:単一責任の原則(SRP)

Java Java
スポンサーリンク

単一責任の原則(SRP)とは

単一責任の原則(Single Responsibility Principle, SRP)は
「クラスやモジュールは“たった1つの責任(変更理由)だけ”を持つべき」
というルールです。

ここでいう「責任」は単なる「仕事の数」ではなく
「なぜこのクラスが変更されるのか」という“理由の種類”のことです。

名前の整形仕様が変わったから、DB の保存形式が変わったから、画面表示のレイアウトが変わったから……。
これらが全部 1 クラスに乗っていると、そのクラスは複数の責任を持っていることになります。


なぜ SRP が重要なのか

理由が複数あるクラスは、変更のたびに壊れやすくなります。

あるチームメンバーは「バリデーション仕様を変えたい」
別のメンバーは「保存方法を変えたい」
さらに別のメンバーは「ログ出力を変えたい」

それらがすべて同じクラスを触り始めると、変更がぶつかったり、副作用で他の処理が壊れたりします。

逆に、1 クラスが 1 つの理由でしか変更されないなら
「このクラスは“これ”のために存在している」と説明がつきやすく、
テストもしやすく、リファクタリングもしやすくなります。

SRP の狙いは
「変更の影響範囲を小さく閉じ込める」
「クラスごとの役割を明確にする」
この2つです。


悪い例:単一責任を守れていないクラス

まずは「やってはいけない例」を見てみます。

final class BadUserService {

    String create(String rawName) {
        // 1. 入力の整形(フォーマット)
        String name = rawName == null ? "" : rawName.trim().replaceAll("\\s+", " ");

        // 2. 検証(バリデーション)
        if (name.isBlank() || name.length() > 50) {
            throw new IllegalArgumentException("invalid name");
        }

        // 3. 保存(永続化)
        System.out.println("DB INSERT name=" + name);

        // 4. 表示用の文字列生成
        return "User(" + name + ")";
    }
}
Java

このクラス(というかメソッド)は、明らかに複数の責任を持っています。

1つめの責任は「名前文字列の整形」。
2つめの責任は「名前の検証」。
3つめの責任は「ユーザの保存方法」。
4つめの責任は「画面やログ用の表示フォーマット」。

どれか 1 つの仕様が変わるたびに create を触ることになり、
他の部分に影響を与えるリスクが高くなります。

さらに、テストを書こうとすると「整形」「検証」「保存」「表示」がくっついているせいで、
単体テストをするのが難しい構造です。


SRP を守った分割例(クラスを責務ごとに分ける)

同じ処理を、SRP を意識して分割してみます。

入力の整形という責任

interface NameNormalizer {
    String normalize(String raw);
}

final class SimpleNameNormalizer implements NameNormalizer {
    @Override
    public String normalize(String raw) {
        return raw == null ? "" : raw.trim().replaceAll("\\s+", " ");
    }
}
Java

このクラスは「名前文字列の整形」という責任だけを持ちます。
仕様変更(半角カナを全角にしたい、など)があっても、このクラスだけを触ればよくなります。

検証という責任

interface NameValidator {
    boolean isValid(String name);
}

final class DefaultNameValidator implements NameValidator {
    @Override
    public boolean isValid(String name) {
        return name != null && !name.isBlank() && name.length() <= 50;
    }
}
Java

ここは「名前が有効かどうかを判断する」責任だけ。
チェックルールが変わってもこのクラスだけに閉じ込められます。

永続化という責任

interface UserRepository {
    void save(String name);
}

final class ConsoleUserRepository implements UserRepository {
    @Override
    public void save(String name) {
        System.out.println("DB INSERT name=" + name);
    }
}
Java

永続化の方法(DB、ファイル、メモリなど)が変わっても、
このインターフェースの実装を差し替えるだけです。

ユースケースの責任(“流れ”の調整役)

final class UserService {

    private final NameNormalizer normalizer;
    private final NameValidator validator;
    private final UserRepository repository;

    UserService(NameNormalizer normalizer,
                NameValidator validator,
                UserRepository repository) {
        this.normalizer = normalizer;
        this.validator = validator;
        this.repository = repository;
    }

    String create(String rawName) {
        String name = normalizer.normalize(rawName);      // 整形の責任を委譲
        if (!validator.isValid(name)) {                  // 検証の責任を委譲
            throw new IllegalArgumentException("invalid name");
        }
        repository.save(name);                           // 永続化の責任を委譲

        return formatForDisplay(name);                   // 表示用フォーマット
    }

    String formatForDisplay(String name) {
        return "User(" + name + ")";
    }
}
Java

UserService の責任は「ユースケースとしての“流れ”を組み立てること」です。

整形そのもののロジックは Normalizer に任せ
検証ルールそのものは Validator に任せ
保存の具体的な方法は Repository に任せる。

UserService はそれらを「どの順番で」「どう組み合わせるか」の責任だけを持ちます。

表示フォーマットも本気で分けたければ、
DisplayFormatter のような責務に切り出しても良いです。


SRP を満たしているかを判断する“質問”

単一責任かどうかをチェックするとき、次のように自分に質問してみてください。

「このクラス(またはメソッド)が変更される理由は、何種類あるか?」

1種類なら SRP 的にはOKです。
2種類以上なら、責任が混ざっている可能性が高いです。

例えば、先ほどの BadUserService は
「整形仕様の変更」「検証仕様の変更」「保存方法の変更」「表示仕様の変更」
という複数の理由で変更されます。
これは SRP を破っているサインです。


単一責任とテストのしやすさ(重要なポイント)

SRP を守ると、テストが驚くほど楽になります。

名前の整形だけをテストしたいなら SimpleNameNormalizer だけをテストすればよく、
DB 接続などの余計な要素はいりません。

検証ルールだけを変えたい・試したいときは DefaultNameValidator だけを対象にできます。

UserService のテストでは、
Normalizer/Validator/Repository をテスト用のフェイク実装で差し替えれば、
「ユースケースの流れが正しいか」を外部接続なしで検証できます。

SRP を守っていないと
「テストのために DB が必要」「テストのために Web が立ち上がってないとダメ」
といったしんどい世界になります。


メソッドレベルの SRP(長いメソッドをどう分けるか)

SRP はクラスだけでなく、メソッドにも当てはまります。

1つのメソッドに
「入力の整形」「検証」「ビジネスロジックの計算」「ログ」「外部呼び出し」
が全部入っていると、読むのも変更するのも苦痛です。

それを次のように段階ごとに分けていきます。

final class OrderService {

    int place(String rawPrice) {
        String normalized = normalize(rawPrice);
        int price = parsePrice(normalized);
        validatePrice(price);
        int discounted = applyDiscount(price);
        saveOrder(discounted);
        return discounted;
    }

    String normalize(String s) {
        return s == null ? "" : s.trim();
    }

    int parsePrice(String s) {
        return Integer.parseInt(s);
    }

    void validatePrice(int price) {
        if (price <= 0) throw new IllegalArgumentException("price");
    }

    int applyDiscount(int price) {
        return Math.max(0, price - 100);
    }

    void saveOrder(int price) {
        System.out.println("SAVE ORDER price=" + price);
    }
}
Java

place は「流れを組み立てる責任」
各メソッドは「そのステップの責任」を持ちます。

こうしてメソッド単位でも責任を分けると、
特定ステップの仕様変更やテストがしやすくなります。


SRP を実務で意識するときのコツ(まとめ)

単一責任の原則は「1 クラス 1 メソッド 1 つのやること」ではありません。
「そのクラス(あるいはメソッド)が変更される理由は 1 種類か?」
に注目することです。

仕様変更の要望を聞いたとき、
「この変更はどの責任の話か?」を考え、
その責任を担当しているクラスだけを変更できるのが理想です。

もし「この変更を入れるには、ここもあそこも全部触らないといけない」
となっているなら、SRP を満たしていないサインかもしれません。

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