1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Laravelにおける最低限のSRPとDIP

1
Posted at

はじめに

この記事は、設計原則の中でも特に「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」を軸にすれば、どんな構成でもぶれずに整理できると思います。

1
0
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
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?