単一責任の原則(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 + ")";
}
}
JavaUserService の責任は「ユースケースとしての“流れ”を組み立てること」です。
整形そのもののロジックは 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);
}
}
Javaplace は「流れを組み立てる責任」
各メソッドは「そのステップの責任」を持ちます。
こうしてメソッド単位でも責任を分けると、
特定ステップの仕様変更やテストがしやすくなります。
SRP を実務で意識するときのコツ(まとめ)
単一責任の原則は「1 クラス 1 メソッド 1 つのやること」ではありません。
「そのクラス(あるいはメソッド)が変更される理由は 1 種類か?」
に注目することです。
仕様変更の要望を聞いたとき、
「この変更はどの責任の話か?」を考え、
その責任を担当しているクラスだけを変更できるのが理想です。
もし「この変更を入れるには、ここもあそこも全部触らないといけない」
となっているなら、SRP を満たしていないサインかもしれません。

