クラス設計の基本(単一責任) — 保守性を高める
「クラスは、変わる理由を一つだけ持つ。」単一責任の原則は、この一文に尽きます。役割を一つに絞ると、変更の影響が限定され、読みやすくテストもしやすいコードになります。初心者でも踏み外しにくい、現場でそのまま使える考え方と書き方をまとめます。
単一責任の考え方
- 目的の定義:
- クラス: 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)が最初の一歩。
- インターフェースで依存を薄くし、差し替えやすくする。

