DDDとクリーンアーキテクチャの各層を理解しよう!責務を明確にして適切な設計を実現

1. はじめに

Laravel × DDD × クリーンアーキテクチャとは?

Laravelは、シンプルなMVC(Model-View-Controller)アーキテクチャを採用しており、直感的に開発ができるフレームワークです。しかし、アプリが成長するにつれ、次のような課題に直面することがあります。

  • Fat Controller / Fat Model 問題:コントローラーやモデルにロジックが集中し、可読性やテストのしやすさが低下
  • 変更に弱い設計:ビジネスロジックとインフラ層(データベース操作など)が密結合してしまい、変更が難しくなる
  • 再利用性の低下:アプリケーション全体の設計が一貫しないため、コードの再利用が難しくなる

こうした課題を解決するために注目されているのが、DDD(ドメイン駆動設計)クリーンアーキテクチャ です。

本記事の目的

本記事では、DDDとクリーンアーキテクチャの違い・共通点を整理し、Laravelにどのように適用できるのかを解説 します。

具体的には、以下のポイントを押さえながら進めていきます。
DDDとクリーンアーキテクチャの基本概念を理解する
Laravelの構造と、DDD/クリーンアーキテクチャの関係を整理する
実際にフォルダ構成を考え、コード例を交えながら実装フローを解説する

これを読むことで、「LaravelでDDD×クリーンアーキテクチャを適用するイメージ」 をつかめるようになります!✨

ではまず、DDDとクリーンアーキテクチャの違い・共通点 から見ていきましょう!

2. DDDとクリーンアーキテクチャの違い・共通点

DDD(ドメイン駆動設計)とは?

DDD(Domain-Driven Design、ドメイン駆動設計)は、ソフトウェアの設計において 「ビジネスロジックを中心に考える」 ことを重視するアプローチです。

DDDの主な概念

DDDでは、以下のような概念が登場します。

エンティティ(Entity)
一意の識別子(ID)を持ち、状態を持つオブジェクト。例:UserOrder など。

値オブジェクト(Value Object)
IDを持たず、属性が同じなら同一と見なされるオブジェクト。例:EmailMoney など。

ドメインサービス(Domain Service)
エンティティや値オブジェクトだけでは表現しにくいビジネスロジックを扱うクラス。

リポジトリ(Repository)
エンティティの永続化(データベースの保存・取得)を抽象化する。

ユースケース(Use Case)
ビジネスロジックの流れを定義し、アプリケーションの動作を統制する。


クリーンアーキテクチャとは?

クリーンアーキテクチャは、ソフトウェアの依存関係を整理し、保守性・テストしやすさを向上させるための設計原則 です。

クリーンアーキテクチャの基本構造は、「同心円状のレイヤー」 で表現されます。

🔵 ドメイン層(Entities) - アプリの核となるビジネスルール
🟢 ユースケース層(Use Cases) - アプリケーションの操作の流れを定義
🟡 インターフェース層(Interface Adapters) - コントローラーやリポジトリのインターフェース
🟠 インフラ層(Frameworks & Drivers) - データベースや外部APIなど

👉 原則:「内側の層は外側に依存してはいけない(依存性逆転の原則)」


DDDとクリーンアーキテクチャの関係

DDDとクリーンアーキテクチャは、相互に補完し合う関係です。

クリーンアーキテクチャDDDの対応概念
エンティティ(Entities)エンティティ / 値オブジェクト
ユースケース(Use Cases)アプリケーションサービス(Use Case)
インターフェース層(Interface Adapters)リポジトリ / ドメインサービス
インフラ層(Infrastructure)Eloquentモデル、API通信

👉 ポイント
DDDの「ドメイン層」は、クリーンアーキテクチャの「エンティティ(Entities)」と一致します。
また、「ユースケース層」は、DDDの「アプリケーションサービス」に対応します。

これらの概念を組み合わせることで、Laravelでも 「ビジネスロジックを適切に分離し、変更に強い設計」 を実現できます!


3. Laravelに適用するアーキテクチャ構成

Laravelのデフォルト構成(MVC)

Laravelの標準的なアーキテクチャは、MVC(Model-View-Controller)パターン に基づいています。
通常、以下のようなフォルダ構成になります。

