🛫

クリーンアーキテクチャ基礎 #1 SRP

に公開

はじめに

『Clean Architecture 達人に学ぶソフトウェアの構造と設計』で学んだ内容をアウトプットします。

OCP編

https://zenn.dev/th_code/articles/29725b15228029

SRP:単一責任原則

この原則の目的は 「機能修正や仕様変更に強い設計」を実現することです。
開発で大きなコストを生むのはプログラムを作成後に「正しく動くか?」の検証以上に「既存箇所へ副作用がないか?」の確認です。SRP を守れば 修正箇所と影響範囲が明確 になり、テスト・レビュー・デバッグ負荷を大幅に削減できます。

“モジュールを変更する理由はただ 1 つであるべき”
― Robert C. Martin

ここで言う「理由」とは アクター(利害関係者、画面、外部システムなど) を指します。
言い換えれば 「モジュールは 1 人のアクターに対して責務を果たすべき」 ということです。

クラスを分散すべきもの/まとめるべきもの

SRPの視点で「同時に変わらないもの」を分割し、「同時に変わるもの」をまとめます。

ユースケース

従業員クラスに3つのメソッドが同居している例からSRP 違反についてを見ていきます。

calculatePay()   # 経理部(CFO)
reportHours()    # 人事部(COO)
save()           # DB管理者(CTO)

経理部の計算仕様が変わると、人事やDB管理者にも影響が波及します。
ユースケースごとに変更理由が異なるため、クラスを分割するか Facade でカプセル化するのが定石です。
特に「画面 A」と「画面 B」という2つの変更理由が混在している場合も SRP違反の典型例です。

同時に変わらないものはひとつの箱に入れず、変わる理由ごとに箱を分ける ─ SRP の本質です。

集約すべきもの

責務とは「できること」ではなく、“取り扱うデータの一貫性を守ること” です。
例としてタイマー機能を実装しているクラスに start / stop / check が同居しても、いずれも「計測データの整合性を維持する」という 1 つの責務に収まるため SRP 違反ではありません。

以上を踏まえてMVCをベースにBad例と原則を適用させたコードを紹介します。

NG パターン — すべて Controller に詰め込む

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Mail;
use Illuminate\Http\Request;

class RegisterController extends Controller
{
    public function __invoke(Request $request)
    {
        // ① バリデーション責務
        $request->validate([
            'name'     => ['required', 'string', 'max:255'],
            'email'    => ['required', 'email', 'unique:users'],
            'password' => ['required', 'min:8'],
        ]);

        // ② 永続化責務
        $user = User::create([
            'name'     => $request->name,
            'email'    => $request->email,
            'password' => Hash::make($request->password),
        ]);

        // ③ 通知責務
        Mail::to($user->email)->send(new \App\Mail\Welcome($user));

        // ④ レスポンス生成責務
        return response()->json($user, 201);
    }
}
変更理由 混在箇所 影響
バリデーション仕様変更 UI の変更で Controller の修正が発生
DB 保存方式変更 インフラ変更が UI 層へ波及
通知方法変更 ビジネス外の変更で Controller の修正が発生
レスポンス形式変更 API 仕様変更で Controller の修正が発生

4 つの責務が 1 クラスに混在し、SRP 違反。テスト・拡張・レビューのコストが爆発します。


OK パターン — SRP 準拠

ディレクトリ構成

app
├── Domains
│   └── User
│       ├── Contracts/UserRepository.php
│       └── Repositories/EloquentUserRepository.php
├── UseCases/Auth/RegisterUser.php
├── Services/Notifications/SendWelcomeMail.php
└── Http
    ├── Controllers/Auth/RegisterController.php
    └── Requests/RegisterRequest.php

Repository — データ永続化責務

interface UserRepository
{
    public function create(array $data): User;
    public function existsByEmail(string $email): bool;
}

use App\Models\User
class EloquentUserRepository implements UserRepository
{
    public function create(array $data): User
    {
        return User::create($data);
    }

    public function existsByEmail(string $email): bool
    {
        return User::whereEmail($email)->exists();
    }
}

UseCase — ビジネスロジック責務

class RegisterUser
{
    public function __construct(private UserRepository $users) {}

    public function execute(array $dto): User
    {
        if ($this->users->existsByEmail($dto['email'])) {
            throw new DomainException('duplicate email');
        }
        $dto['password'] = Hash::make($dto['password']);
        return $this->users->create($dto);
    }
}

Notification Service — 通知責務

class SendWelcomeMail
{
    public function __invoke(User $user): void
    {
        Mail::to($user->email)->queue(new Welcome($user));
    }
}

Controller — UI 入口責務

class RegisterController extends Controller
{
    public function __construct(
        private RegisterUser $register,
        private SendWelcomeMail $notify
    ) {}

    public function __invoke(RegisterRequest $request)
    {
        $user = $this->register->execute($request->validated());
        ($this->notify)($user);
        return response()->json($user, 201);
    }
}

SRP 適用後の影響範囲

変更シナリオ 触るファイル 影響範囲
ハッシュ方式変更 RegisterUser ビジネス層のみ
外部 API 保存へ変更 Repository 実装のみ 他層ノータッチ
通知を Slack に変更 SendWelcomeMail 通知層のみ
入力項目追加 RegisterRequest UI 層のみ

まとめ

  • SRP の狙いは「1 つの変更理由=1 モジュール」を徹底し、保守・テスト・拡張のコストを局所化すること。
  • 分割すべきもの: アクターやユースケース、画面など変更要因が異なる振る舞い。
  • 集約すべきもの: 常に同時に変わるデータと操作(データの一貫性を守るため)。

Discussion