【Laravel】DDD (Domain-Driven Design)アーキテクチャでの開発を例を使って説明
今回はDDD (Domain-Driven Design)アーキテクチャでの開発の紹介です。
DDDとは?
Domain-Driven Design(ドメイン駆動設計)はビジネスの要件と複雑性を中心に、ソフトウェアの設計と開発を進める手法です。
つまりアーキテクチャの一つの手法です。
tree
手っ取り早く、ディレクトリとコードを見て理解した方が早いと思うので、まずはプロジェクト構造を見てみましょう。
今回は例としてドメイン直下にUserディレクトリを作成しました。
ドメイン層に合わせて、AAAやBBBのようにディレクトリを作成していきます。
app
├── Domains
│ ├── User
│ │ ├── DTO
│ │ ├── Entities
│ │ ├── Services
│ │ └── Repositories
│ ├── AAA
│ │ ├── DTO
│ │ ├── Entities
│ │ ├── Services
│ │ └── Repositories
│ ├── BBB
│ │ ├── DTO
│ │ ├── Entities
│ │ ├── Services
│ │ └── Repositories
├── Exceptions
├── Http
│ ├── Controllers
│ ├── Middleware
│ ├── Requests
│ └── Kernel.php
├── Infrastructures
│ ├── Repositories
├── UseCases
└── Models
スポンサードサーチ
Domain
ドメインロジックとビジネスルールをカプセル化し、アプリケーションのコア部分を管理します。
ドメインの中に、以下の構造になっています。
- DTO:データ転送オブジェクトを格納します。
- Entities:エンティティはビジネスルールを持ち、データの永続化と関連するロジックを担当します。
- Services:エンティティの操作やビジネスロジックの実装を行います。
- Repositories:データベースや他の永続化ストレージとエンティティの間のやり取りを抽象化します。
DTO
データ転送オブジェクトとして使用されます。
コンストラクタでユーザー情報を格納するためのオブジェクトを生成しています。
オブジェクトを生成するにはnewでインスタンスを生成して、引数に$id, $name, $emailを指定することで、UserDTOクラスでデータを受け取ります。
<?php
namespace App\Domains\User\DTO;
/**
* UserDTOクラスは、ユーザー情報を転送するためのデータ構造です。
*/
class UserDTO
{
public $id;
public $name;
public $email;
/**
* UserDTOのコンストラクタ
*
* @param int $id ユーザーID
* @param string $name ユーザー名
* @param string $email ユーザーのメールアドレス
*/
public function __construct($id, $name, $email)
{
$this->id = $id;
$this->name = $name;
$this->email = $email;
}
}
Entity
エンティティは、ビジネスロジックやルールを持ち、ドメイン層で主に使用されます。
エンティティ (Entity) は、ドメイン駆動設計 (DDD: Domain-Driven Design) における基本的な概念の一つで、システム内で識別可能なオブジェクトを表します。
エンティティは一意の識別子(ID)を持ち、その属性や振る舞いを定義します。
<?php
namespace App\Domains\User\Entities;
/**
* Userエンティティは、ユーザーのビジネスロジックを表します。
*/
class User
{
private $id;
private $name;
private $email;
/**
* Userのコンストラクタ
*
* @param int $id ユーザーID
* @param string $name ユーザー名
* @param string $email ユーザーのメールアドレス
*/
public function __construct($id, $name, $email)
{
$this->id = $id;
$this->name = $name;
$this->email = $email;
}
/**
* ユーザーIDを取得するメソッド
*
* @return int ユーザーID
*/
public function getId()
{
return $this->id;
}
/**
* ユーザー名を取得するメソッド
*
* @return string ユーザー名
*/
public function getName()
{
return $this->name;
}
/**
* ユーザーのメールアドレスを取得するメソッド
*
* @return string ユーザーのメールアドレス
*/
public function getEmail()
{
return $this->email;
}
}
Service
Serviceでは、ビジネスロジックを記述します。
コンストラクタで、UserRepositoryInterface型の引数$userRepository を受け取ります。
これにより、UserService クラスは、UserRepositoryInterface を実装した任意のクラスのインスタンスを受け取ることができます。
getUserByIdメソッドは、与えられたユーザーIDに基づいてユーザー情報を取得するためのメソッドです。
UserRepositoryInterfaceを通じてユーザー情報をデータベースから取得します。
<?php
namespace App\Domains\User\Services;
use App\Domains\User\Repositories\UserRepositoryInterface;
/**
* UserServiceは、ユーザーに関するビジネスロジックを実行します。
*/
class UserService
{
private $userRepository;
/**
* UserServiceのコンストラクタ
*
* @param UserRepositoryInterface $userRepository ユーザーリポジトリ
*/
public function __construct(UserRepositoryInterface $userRepository)
{
$this->userRepository = $userRepository;
}
/**
* ユーザーIDでユーザーを取得するメソッド
*
* @param int $id ユーザーID
* @return User|null ユーザーエンティティ
*/
public function getUserById($id)
{
return $this->userRepository->findById($id);
}
}
Repository Interface
Repository Interfaceはリポジトリのインターフェースを定義し、抽象化します。
リポジトリクラスにはinterfaceを使用します。
<?php
namespace App\Domains\User\Repositories;
/**
* UserRepositoryInterfaceは、リポジトリのインターフェースを定義します。
*/
interface UserRepositoryInterface
{
/**
* ユーザーIDでユーザーを検索するメソッド
*
* @param int $id ユーザーID
* @return User|null ユーザーエンティティ
*/
public function findById($id);
}
Domain層をRepositorieはInfrastructure層でImplementationで実行します。
app
├── Domains
│ ├── User
│ │ ├── DTO
│ │ ├── Entities
│ │ ├── Services
│ │ │ └── UserService.php
│ │ └── Repositories
│ │ └── UserRepositoryInterface.php
├── Exceptions
├── Http
│ ├── Controllers
│ │ └── UserController.php
│ ├── Middleware
│ ├── Requests
│ └── Kernel.php
├── Infrastructures
│ ├── Repositories
│ │ └── UserRepository.php
├── UseCases
└── Models
Controller
リクエストを処理し、レスポンスを返します。
showメソッド内でリクエストを受け取り、UsecaseのGetUserUseCaseクラスのexecuteを呼び出しています。
<?php
namespace App\Http\Controllers;
use App\Domains\User\Services\UserService;
use App\Http\Requests\UserRequest;
use App\UseCases\GetUserUseCase;
/**
* UserControllerは、ユーザーのリクエストを処理し、レスポンスを返します。
*/
class UserController extends Controller
{
private $getUserUseCase;
/**
* UserControllerのコンストラクタ
*
* @param GetUserUseCase $getUserUseCase ユースケース
*/
public function __construct(GetUserUseCase $getUserUseCase)
{
$this->getUserUseCase = $getUserUseCase;
}
/**
* ユーザーを表示するメソッド
*
* @param UserRequest $request ユーザーリクエスト
* @param int $id ユーザーID
* @return \Illuminate\Http\JsonResponse JSONレスポンス
*/
public function show(UserRequest $request, $id)
{
$userDTO = $this->getUserUseCase->execute($id);
return response()->json($userDTO);
}
}
Request
リクエストのバリデーションを行います。
- authorizeメソッド:リクエストの認可をします。
- rulesメソッド:リクエストのバリデーションルールを定義する。
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
/**
* UserRequestは、ユーザーリクエストのバリデーションを行います。
*/
class UserRequest extends FormRequest
{
/**
* リクエストが許可されているかを判断するメソッド
*
* @return bool
*/
public function authorize()
{
return true; // 認可ロジックをここに記述する
}
/**
* リクエストのバリデーションルールを定義するメソッド
*
* @return array
*/
public function rules()
{
return [
'id' => 'required|integer|exists:users,id'
];
}
}
UseCase
ユースケースの実行ロジックを定義し、サービスからDTOを返します。
executeメソッドでユーザーIDを受け取り、UserServiceクラスのgetUserByIdメソッドを呼び出し、UserDTOを生成します。
<?php
namespace App\UseCases;
use App\Domains\User\Services\UserService;
use App\Domains\User\DTO\UserDTO;
/**
* GetUserUseCaseは、ユーザー情報を取得するユースケースを実行します。
*/
class GetUserUseCase
{
private $userService;
/**
* GetUserUseCaseのコンストラクタ
*
* @param UserService $userService ユーザーサービス
*/
public function __construct(UserService $userService)
{
$this->userService = $userService;
}
/**
* ユーザーIDでユーザーを取得し、UserDTOを返すメソッド
*
* @param int $id ユーザーID
* @return UserDTO ユーザーDTO
*/
public function execute($id)
{
$user = $this->userService->getUserById($id);
return new UserDTO($user->id, $user->name, $user->email);
}
}
Infrastructures
Infrastructuresでは、Repository ImplementationのUserRepositoryInterfaceリポジトリの具体的な実装です。
EloquentUserRepositoryクラスは、UserRepositoryInterface インターフェースを実装し、Eloquent ORMを使用してデータベース操作を行います。
Implementationクラスは、データベースとのやり取りを抽象化し、ビジネスロジックからデータアクセスの詳細を隠す役割を果たします。
<?php
namespace App\Infrastructures\Repositories;
use App\Domains\User\Repositories\UserRepositoryInterface;
use App\Models\User;
/**
* UserRepositoryは、Eloquent ORMを使用してデータベース操作を実装します。
*/
class UserRepository implements UserRepositoryInterface
{
/**
* ユーザーIDでユーザーを検索するメソッド
*
* @param int $id ユーザーID
* @return User|null ユーザーエンティティ
*/
public function findById($id)
{
return User::find($id);
}
}
interface側にも記載しましたが、domain層のinterfasceはInfrastructure層でImplementationします。
app
├── Domains
│ ├── User
│ │ ├── DTO
│ │ ├── Entities
│ │ ├── Services
│ │ │ └── UserService.php
│ │ └── Repositories
│ │ └── UserRepositoryInterface.php
├── Exceptions
├── Http
│ ├── Controllers
│ │ └── UserController.php
│ ├── Middleware
│ ├── Requests
│ └── Kernel.php
├── Infrastructures
│ ├── Repositories
│ │ └── UserRepository.php
├── UseCases
└── Models
スポンサードサーチ
Model
Eloquent ORMを使用してデータベースとやり取りします。
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
// Userモデルは、Eloquent ORMを使用してデータベースとやり取りします。
class User extends Model
{
protected $fillable = ['name', 'email'];
}
ルート設定
routes/web.php または routes/api.phpにUserControllerクラスのshowメソッドを実行できる記述を行います。
use App\Http\Controllers\UserController;
Route::get('/user/{id}', [UserController::class, 'show']);
スポンサードサーチ
実装のまとめ
app
├── Domains
│ ├── User
│ │ ├── DTO
│ │ │ └── UserDTO.php
│ │ ├── Entities
│ │ │ └── User.php
│ │ ├── Services
│ │ │ └── UserService.php
│ │ └── Repositories
│ │ └── UserRepositoryInterface.php
├── Exceptions
├── Http
│ ├── Controllers
│ │ └── UserController.php
│ ├── Middleware
│ ├── Requests
│ │ └── UserRequest.php
│ └── Kernel.php
├── Infrastructures
│ ├── Repositories
│ │ └── UserRepository.php
├── UseCases
└── Models
└── User.php
実行される順番
- ルート設定: /user/{id} にリクエストが送られると、UserController の show メソッドが呼び出される。
- コントローラのメソッド: UserController の show メソッドが UserService の getUserById メソッドを呼び出す。
- サービスのメソッド: UserService の getUserById メソッドが UserRepository の findById メソッドを呼び出す。
- リポジトリのメソッド: UserRepository の findById メソッドがデータベースからユーザー情報を取得する。
- レスポンス: 取得したユーザー情報が UserService、UserController を経由して JSON レスポンスとしてクライアントに返される。
ドメイン駆動設計 (DDD) のまとめ
ドメイン駆動設計 (DDD: Domain-Driven Design) は、ソフトウェア開発の手法であり、複雑なソフトウェアシステムの設計と実装において、ドメイン(問題領域)のモデルを中心に据えることを目的としています。
今回の例は上記コード一覧になりますが、その他として使用してない仕様も含め、DDDについてまとめているので、さわりだけでも理解してDDDを取り入れていきましょう
- ドメインモデル
ドメインモデルは、ビジネスドメイン(問題領域)を表現するオブジェクトモデルです。DDDでは、このモデルを中心にソフトウェアの設計が行われます。 - エンティティ (Entity)
エンティティは、DDDにおける基本的な概念の一つで、一意の識別子を持つオブジェクトを表します。エンティティは、その属性や振る舞いを持ち、システム内で一意に識別されます。 - 値オブジェクト (Value Object)
値オブジェクトは、識別子を持たないオブジェクトであり、属性の集合として扱われます。DDDでは、値オブジェクトは不変であり、同じ属性を持つオブジェクトは同一とみなされます。 - アグリゲート (Aggregate)
アグリゲートは、関連するエンティティと値オブジェクトの集合であり、DDDにおいて一貫性のある変更を保証するための単位です。アグリゲートにはルートエンティティが存在し、外部からのアクセスはルートエンティティを通じて行われます。 - リポジトリ (Repository)
リポジトリは、エンティティやアグリゲートを永続化するためのインターフェースを提供します。DDDでは、リポジトリを使用してデータアクセスの詳細を隠蔽し、ドメインロジックとデータアクセスロジックを分離します。 - サービス (Service)
サービスは、エンティティや値オブジェクトが持つべきでないビジネスロジックをカプセル化します。DDDにおけるサービスは、ドメインサービスとアプリケーションサービスに分類されます。 - ファクトリ (Factory)
ファクトリは、エンティティや値オブジェクトの生成をカプセル化します。DDDでは、ファクトリを使用することでオブジェクトの生成に関する複雑なロジックを隠蔽し、クライアントコードを簡潔に保ちます。 - ドメインイベント (Domain Event)
ドメインイベントは、ドメイン内で重要な出来事を表します。DDDでは、ドメインイベントを使用してシステムの状態変化を他のコンポーネントに通知し、疎結合なアーキテクチャを実現します。
DDDのレイヤードアーキテクチャ
DDDは、システムを複数のレイヤーに分割するレイヤードアーキテクチャを採用することが一般的です。典型的なレイヤー構成は以下の通りです。
- プレゼンテーション層: ユーザーインターフェースやAPIエンドポイントを提供します。
- アプリケーション層: ユースケースを実行し、ドメインロジックを調整します。
- ドメイン層: エンティティ、値オブジェクト、アグリゲート、ドメインサービスなどのドメインロジックを含みます。
- インフラストラクチャ層: データベースアクセス、外部システムとの連携、永続化の実装などを担当します。
DDDの利点
- ビジネス価値の反映: DDDでは、ドメインモデルを通じてソフトウェアがビジネスのルールやプロセスを正確に反映します。
- 保守性の向上: DDDにより、ドメインロジックとインフラストラクチャロジックが分離されるため、システムの保守性が向上します。
- コミュニケーションの改善: DDDのユビキタス言語(共通の言語)を使用することで、ドメインエキスパートと開発者間のコミュニケーションが改善されます。
- 柔軟性と拡張性: DDDのレイヤードアーキテクチャやリポジトリパターンにより、システムが柔軟かつ拡張性のあるものになります。