app/
├── Http/Controllers/   # コントローラー(プレゼンテーション層)
├── Models/             # Eloquentモデル(ビジネスロジック+DB操作)
├── Providers/          # サービスプロバイダ
database/
├── migrations/         # マイグレーション
├── factories/          # データ生成用ファクトリ

なぜDDD・クリーンアーキテクチャを適用するのか?

Laravelのデフォルト構造では、Eloquentモデルにビジネスロジックが集中しやすく、Fat Model問題が発生しがち です。
また、ビジネスロジックとデータベース操作が密結合し、テストや保守が難しくなることがあります。

そこで、DDDとクリーンアーキテクチャの考え方を適用し、ビジネスロジックとインフラ層を分離する構成 に変更します。


DDD×クリーンアーキテクチャを適用したLaravelのフォルダ構成

以下のようにフォルダを分けることで、各責務を明確にし、保守性・可読性を向上させます。

app/
├── Domain/             # ドメイン層(エンティティ、値オブジェクト、ドメインサービス)
│   ├── Entities/      # エンティティ(例:User.php)
│   ├── ValueObjects/  # 値オブジェクト(例:Email.php)
│   ├── Services/      # ドメインサービス(例:UserDomainService.php)
│   └── Repositories/  # リポジトリインターフェース(例:UserRepositoryInterface.php)
│
├── Application/        # アプリケーション層(ユースケース)
│   ├── DTOs/          # データ転送オブジェクト(例:UserDTO.php)
│   ├── UseCases/      # ユースケース(例:CreateUserUseCase.php)
│
├── Infrastructure/     # インフラ層(DB・API・外部サービスとの連携)
│   ├── Persistence/   # DBリポジトリ実装(例:UserEloquentRepository.php)
│   ├── Services/      # API通信などの外部連携(例:PaymentService.php)
│
├── Http/              # プレゼンテーション層(リクエストの受付)
│   ├── Controllers/   # コントローラー(例:UserController.php)
│   ├── Requests/      # バリデーション(例:CreateUserRequest.php)
│   ├── Responses/     # レスポンス用クラス

各層の役割とLaravelの対応部分

役割Laravelでの対応部分
ドメイン層(Domain)ビジネスルールを定義(エンティティ・値オブジェクト)app/Domain/
アプリケーション層(Application)ユースケースの実装(アプリの操作フローを管理)app/Application/UseCases/
インフラ層(Infrastructure)DBやAPIとの連携を担当app/Infrastructure/Persistence/
プレゼンテーション層(Http)ユーザーからのリクエストを処理app/Http/Controllers/

ポイント

ビジネスロジックをEloquentモデルに書かず、ドメイン層で管理する
データベースやAPIの処理は、インフラ層(Infrastructure)に分離する
アプリケーション層でユースケースを管理し、責務を明確にする

4. 具体的な実装フロー

ここでは、DDD×クリーンアーキテクチャを適用したLaravelの実装方法 を、以下の流れで解説していきます。

  1. ドメイン層の実装(エンティティ・値オブジェクト・ドメインサービス)
  2. アプリケーション層の実装(ユースケース・DTO)
  3. インフラ層の実装(リポジトリ)
  4. プレゼンテーション層の実装(コントローラー)

4.1 ドメイン層の実装

(1) エンティティの作成

エンティティは、ドメインの主要なビジネスルールを持つオブジェクトです。
例えば、User エンティティを作成してみましょう。

📌 app/Domain/Entities/User.php

namespace App\Domain\Entities;

class User
{
    private int $id;
    private string $name;
    private string $email;

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

    public function getId(): int
    {
        return $this->id;
    }

    public function getName(): string
    {
        return $this->name;
    }

    public function getEmail(): string
    {
        return $this->email;
    }
}

👉 ポイント

  • エンティティはビジネスルールを表現し、データベース操作のロジックを持たせない。
  • getId()getEmail() などのメソッドを作成し、データの取得を適切に管理する。

(2) 値オブジェクトの作成

値オブジェクトは、IDを持たず、属性が同じなら同一とみなされるオブジェクト です。
例えば、Email 値オブジェクトを作成してみましょう。

📌 app/Domain/ValueObjects/Email.php

namespace App\Domain\ValueObjects;

class Email
{
    private string $email;

