5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Single Responsibility Principle(単一責務の原則)について考えた

5
Last updated at Posted at 2019-06-21

はじめに

この記事は、SOLID原則のまとめ の続きです。

今回は、SOLID原則のひとつである Single Responsibility Principle(単一責務の原則)について、定義の深掘りから実務での適用パターンまで、コード例を交えながら考えていきます。

Single Responsibility Principle(SRP)とは

SRP は 「クラスを変更する理由は1つでなければならない」 という原則です。

Robert C. Martin は後の著書 Clean Architecture で、この定義をさらに明確化しています。

「モジュールはたったひとつのアクターに対して責務を負うべきである」

ここでいう アクター(actor) とは、そのモジュールの変更を要求する利害関係者やユーザーのグループのことです。つまり SRP は単に「1つのことだけをする」という意味ではなく、「変更を要求する人(アクター)が1人だけになるように設計する」 ということを意味しています。

「責務」と「変更理由」の関係

Martin は「責務とは変更する理由である」と定義しています。さらに、以下のように言い換えています。

同じ理由で変わるものは集め、異なる理由で変わるものは分離せよ

これは 凝集度(cohesion)結合度(coupling) の考え方と密接に関連しています。

  • 凝集度を高める: 同じ関心事に属するものを1つのモジュールにまとめる
  • 結合度を低くする: 異なる関心事を持つモジュール間の依存を減らす

SRP に従った設計は、自然と高凝集・低結合な構造になります。

SRP に違反した例: God Object

SRP に違反しているクラスの典型例として God Object があります。God Object とは、あまりにも多くのことを知りすぎている・やりすぎているオブジェクトのことで、アンチパターンとして知られています。

参考: God object - Wikipedia

例1: Person クラスがメールの検証まで担当する

<?php

class Person {
    private $name;
    private $email;

    public function __construct(string $name, string $email){
        $this->name = $name;

        if($this->validateEmail($email)) {
            $this->email = $email;
        }
        else {
            throw new Exception('Invalid Email');
        }
    }

    private function validateEmail(string $email) {
        return filter_var($email, FILTER_VALIDATE_EMAIL);
    }

    public function greet() {
        echo("Hello");
    }
}

$person = new Person("test", "test@test.com");
$person->greet();

Person クラスが「人の管理」と「メールアドレスの検証」という2つの責務を持ってしまっています。メールアドレスの検証ルールが変わった場合にも Person クラスを修正する必要があり、変更理由が複数存在する状態です。

例2: Employee クラスが複数のアクターに責務を負う

実務でよく見かける、より分かりやすい違反例を見てみます。

<?php

class Employee {
    private $name;
    private $hourlyRate;
    private $hoursWorked;

    public function __construct(string $name, int $hourlyRate, int $hoursWorked) {
        $this->name = $name;
        $this->hourlyRate = $hourlyRate;
        $this->hoursWorked = $hoursWorked;
    }

    // 経理部門が使う: 給与計算
    public function calculatePay(): int {
        return $this->hourlyRate * $this->hoursWorked;
    }

    // 人事部門が使う: 勤務時間レポート
    public function reportHours(): string {
        return "{$this->name}: {$this->hoursWorked}時間";
    }

    // IT部門が使う: データベースへの保存
    public function save(): void {
        echo "DBに保存: {$this->name}\n";
    }
}

この Employee クラスは3つの異なるアクター(経理部門・人事部門・IT部門)に対して責務を負っています。

  • 給与計算のルールが変わる → 経理部門の要求で calculatePay を修正
  • レポートのフォーマットが変わる → 人事部門の要求で reportHours を修正
  • DBスキーマが変わる → IT部門の要求で save を修正

1つのクラスに3つの変更理由が存在しており、SRP に違反しています。

SRP に準拠した例

例1の改善: Value Object パターンで責務を分離

メールアドレスに関する責務を Email クラス(Value Object)に分離します。

<?php

class Person {
    private $name;
    private $email;

    public function __construct(string $name, Email $email){
        $this->name = $name;
        $this->email = $email;
    }

    public function greet() {
        echo("Hello");
    }
}

class Email {
    private $email;

    public function __construct(string $email){
        if($this->validateEmail($email)) {
            $this->email = $email;
        }
        else {
            throw new Exception('Invalid Email');
        }
    }

    private function validateEmail(string $email) {
        return filter_var($email, FILTER_VALIDATE_EMAIL);
    }
}

$email = new Email("test@test.com");
$person = new Person("test", $email);
$person->greet();

Person クラスは人に関する処理、Email クラスはメールアドレスに関する処理と、それぞれが単一の責務を持つようになりました。メールアドレスの検証ルールが変わっても Person クラスに影響はありません。

例2の改善: アクターごとにクラスを分離

<?php

class Employee {
    private $name;
    private $hourlyRate;
    private $hoursWorked;

    public function __construct(string $name, int $hourlyRate, int $hoursWorked) {
        $this->name = $name;
        $this->hourlyRate = $hourlyRate;
        $this->hoursWorked = $hoursWorked;
    }

    public function getName(): string { return $this->name; }
    public function getHourlyRate(): int { return $this->hourlyRate; }
    public function getHoursWorked(): int { return $this->hoursWorked; }
}

// 経理部門向け: 給与計算
class PayCalculator {
    public function calculatePay(Employee $employee): int {
        return $employee->getHourlyRate() * $employee->getHoursWorked();
    }
}

// 人事部門向け: 勤務時間レポート
class HourReporter {
    public function reportHours(Employee $employee): string {
        return "{$employee->getName()}: {$employee->getHoursWorked()}時間";
    }
}

// IT部門向け: データの永続化
class EmployeeRepository {
    public function save(Employee $employee): void {
        echo "DBに保存: {$employee->getName()}\n";
    }
}

各クラスが1つのアクターに対してのみ責務を負うようになりました。給与計算のルールが変わっても、影響を受けるのは PayCalculator だけです。

SRP を適用する際の注意点

分割しすぎに注意する

SRP を機械的に適用すると、非常に小さなクラスが大量に生まれ、コードの見通しが悪くなることがあります。責務の境界は「変更理由(アクター)」で判断し、実際に異なるタイミング・異なる理由で変更される可能性が低いものまで無理に分離する必要はありません。

SRP 違反のサイン

以下のような兆候が見られたら、SRP 違反を疑ってみてください。

  • クラスのコンストラクタに多くの依存が注入されている(Constructor Over-Injection)
  • クラス名に「And」や「Manager」「Service」など曖昧な名前がついている
  • 1つのクラスの変更が、無関係に見える機能のテストを壊す
  • クラスのメソッドが互いに関連性の薄いデータを扱っている

定期的な見直しが大切

プロジェクトが進行するにつれて、新機能の追加や要件の変更により、当初は単一責務だったクラスが複数の責務を持つようになることがあります。定期的にコードをレビューし、必要に応じてリファクタリングを行うことが重要です。

まとめ

SRP は「1つのクラスには1つの変更理由だけ」というシンプルな原則ですが、その本質は 「変更を要求するアクターごとにモジュールを分離する」 ことにあります。God Object のような多責務クラスを避け、高凝集・低結合な設計を心がけることで、変更に強く保守しやすいコードになります。

参考記事・データ

5
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?