クリーンアーキテクチャ基礎 #1 SRP
はじめに
『Clean Architecture 達人に学ぶソフトウェアの構造と設計』で学んだ内容をアウトプットします。
OCP編
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