    public function __construct(string $email)
    {
        if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            throw new \InvalidArgumentException("Invalid email format.");
        }
        $this->email = $email;
    }

    public function getValue(): string
    {
        return $this->email;
    }
}

👉 ポイント

  • メールアドレスの形式チェックをコンストラクタで行い、不正な値を防ぐ
  • getValue() メソッドでメールアドレスの値を取得する。

(3) ドメインサービスの作成

ドメインサービスは、エンティティや値オブジェクトだけでは表現しにくいビジネスロジック を担当します。
例えば、「ユーザーのメールアドレスがすでに存在するかをチェックする」ドメインサービスを作成します。

📌 app/Domain/Services/UserDomainService.php

namespace App\Domain\Services;

use App\Domain\Repositories\UserRepositoryInterface;
use App\Domain\ValueObjects\Email;

class UserDomainService
{
    private UserRepositoryInterface $userRepository;

    public function __construct(UserRepositoryInterface $userRepository)
    {
        $this->userRepository = $userRepository;
    }

    public function isEmailAlreadyTaken(Email $email): bool
    {
        return $this->userRepository->findByEmail($email) !== null;
    }
}

👉 ポイント

  • UserRepositoryInterface を使ってデータを取得し、ドメインサービスはデータベースの詳細を知らない
  • 依存関係を注入(DI)することで、テストしやすくする。

4.2 アプリケーション層の実装

(1) DTO(データ転送オブジェクト)の作成

DTO(Data Transfer Object)は、データの受け渡しに使うオブジェクト です。
例えば、UserDTO を作成してみましょう。

📌 app/Application/DTOs/UserDTO.php

namespace App\Application\DTOs;

class UserDTO
{
    public string $name;
    public string $email;

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

👉 ポイント

  • UserDTO は、ユースケース層とプレゼンテーション層のデータのやり取り に使う。
  • 配列ではなく、型安全なオブジェクトとして扱える

(2) ユースケースの作成

ユースケースは、アプリケーションの動作フローを定義する 役割を持ちます。
例えば、「ユーザーを作成する」ユースケースを作成してみましょう。

📌 app/Application/UseCases/CreateUserUseCase.php

namespace App\Application\UseCases;

use App\Domain\Entities\User;
use App\Domain\Services\UserDomainService;
use App\Domain\Repositories\UserRepositoryInterface;
use App\Application\DTOs\UserDTO;

class CreateUserUseCase
{
    private UserRepositoryInterface $userRepository;
    private UserDomainService $userDomainService;

    public function __construct(
        UserRepositoryInterface $userRepository,
        UserDomainService $userDomainService
    ) {
        $this->userRepository = $userRepository;
        $this->userDomainService = $userDomainService;
    }

    public function execute(UserDTO $userDTO): User
    {
        // メールアドレスの重複チェック
        if ($this->userDomainService->isEmailAlreadyTaken(new \App\Domain\ValueObjects\Email($userDTO->email))) {
            throw new \Exception("This email is already taken.");
        }

        // ユーザー作成
        $user = new User(0, $userDTO->name, $userDTO->email);

        // 永続化
        $this->userRepository->save($user);

        return $user;
    }
}

👉 ポイント

  • ユースケース内でビジネスロジックを書かず、ドメイン層のサービスを利用する
  • 依存するリポジトリやサービスをコンストラクタで注入する。

4.3 インフラ層の実装(リポジトリ)

インフラ層は、データベースや外部サービスとの連携を担当する層 です。
ここでは、リポジトリパターンを使い、Eloquentを直接操作せずにデータアクセスを抽象化 します。


(1) リポジトリインターフェースの作成

まず、リポジトリのインターフェースを定義します。

📌 app/Domain/Repositories/UserRepositoryInterface.php

namespace App\Domain\Repositories;

use App\Domain\Entities\User;
use App\Domain\ValueObjects\Email;

interface UserRepositoryInterface
{
    public function findByEmail(Email $email): ?User;
    public function save(User $user): void;
}

👉 ポイント

