単一責任の原則

SOLID の S - クラスやモジュールが変更される理由は 1 つだけであるべきという設計原則

SOLID設計原則

単一責任の原則とは

単一責任の原則 (Single Responsibility Principle, SRP) は、SOLID 原則の S にあたり、「クラスを変更する理由は 1 つだけであるべき」という設計原則である。Robert C. Martin が 1990 年代後半に、それ以前からあった凝集度・結合度の議論を 1 つの原則としてまとめたもので、著書「Agile Software Development, Principles, Patterns, and Practices」で広く知られるようになった。

重要なのは、「1 つのことだけをする」という意味ではないことだ。Martin は 2014 年の記事「The Single Responsibility Principle」で、「変更する理由」を突き詰めると「誰に対して責任を負うか」になると言い直し、これは人についての原則だと述べている。モジュールへの変更要求が、単一の業務機能とそれを代表する一群の人々からしか来ない状態に保つ、という読み方である。ユーザー管理と請求処理を 1 つのクラスに混在させると、経理部門の要件変更がユーザー管理に波及するリスクがある。変更の理由が異なるコードを分離することが本質だ。

よくある誤解

「1 クラス 1 メソッド」ではない

SRP を極端に解釈して、クラスにメソッドを 1 つしか持たせないのは過剰分割だ。SRP が求めるのは「変更理由の単一性」であり、メソッド数の制限ではない。UserRepositoryfindByIdfindByEmailsavedelete を持つのは SRP に反しない。これらはすべて「ユーザーの永続化」という同一の責務に属する。

凝集度との関係

SRP と凝集度 (Cohesion) は表裏一体だ。SRP に従うと自然に凝集度が高くなる。逆に、凝集度が低いクラス (互いに無関係なメソッドが混在) は SRP に違反している可能性が高い。

Martin 自身も「同じ理由で変わるものは集め、違う理由で変わるものは分ける」と言い換え、これは凝集度と結合度の別の定義にすぎないと説明している。系譜としても、結合度と凝集度は Larry Constantine が導入し Tom DeMarco や Meilir Page-Jones らが発展させた概念で、SRP はそれを 1 つの原則に束ね直したものだ。

違反の兆候

コードレビューで以下のパターンを見つけたら SRP 違反を疑う。

  • クラスが複数の理由で変更される (「ユーザー管理の仕様変更」と「レポート形式の変更」が同じクラスに影響する)
  • クラス名に「And」「Manager」「Handler」「Processor」が含まれる (UserAndBillingManager)
  • メソッドが互いに無関係なデータを操作している
  • テスト時に無関係なモックが大量に必要になる
  • クラスのコンストラクタに注入する依存が 5 つ以上ある

実務での適用

Martin が 2014 年の記事で使った Employee クラスの例が分かりやすい。給与計算の規則は CFO 配下、勤務時間レポートの書式は監査部門を管轄する COO 配下、データベースへの保存は CTO 配下だ。3 人の別々の責任者が、それぞれ独立に変更を要求してくる。

// ❌ SRP 違反: 3 人の要求元が 1 クラスに同居している
declare const db: { write(table: string, row: object): void };

class Employee {
  constructor(
    readonly hours: number,
    readonly hourlyRate: number,
  ) {}

  calculatePay(): number {
    // 変更要求は CFO 配下 (給与計算の規則) から来る
    return this.hours * this.hourlyRate;
  }

  reportHours(): string {
    // 変更要求は COO 配下 (監査レポートの書式) から来る
    return `${this.hours} h`;
  }

  save(): void {
    // 変更要求は CTO 配下 (永続化の方式) から来る
    db.write('employee', { hours: this.hours, rate: this.hourlyRate });
  }
}
// ✅ SRP 準拠: 要求元ごとにモジュールを分ける
declare const db: { write(table: string, row: object): void };

class Employee {
  constructor(
    readonly hours: number,
    readonly hourlyRate: number,
  ) {}
}

class PayCalculator {
  calculate(employee: Employee): number {
    return employee.hours * employee.hourlyRate;
  }
}

class HourReporter {
  report(employee: Employee): string {
    return `${employee.hours} h`;
  }
}

class EmployeeRepository {
  save(employee: Employee): void {
    db.write('employee', { hours: employee.hours, rate: employee.hourlyRate });
  }
}

分離していないと、CFO 配下の要求で calculatePay を直したときに reportHours を壊し、何も頼んでいない COO 側で障害が出る。自分が依頼した覚えのない箇所が壊れることは、依頼者の信頼を最も損なう。責務を分離しておけば、変更の影響範囲が要求元の組織と一致する。

SRP の適用レベル

SRP はクラスだけでなく、複数のレベルで適用できる。

レベルSRP の適用
関数1 つの処理だけを行うバリデーションと保存を分離
クラス1 つのアクターに対して責任を持つPayCalculator と HourReporter
モジュール1 つのドメイン領域を担当する認証モジュールと課金モジュール
マイクロサービス1 つのビジネス機能を提供する注文サービスと在庫サービス

マイクロサービスの境界設計は、SRP をサービスレベルに拡張したものと捉えられる。

過剰分割の罠

SRP を厳密に適用しすぎると、クラス数が爆発して逆に保守性が下がる。1 つの変更で 10 個のファイルを修正する状況は、分割が過剰な兆候だ。

判断基準は「実際に異なるタイミング・異なる理由で変更されるか」である。理論上は分離できても、実際には常に一緒に変更されるコードを無理に分割する必要はない。YAGNI 原則とのバランスを取る。

迷ったときは、想像上の責務ではなく変更履歴を見るとよい。過去のコミットで常に一緒に変更されてきたコードは同じ理由で変わっており、片方だけが何度も変更されているなら理由は別だ。分割すべき境界の証拠は、設計図よりリポジトリの履歴に残っている。

この記事は役に立ちましたか?

関連用語

関連する記事