Java 逆引き集 | クラス設計の基本(単一責任) — 保守性を高める

Java Java
スポンサーリンク

クラス設計の基本(単一責任) — 保守性を高める

「クラスは、変わる理由を一つだけ持つ。」単一責任の原則は、この一文に尽きます。役割を一つに絞ると、変更の影響が限定され、読みやすくテストもしやすいコードになります。初心者でも踏み外しにくい、現場でそのまま使える考え方と書き方をまとめます。


単一責任の考え方

  • 目的の定義:
    • クラス: 1つの関心事(責務)だけを扱う。
    • メソッド: 1つのことを、最後までやり切る(副作用は最小に)。
  • 変化の理由: そのクラスが「なぜ変更されるのか」を1つに限定する。仕様追加やUI変更、保存先変更など複数理由が混在したら分割のサイン。

ありがちな「責務過多」サイン

  • 肥大メソッド: 100行超え、条件分岐だらけ、コメントで「ここから保存」「ここから整形」。
  • ミックス責務: 1クラスに「計算+永続化+表示整形」が同居。
  • テスト困難: 1つのメソッドをテストするためにDBやネットが必要。
  • 再利用不可: 一部だけ使いたいのに、巨大クラスごと依存が必要。

悪い例から良い例へ(リファクタリング)

Before: 1クラスに全部入り

class ReportService {
    public void generateAndSave(String id) {
        String data = fetch(id);             // 取得(IO)
        String csv = formatCsv(data);        // 整形(表示)
        saveToFile(csv);                     // 保存(IO)
    }
    private String fetch(String id) { /* ... */ }
    private String formatCsv(String s) { /* ... */ }
    private void saveToFile(String csv) { /* ... */ }
}
Java
  • 問題: 取得仕様が変わっても、整形仕様が変わっても、保存先が変わっても同じクラスが変更対象。変わる理由が複数。

After: 責務ごとに分割+組み立て

interface Fetcher { String fetch(String id); }
interface Formatter { String format(String s); }
interface Saver { void save(String content); }

class ReportUseCase {
    private final Fetcher fetcher;
    private final Formatter formatter;
    private final Saver saver;

    ReportUseCase(Fetcher f, Formatter fm, Saver s) {
        this.fetcher = f; this.formatter = fm; this.saver = s;
    }

    public void execute(String id) {
        String raw = fetcher.fetch(id);
        String out = formatter.format(raw);
        saver.save(out);
    }
}
Java
  • 効果: 取得・整形・保存の変更理由が分離。差し替え容易、テストはモックで個別に可能。

小さく分けるための境界線

  • 入出力(IO)と純ロジックの分離: IOは遅く不安定。計算・整形と切り離す。
  • ドメインと表示の分離: ビジネスルール(計算)と画面用文字列(整形)を別クラスに。
  • 永続化の分離: データ保存(DB/ファイル)をリポジトリに委譲。
  • ユーティリティの抽出: 再利用可能な純関数はユーティリティへ。

例題で身につける

例題1: 価格計算と税率表示の分離

class PriceCalculator {
    public int withTax(int net, double taxRate) {
        return (int) Math.round(net * (1 + taxRate));
    }
}

class PriceFormatter {
    public String label(int amountYen) {
        return String.format("%,d円", amountYen);
    }
}

// 組み合わせる側(ユースケース)
class QuoteUseCase {
    private final PriceCalculator calc = new PriceCalculator();
    private final PriceFormatter fmt = new PriceFormatter();

    public String quote(int net, double taxRate) {
        int gross = calc.withTax(net, taxRate);
        return fmt.label(gross);
    }
}
Java
  • ポイント: 計算と表示を分けると、税率仕様変更でも表示ロジックは不変。

例題2: ユーザー検証と保存の分離

class UserValidator {
    public void validate(String name, String email) {
        if (name == null || name.isBlank()) throw new IllegalArgumentException("name");
        if (email == null || !email.contains("@")) throw new IllegalArgumentException("email");
    }
}

interface UserRepository { void save(String name, String email); }

class RegisterUserUseCase {
    private final UserValidator validator;
    private final UserRepository repo;
    RegisterUserUseCase(UserValidator v, UserRepository r) { this.validator = v; this.repo = r; }

    public void register(String name, String email) {
        validator.validate(name, email);
        repo.save(name, email);
    }
}
Java
  • ポイント: 検証と永続化を分離。保存先変更(DB→ファイル)でも検証は触らない。

例題3: ログ整形と出力の分離

class LogFormatter {
    public String format(String level, String msg) {
        return String.format("[%s] %s", level, msg);
    }
}
interface LogSink { void write(String line); }

class Logger {
    private final LogFormatter fmt;
    private final LogSink sink;
    Logger(LogFormatter f, LogSink s) { this.fmt = f; this.sink = s; }
    public void info(String msg) { sink.write(fmt.format("INFO", msg)); }
}
Java
  • ポイント: 出力先変更(コンソール→ファイル→HTTP)でも整形は不変。

すぐ使えるテンプレート

  • ユースケース組み立て
class UseCase {
    private final A a; private final B b; private final C c;
    UseCase(A a, B b, C c) { this.a = a; this.b = b; this.c = c; }
    public Result run(Input in) {
        var x = a.step(in);
        var y = b.transform(x);
        return c.finish(y);
    }
}
Java
  • リポジトリ(永続化境界)
interface Repository<T> {
    void save(T t);
    T findById(String id);
}
Java
  • サービス分割指針
// 計算(純ロジック) → IOを持たない純メソッド
// 変換(DTO/整形)   → 表示用にマッピング
// IO(外部境界)     → DB/ファイル/HTTPを担当
Java

実務のコツ

  • 命名で責務を固定: 語尾を揃える(Calculator/Formatter/Repository/UseCase)。
  • 1ファイル=1責務が基本: クラスファイル内に“別責務のメソッド”を混ぜない。
  • テストで責務を検証: 純ロジックはユニットテスト、IOは統合テスト。モックで境界を確認。
  • 依存は内向きに: ユースケースがインターフェースに依存し、具体実装は後から注入。
  • 小さく始めて分割: まず動かし、変更理由が2つ見えたら分割タイミング。

チェックリスト(5分でセルフレビュー)

  • このクラス、変更理由はいくつある? 2つ以上なら分割候補。
  • IOと計算、混在してない? 混ざっていたら切り離す。
  • テストしやすい? 純ロジックがモックなしでテストできるか。
  • 名前は責務を表している? 曖昧なら役割を再定義。
  • 依存方向は適切? 上位(ユースケース)が下位の詳細に直接依存していないか。

まとめ

  • 単一責任=「変わる理由を一つに」。責務を分けるほど、変更の影響が局所化し保守が楽になる。
  • 境界の分離(計算・整形・IO)が最初の一歩。
  • インターフェースで依存を薄くし、差し替えやすくする。

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