  • findByEmail()Email 値オブジェクトを引数に取り、User エンティティを返す。
  • save() メソッドは User エンティティを保存するが、データベースの詳細は隠蔽 する。

(2) Eloquentを使ったリポジトリの実装

インフラ層で Eloquentを使ったリポジトリの実装 を作成します。

📌 app/Infrastructure/Persistence/UserEloquentRepository.php

namespace App\Infrastructure\Persistence;

use App\Domain\Repositories\UserRepositoryInterface;
use App\Domain\Entities\User;
use App\Domain\ValueObjects\Email;
use App\Models\User as EloquentUser;

class UserEloquentRepository implements UserRepositoryInterface
{
    public function findByEmail(Email $email): ?User
    {
        $user = EloquentUser::where('email', $email->getValue())->first();
        if (!$user) {
            return null;
        }

        return new User($user->id, $user->name, $user->email);
    }

    public function save(User $user): void
    {
        EloquentUser::updateOrCreate(
            ['id' => $user->getId()],
            ['name' => $user->getName(), 'email' => $user->getEmail()]
        );
    }
}

👉 ポイント

  • EloquentUser を直接操作せず、User エンティティとして扱う。
  • findByEmail() メソッドでは、エンティティを返すことでデータベースの詳細を隠蔽 する。

4.4 プレゼンテーション層の実装(コントローラー)

プレゼンテーション層では、リクエストを受け取り、ユースケースを実行し、レスポンスを返す役割 を持ちます。
ここでは、ユーザー作成API を実装します。


(1) リクエストバリデーションの作成

まず、リクエストのバリデーションを行う FormRequest クラスを作成します。

📌 app/Http/Requests/CreateUserRequest.php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class CreateUserRequest extends FormRequest
{
    public function rules(): array
    {
        return [
            'name' => 'required|string|max:255',
            'email' => 'required|email|unique:users,email',
        ];
    }
}

👉 ポイント

  • リクエストのバリデーションをコントローラーから分離 し、役割を明確にする。

(2) コントローラーの実装

次に、コントローラーで CreateUserUseCase を呼び出します。

📌 app/Http/Controllers/UserController.php

namespace App\Http\Controllers;

use App\Application\UseCases\CreateUserUseCase;
use App\Application\DTOs\UserDTO;
use App\Http\Requests\CreateUserRequest;
use Illuminate\Http\JsonResponse;

class UserController extends Controller
{
    private CreateUserUseCase $createUserUseCase;

    public function __construct(CreateUserUseCase $createUserUseCase)
    {
        $this->createUserUseCase = $createUserUseCase;
    }

    public function store(CreateUserRequest $request): JsonResponse
    {
        $userDTO = new UserDTO($request->name, $request->email);
        $user = $this->createUserUseCase->execute($userDTO);

        return response()->json([
            'id' => $user->getId(),
            'name' => $user->getName(),
            'email' => $user->getEmail(),
        ], 201);
    }
}

👉 ポイント

  • CreateUserUseCase を利用し、ビジネスロジックを コントローラーに書かない
  • ユースケースに UserDTO を渡し、データの受け渡しを明確にする。
  • JsonResponse でAPIレスポンスを返す。

(3) ルーティングの設定

最後に、ルートを設定してエンドポイントを有効にします。

📌 routes/api.php

use App\Http\Controllers\UserController;
use Illuminate\Support\Facades\Route;

Route::post('/users', [UserController::class, 'store']);

👉 ポイント

  • /api/usersPOST リクエストを送ると、ユーザーが作成される。

まとめ

✔️ この記事で学んだこと

✅ DDDとクリーンアーキテクチャの違いと共通点
✅ Laravelに適用するためのフォルダ構成
✅ 各層(ドメイン・アプリケーション・インフラ・プレゼンテーション)の実装方法

✔️ DDD×クリーンアーキテクチャを採用すべきケース

  • 大規模なアプリケーションで、変更や拡張が頻繁に発生する場合
  • ビジネスルールが複雑で、ドメイン知識を整理したい場合
  • テストのしやすさや保守性を向上させたい場合

✔️ LaravelでDDDを導入する際の注意点

  • 小規模なプロジェクトではオーバーエンジニアリングになりやすい
  • 開発チームがDDDの概念を理解していないと逆に複雑化する
  • メリットを活かすためには、適切な設計とルールを守ることが重要

🚀 次のステップ

この記事をもとに、実際にLaravelプロジェクトにDDD×クリーンアーキテクチャを導入 してみてください!

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です