LESSON 30分

ストーリー

高橋アーキテクト
SOLID原則の最初は S — Single Responsibility Principle だ
高橋アーキテクト
“1つのクラスには1つの理由でしか変更が起きないようにすべき”。これがロバート・C・マーティンが提唱したSRPの定義だ
あなた
変更の理由…ですか?
高橋アーキテクト
そう。“責任”とは”変更の理由”と読み替えてもいい。あるクラスに変更が必要になるシナリオが複数あるなら、そのクラスは責任を持ちすぎている

SRPとは

Single Responsibility Principle(単一責任の原則): クラスを変更する理由は、ただ1つであるべきである。

ここでの「責任」は「変更の理由」と同義です。あるクラスが複数の理由で変更されるなら、そのクラスは複数の責任を負っています。


SRP違反の例

// SRP違反:3つの変更理由がある
class Employee {
  private name: string;
  private hourlyRate: number;
  private hoursWorked: number;

  // 責任1: ビジネスロジック(給与計算ルールの変更時)
  calculatePay(): number {
    return this.hourlyRate * this.hoursWorked;
  }

  // 責任2: レポート(レポート形式の変更時)
  generateReport(): string {
    return `Employee Report\n` +
           `Name: ${this.name}\n` +
           `Pay: ${this.calculatePay()}`;
  }

  // 責任3: データ永続化(DB構造の変更時)
  save(): void {
    const sql = `INSERT INTO employees (name, rate, hours)
                 VALUES ('${this.name}', ${this.hourlyRate}, ${this.hoursWorked})`;
    database.execute(sql);
  }
}

このクラスは以下の3つの理由で変更される可能性があります:

  1. 給与計算ルールが変わったcalculatePay() を変更
  2. レポートの形式が変わったgenerateReport() を変更
  3. データベース構造が変わったsave() を変更

SRPを適用した設計

// 責任1: 従業員のビジネスロジック
class Employee {
  constructor(
    readonly name: string,
    readonly hourlyRate: number,
    readonly hoursWorked: number
  ) {}

  calculatePay(): number {
    return this.hourlyRate * this.hoursWorked;
  }
}

// 責任2: レポート生成
class EmployeeReportGenerator {
  generate(employee: Employee): string {
    return `Employee Report\n` +
           `Name: ${employee.name}\n` +
           `Pay: ${employee.calculatePay()}`;
  }
}

// 責任3: データ永続化
class EmployeeRepository {
  save(employee: Employee): void {
    // ORMやクエリビルダーを使用
    this.db.employees.create({
      name: employee.name,
      hourlyRate: employee.hourlyRate,
      hoursWorked: employee.hoursWorked,
    });
  }
}

実践的なSRPの判断基準

「この変更は誰が要求するか」テスト

高橋アーキテクトのアドバイス:

「SRPの判断に迷ったら、“この変更を要求するのは誰か?“と考えてみよう。経理部門、マーケティング部門、IT部門 — 異なるステークホルダーが変更を要求するなら、それは異なる責任だ」

現実的な分割のコツ

// 分割しすぎ(過剰設計)
class UserNameValidator { /* 名前だけ検証 */ }
class UserEmailValidator { /* メールだけ検証 */ }
class UserAgeValidator { /* 年齢だけ検証 */ }

// 適切な粒度
class UserValidator {
  validate(user: UserInput): ValidationResult {
    return ValidationResult.combine([
      this.validateName(user.name),
      this.validateEmail(user.email),
      this.validateAge(user.age),
    ]);
  }

  private validateName(name: string): ValidationResult { /* ... */ }
  private validateEmail(email: string): ValidationResult { /* ... */ }
  private validateAge(age: number): ValidationResult { /* ... */ }
}

SRPは「1クラス1メソッド」を意味しません。「ユーザー入力のバリデーション」という1つの責任の中に複数のメソッドがあるのは自然です。


SRP適用のよくある間違い

間違い正しい理解
1クラス1メソッドにすべき1つの変更理由に対して複数メソッドは OK
全てのメソッドを別クラスに関連する操作は同じクラスにまとめてよい
ユーティリティクラスは SRP 違反同じカテゴリの操作をまとめるのは OK

まとめ

ポイント内容
SRPの定義クラスを変更する理由はただ1つであるべき
責任 = 変更の理由誰が変更を要求するかで判断する
適切な粒度分割しすぎず、関連する操作はまとめる
効果変更の影響範囲が限定され、保守性が向上

チェックリスト

  • SRPの定義を自分の言葉で説明できる
  • 「責任 = 変更の理由」を理解した
  • SRP違反のコードを見つけて分割できる

次のステップへ

次は SOLID の O — オープン・クローズドの原則(OCP)を学びます。「変更に閉じ、拡張に開いている」という一見矛盾した概念を理解しましょう。


推定読了時間: 30分