はじめに
この記事は、設計原則の中でも特に「SRP(単一責務の原則)」と「DIP(依存関係の逆転原則)」に焦点を当てたものとなります。
どちらもSOLID原則の中核ですが、「実務でどう生かすのか?」となると、少し抽象的でわかりづらいものです。
他人のコードをPRする機会が増える中、Controllerが肥大化していたり、Serviceが何でも屋となっていたりするケースをよく見かけます。
要因はいろいろありますが、特に、「SRP」と「DIP」が曖昧に扱われていることが多いからなのかなと思っており、そこで、自分の中でそれぞれの原則を整理し、日常的によく利用するLaravelでどう設計すれば現場で効くのかを言語化してみました。
Laravel以外(例:NestJS、Spring Bootなど)でも通じる考え方だと思うので、ぜひ参考にしてみてください。
SRP(単一責務の原則)とは
クラスは、たった1つの理由でしか変更されてはならない
つまり、「そのクラスが何をしているか」ではなく、「なぜ変更されるか」を1つに絞ることがポイントです。
❌ 悪例
class UserController extends Controller
{
public function store(Request $request)
{
// バリデーション
$validated = $request->validate([
'email' => 'required|email',
'password' => 'required|min:8'
]);
// ドメインロジック
$user = User::create([...]);
// メール送信
Mail::to($user->email)->send(new WelcomeMail($user));
}
}
このControllerは、「バリデーションの責務」「ユーザー登録の責務」「メール送信の責務」をすべて持っています。
つまり「3つの理由で変更されうる」ということです。
これが、SRP違反です。
✅ 改善例:責務を分ける(Controller / UseCase / Service / Repository)
app/
├── Http/
│ ├── Controllers/
│ │ └── UserController.php
│ └── Requests/
│ └── StoreRequest.php
├── UseCases/
│ └── User/
│ └── StoreAction.php
├── Repositories/
│ ├── UserRepositoryInterface.php
│ └── UserRepository.php
└── Services/
├── MailServiceInterface.php
└── MailService.php
こんな感じで、ディレクトリは分けると良いです。
例としてコードを残しておきます。参考にしてください。
Controller(HTTP)
// app/Http/Controllers/UserController.php
class UserController extends Controller
{
public function store(StoreRequest $request, StoreAction $action)
{
$v = $request->validated();
$user = $action(
firstName: $v['first_name'],
lastName: $v['last_name'],
email: $v['email'],
password: $v['password'],
);
return response()->json($user);
}
}
※今回、FormRequestの実装は一旦飛ばします。
UseCase(Action ユースケースの手続き)
// app/UseCases/User/StoreAction.php
class StoreAction
{
public function __construct(
private UserRepositoryInterface $userRepository,
private MailServiceInterface $mailService,
) {}
public function __invoke(string $firstName, string $lastName, string $email, string $password): User {
$user = $this->userRepository->register(
firstName: $firstName,
lastName: $lastName,
email: $email,
password: $password,
);
$this->mailService->sendWelcome($user);
return $user;
}
}
Repository(永続化の責務)
// app/Repositories/UserRepositoryInterface.php
interface UserRepositoryInterface
{
public function register(string $firstName, string $lastName, string $email, string $password): User;
}
// app/Repositories/UserRepository.php
class UserRepository implements UserRepositoryInterface
{
public function register(string $firstName, string $lastName, string $email, string $password): User {
return User::create([
'first_name' => $firstName,
'last_name' => $lastName,
'email' => $email,
'password' => Hash::make($password),
]);
}
}
Service(外部I/Oの責務)
// app/Services/MailServiceInterface.php
interface MailServiceInterface
{
public function sendWelcome(User $user): void;
}
// app/Services/MailService.php
class MailService implements MailServiceInterface
{
public function sendWelcome(User $user): void
{
Mail::to($user->email)->send(new WelcomeMail($user));
}
}
役割
ディレクトリには、それぞれ役割があります。
細かく分けたり、分け方は別でありますが、一旦ここでは以下のように分けています。
| 層 | 役割 | 主な責務 | 変更理由 |
|---|---|---|---|
| Controller | HTTPの入り口 | リクエスト・レスポンスの制御 | ルーティングやI/O仕様変更 |
| UseCase(Action) | アプリケーションの手続き | 業務処理の順序やルール | ビジネスフローの変更 |
| Service | 外部連携 | 外部API、メール、ストレージ | 外部仕様変更 |
| Repository | 永続化の抽象化 | DB操作、検索ロジック | データソース変更 |
これで各クラスの責務の境界が明確になり、SRPが成立します。
SRPで登場した UserRepositoryInterface / MailServiceInterface が、この後のDIPでそのまま「抽象として依存すべき対象」になります。
DIP(依存関係逆転の原則)とは
高水準モジュールは低水準モジュールに依存してはならない。両者は抽象(インターフェイス)に依存すべきである。
まず何が高水準/低水準か?
- 高水準 = アプリの手続き(今回で言う Action)
- 低水準 = 実装詳細(Repositoryの実装, Serviceの実装 など)
通常、上位層(UseCase)が下位層(具体的なServiceやRepository)に依存します。
これだと上位が下位の変更に引きずられます。
そこで、「上位は具体実装を知らず、契約(Interface)だけを見る」 ようにすることで依存の向きを抽象側に反転させます。これが 「依存関係の逆転」 です。
❌ 悪例
class StoreAction
{
public function __construct(private MailService $mailService) {} // ← 実装に依存
public function __invoke(string $firstName, string $lastName, string $email, string $password): User { /* ... */ }
}
Actionが実装に依存しているため、差し替え・テスト時にAction側まで変更が波及する。
✅ 改善例:契約(Interface)を介して依存
class StoreAction
{
public function __construct(
private UserRepositoryInterface $userRepository, // ← 抽象
private MailServiceInterface $mailService, // ← 抽象
) {}
public function __invoke(
string $firstName, string $lastName, string $email, string $password
): User {
$user = $this->userRepository->register(
firstName: $firstName,
lastName: $lastName,
email: $email,
password: $password,
);
$this->mailService->sendWelcome($user);
return $user;
}
}
DIコンテナで 抽象 → 具象 を束ねる
// app/Providers/AppServiceProvider.php
public function register(): void
{
$this->app->bind(UserRepositoryInterface::class, UserRepository::class);
$this->app->bind(MailServiceInterface::class, MailService::class);
}
こうしておけば、将来「キャッシュ付きRepository」や「外部メールAPI版Service」に差し替えても、Actionは一切変更不要。
上位(Action) → 抽象 → 下位(実装)の矢印に揃う = 依存関係の逆転が成立します。
まとめ
SRPとDIPは、「変更に強い構造を保つための着眼点」 です。
LaravelのDIコンテナとフォルダ構成を活かせば、Controllerを薄く保ち、UseCaseでアプリの手続きを定義し、外部連携はServiceで切り離し、永続化はRepositoryに閉じ込める。
これだけで、シンプルかつ見通しの良い設計を自然に実現できています。(もっと責務を分離することもできます。)
「クリーンアーキテクチャ」と言うと、他にもOCPやLSPなど多くの原則がありますが、まずSRPでどこが変わるかを1点化し、DIPでどう結ぶかを制御するだけで、大部分の設計問題は自然と収束します。
LaravelのようにDIが強力なフレームワークでは、この2つを押さえるだけで「保守・変更・テストのしやすさ」が劇的に変わります。
ここからDDDやCQRS、イベント駆動などに拡張していく場合も、この「SRP × DIP」を軸にすれば、どんな構成でもぶれずに整理できると思います。