責務分離とは
責務分離は「ひとつのクラスやメソッドが、ひとつの明確な役割(Responsibility)だけを担うように分ける」設計の基本です。入力の整形、検証、計算、保存、表示などの関心事を混ぜず、境界を切ります。混ざると変更の波及が大きくなり、テストが難しく、バグが潜みやすくなります。分離すれば、各責務が小さく理解しやすくなり、差し替え・再利用・テストが簡単になります。
なぜ分離するのか(重要)
責務が混ざったクラスは「神クラス化」しやすく、変更点が広範囲に及びます。たとえば、表示仕様の変更で保存処理が壊れる、といった偶発的な影響が起きます。責務分離は変更の局所化を生み、テスト対象が小さくなるため、動作を確信しやすくなります。さらに、分離された小さな責務はインターフェース化して組み替え可能になり、拡張に強い構造を作れます。
例題で見る「悪い設計」から「良い分離」へ
混ざった責務の悪い例
final class BadUserService {
String create(String rawName) {
// 入力整形
String name = rawName == null ? "" : rawName.trim().replaceAll("\\s+", " ");
// 検証
if (name.isBlank() || name.length() > 50) throw new IllegalArgumentException("name");
// 永続化
System.out.println("DB: insert name=" + name);
// 表示用フォーマット
return "User(" + name + ")";
}
}
Java整形・検証・保存・表示が1メソッドに詰め込まれています。表示仕様を変えるだけでも create を触る必要があり、保存の不具合を招きやすい構造です。
責務分離した良い例(役割を切る)
interface Normalizer { String apply(String s); }
final class NameNormalizer implements Normalizer {
@Override public String apply(String s) {
return s == null ? "" : s.trim().replaceAll("\\s+", " ");
}
}
interface Validator { boolean valid(String s); }
final class NameValidator implements Validator {
@Override public boolean valid(String s) {
return s != null && !s.isBlank() && s.length() <= 50;
}
}
interface UserRepository { void save(String name); }
final class ConsoleUserRepository implements UserRepository {
@Override public void save(String name) { System.out.println("DB: insert name=" + name); }
}
final class UserService {
private final Normalizer normalizer;
private final Validator validator;
private final UserRepository repository;
UserService(Normalizer n, Validator v, UserRepository r) {
this.normalizer = n; this.validator = v; this.repository = r;
}
String create(String rawName) {
String name = normalizer.apply(rawName); // 整形
if (!validator.valid(name)) throw new IllegalArgumentException("name"); // 検証
repository.save(name); // 保存
return "User(" + name + ")"; // 表示(必要なら別責務へ)
}
}
Java整形、検証、保存をそれぞれの責務に分離しました。いずれかの仕様変更が他へ波及しにくく、テストも各責務単位で可能になります。
分離を支える道具立て(深掘り)
インターフェースで役割を明確化する
責務はインターフェースで「契約」を表し、実装は交換可能にします。呼び出し側は契約に依存し、具体は注入で差し替えます。これにより、テストではフェイク実装を渡すだけで検証できます。
final class FakeRepo implements UserRepository {
@Override public void save(String name) { /* in-memory */ }
}
Javaコンポジション(委譲)で組み合わせる
継承より委譲が分離と相性が良いです。各責務を部品として“持ち”、仕事を委譲します。部品の差し替えで振る舞いを変えられ、結合が弱く保たれます。
層で関心を分ける
機能ごとに app(ユースケース)/domain(ルール・契約)/infra(外部接続)へ分けます。app は domain の抽象に依存、infra は抽象を実装。向きがそろうと、変更の局所化とテスト容易性が高まります。
メソッド内の責務分離(小さな単位の勘所)
大きなメソッドは、段階ごとに小さなメソッドへ切り出します。命名で意図を明確にし、引数と戻り値で境界を作ります。副作用(外部への出力や状態変更)は一箇所へ集約し、その他の処理は純粋関数(入力→出力)に寄せるとテストが容易になります。
final class OrderCalculator {
int calcTotal(String raw) {
String normalized = preprocess(raw); // 前処理
int base = parsePrice(normalized); // 解析
return applyDiscount(base); // 割引適用(副作用なし)
}
String preprocess(String s) { return s == null ? "" : s.trim(); }
int parsePrice(String s) { return Integer.parseInt(s); }
int applyDiscount(int price) { return Math.max(0, price - 100); }
}
Java責務が混じりやすい箇所と対策(重要)
UIロジックとドメインロジック、外部接続とビジネスルール、整形と検証は混ざりやすい組み合わせです。対策は「境界の明示」と「注入」。UIは入力を受け取ってユースケースへ渡すだけ、ユースケースはドメインの抽象へ依存、外部接続はインフラ層でアダプタとして実装。整形と検証は別責務に切り、仕様の変更に強くします。状態を持つ処理は骨格(抽象クラス)で流れを固定し、やり方の違いは役割(インターフェース)で差し替える併用も効果的です。
まとめと実践の指針
責務分離は「ひとつの役割をひとつの場所へ」。整形・検証・計算・保存・表示を混ぜず、インターフェースで契約化し、委譲で組み合わせ、層で関心を分ける。メソッドは段階ごとに切り出し、副作用は集約、純粋関数に寄せる。これだけで可読性が上がり、変更は局所化され、テストと拡張が楽になります。
