はじめに
私は現在web企業1年目の新人で自社開発のアプリのリファクタリングチームに所属しています。そこでは、ドメイン駆動設計の考えを用いてリファクタリングを行っています。私としては、学生時代に個人開発をやっていた時に設計に関して何も気にせず開発していました。チームにアサインされるにあたって、ドメイン駆動設計を勉強し、それと同時に自分が学生時代に設計なしにやっていたものは、ユースケース駆動設計っぽく開発していたことを知りました。
本記事では、ユースケース駆動設計とドメイン駆動設計について説明し、それぞれをLaravelで表現してみようと思います。
想定アプリケーション
設計をするにあたって、どのようなアプリケーションを作成するか決めます。CRUDの処理があるブログアプリを作成します。
※設計についてお話しするため、認証や管理機能など細かい機能は想定していません。
ユースケース駆動設計
ユースケース駆動設計の基盤は、ユースケースです。このユースケースから、アプリの詳細を決めていきます。
ユースケース駆動設計の流れ
-
ユースケースの定義
まず、システムの利用者や関係者(アクター)がシステムに対してどのような操作を行うかを、ユースケースとして定義します。ユースケースには、システムの機能やシナリオが具体的に記述され、どのようにシステムを使うかの視点から要件を明確にします。 -
ユースケースモデルの作成
複数のユースケースを洗い出した後、アクターとユースケースの関係を表した「ユースケース図」を作成します。これは、システムの全体像や各ユースケースの関係性を視覚化するためのものです。 -
ユースケース記述の詳細化
各ユースケースについて、システムがどのように振る舞うかの詳細な記述を行います。基本フローや代替フローなどのシナリオを記述し、操作の流れやシステムの応答が明確になるようにします。 -
ユースケースに基づく設計
ユースケース記述をもとに、設計クラス図やシーケンス図、コラボレーション図などの設計資料を作成します。これにより、ユースケースがどのようにシステム内部で実装されるかが具体化され、設計が進みます。 -
実装とテスト
ユースケースを単位とした設計が完了したら、実際に実装とテストを行います。ユースケースごとにテストケースが設定されているため、テストが明確で実施しやすいのが特徴です。
これがユースケース駆動設計の流れですが、本記事では簡略化してお話しします。
ブログアプリのユースケース駆動設計
ブログアプリを例にしてユースケース駆動設計を説明します。ブログアプリでは、ユーザーが記事を投稿、閲覧、編集、削除する基本的な機能が考えられます。これをユースケースとして設計に反映し、Laravelでの実装方法も合わせて見ていきます。
ユースケースの定義
まず、ブログアプリの代表的なユースケースを定義します。ここでは、以下の4つのユースケースを例にします。
-
記事の投稿
ユーザーが新しいブログ記事を作成し、保存します。 -
記事の閲覧
ユーザーがブログ記事の一覧を見たり、詳細を表示したりします。 -
記事の編集
ユーザーが既存のブログ記事を修正します。 -
記事の削除
ユーザーがブログ記事を削除します。
ユースケースモデルの作成
上記のユースケースを「ユースケース図」で表すのですが、簡略化して考えます。
- アクター: ユーザー
- ユースケース:
- 記事の投稿
- 記事の閲覧
- 記事の編集
- 記事の削除
このようなユースケースモデルをもとに、Laravelでの設計に落とし込みます。
Laravelでの設計と実装
モデルの作成
ブログ記事を扱うためのモデルを作成します。記事には「タイトル」「本文」「作成日」「更新日」といったフィールドがあるとします。
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
protected $fillable = ['title', 'content'];
}
コントローラの作成
記事の投稿、閲覧、編集、削除の機能を担うコントローラを作成します。
namespace App\Http\Controllers;
use App\Models\Post;
use Illuminate\Http\Request;
class PostController extends Controller
{
// 記事一覧表示(ユースケース: 記事の閲覧)
public function index()
{
$posts = Post::all();
return view('posts.index', compact('posts'));
}
// 記事の作成フォーム表示(ユースケース: 記事の投稿)
public function create()
{
return view('posts.create');
}
// 記事の保存処理(ユースケース: 記事の投稿)
public function store(Request $request)
{
Post::create($request->validate([
'title' => 'required|string|max:255',
'content' => 'required|string',
]));
return redirect()->route('posts.index');
}
// 記事の編集フォーム表示(ユースケース: 記事の編集)
public function edit(Post $post)
{
return view('posts.edit', compact('post'));
}
// 記事の更新処理(ユースケース: 記事の編集)
public function update(Request $request, Post $post)
{
$post->update($request->validate([
'title' => 'required|string|max:255',
'content' => 'required|string',
]));
return redirect()->route('posts.index');
}
// 記事の削除処理(ユースケース: 記事の削除)
public function destroy(Post $post)
{
$post->delete();
return redirect()->route('posts.index');
}
}
ルーティングの設定
ルートファイルで各ユースケースに対応するエンドポイントを設定します。
use App\Http\Controllers\PostController;
Route::get('/posts', [PostController::class, 'index'])->name('posts.index');
Route::get('/posts/create', [PostController::class, 'create'])->name('posts.create');
Route::post('/posts', [PostController::class, 'store'])->name('posts.store');
Route::get('/posts/{post}/edit', [PostController::class, 'edit'])->name('posts.edit');
Route::put('/posts/{post}', [PostController::class, 'update'])->name('posts.update');
Route::delete('/posts/{post}', [PostController::class, 'destroy'])->name('posts.destroy');
ビューの作成
Bladeテンプレートを使って、ユーザーインターフェースを作成します。今回はユースケースが4つあるので、4つのBladeファイルを作成します。
ユースケース駆動設計のまとめ
ユースケース駆動設計を使うと、ブログアプリの設計・実装が「ユーザーがどう使うか」に基づいて整理され、ユーザーに必要な機能を漏れなく実装することができます。Laravelでは、ルーティング、コントローラ、モデル、ビューの各部分をユースケースに基づいて構成することで、ユーザー視点に沿ったシンプルで使いやすいアプリを構築できます。
ドメイン駆動設計
ドメイン駆動設計の基盤は、ドメインです。ドメインから、アプリの詳細を決めていきます。
ドメイン駆動設計の流れ
-
ドメインの理解
ドメイン駆動設計の第一歩は、業務領域(ドメイン)の深い理解です。システムの背景となる業務プロセスやルール、課題を明確にし、開発者とドメイン専門家(ビジネスの知識を持つ人)が密接に協力して進めます。- ユビキタス言語の定義
ドメインに関連する用語の意味を統一し、開発者と専門家が共通の言語でコミュニケーションできるようにします。例: 「顧客」「注文」「在庫」など。 - 主要なドメインの特定
ドメイン全体を調査し、システムが扱う主要な領域を抽出します。
- ユビキタス言語の定義
-
境界付けられたコンテキスト(Bounded Context)の定義
ドメインを複数のコンテキストに分割し、それぞれの境界と責務を明確にします。- コンテキストマップの作成
コンテキスト間の関係やデータの流れを示す図を作成します。
例: 「注文管理」と「顧客管理」は別のコンテキストとする。
- コンテキストマップの作成
-
ドメインモデルの設計
ドメインのルールや業務プロセスを反映したモデルを作成します。- エンティティと値オブジェクトの定義
- エンティティ: ユニークなIDを持つオブジェクト(例: 顧客、注文)。
- 値オブジェクト: 属性によって同一性が定義されるオブジェクト(例: 住所、金額)。
- 集約の設計
複数のエンティティや値オブジェクトをまとめた単位を設計します(集約ルートが中心)。 - ドメインサービスの作成
ドメインロジックがエンティティや値オブジェクトに収まらない場合は、サービスクラスを利用。
- エンティティと値オブジェクトの定義
-
アーキテクチャの設計
ドメインモデルを中心としたアーキテクチャを設計します。- レイヤードアーキテクチャ
システムを「ドメイン層」「アプリケーション層」「インフラ層」「プレゼンテーション層」に分け、それぞれの役割を明確化します。 - 依存関係の整理
ドメイン層が他の層に依存しないように設計します。インターフェースと依存性注入を利用。
- レイヤードアーキテクチャ
-
実装
設計したドメインモデルやアーキテクチャに基づいて、コードを実装します。- エンティティや値オブジェクトの実装
ドメインルールを反映したクラスを作成します。 - リポジトリの作成
データベース操作の責務を担うリポジトリクラスを実装します。 - アプリケーションサービスの実装
ユースケースに基づく操作を実装し、ドメインモデルを活用します。
- エンティティや値オブジェクトの実装
-
テスト
ドメイン駆動設計の各コンポーネントに対してテストを行い、正確性を確認します。- ユニットテスト: ドメインロジックやエンティティの動作を確認します。
- 統合テスト: アプリケーション層やインフラ層が正しく連携しているかを確認します。
- エンドツーエンドテスト: システム全体の動作をテストします。
-
継続的なリファクタリング
ドメインモデルは、業務要件やユビキタス言語の進化に応じて変更されることがあります。これに合わせてコードや設計をリファクタリングし、業務に適応し続けるシステムを維持します。
これがドメイン駆動設計の流れですが、本記事では簡略化してお話しします。
ブログアプリのドメイン駆動設計
ブログアプリを例に、ドメイン駆動設計を「ドメインの理解」から「実装」まで一貫して説明します。以下のプロセスに従い、設計とLaravelでの実装例を示します。
ドメインの理解
ブログアプリの主要な機能を整理し、ユビキタス言語を定義します。これにより、関係者全員が共通の言葉で設計・実装を進められるようになります。
主要な機能
- 記事の投稿
- 記事の閲覧
- 記事の編集
- 記事の削除
ユビキタス言語の例
- 記事(Post): ユーザーが投稿するコンテンツ。タイトル、本文、作成日時などを持つ。
- ユーザー(User): 記事を投稿・編集する主体。
- コメント(Comment): 記事に対して投稿される意見や感想。
境界付けられたコンテキストの定義
ブログアプリを以下のコンテキストに分割します:
- 記事管理コンテキスト: 記事の作成、閲覧、編集、削除。
- ユーザー管理コンテキスト: ユーザーの登録、認証。
- コメント管理コンテキスト: コメントの投稿、削除。
ドメインモデルの設計
エンティティ
-
Post(記事)
- フィールド: id, title, content, createdAt, updatedAt, author
- 振る舞い: 記事の内容更新、削除。
-
User(ユーザー)
- フィールド: id, name, email, password
- 振る舞い: ユーザー認証、権限管理。
値オブジェクト
- Content(記事の本文)
- フィールド: text
- 振る舞い: 長さ制限やフォーマット検証。
アーキテクチャ設計
レイヤードアーキテクチャを採用し、責務を分離します。
- ドメイン層
- エンティティ、値オブジェクト、ドメインサービスを定義。
- アプリケーション層
- ユースケースに応じた操作(例: 記事の投稿、編集)。
- インフラ層
- データベースや外部システムとの連携を担当。
- プレゼンテーション層
- ユーザーインターフェースやAPIを提供。
Laravelでの実装
ドメイン層: モデルの設計
namespace App\Domain\Entities;
use DateTime;
class Post
{
private string $id;
private string $title;
private string $content;
private DateTime $createdAt;
private DateTime $updatedAt;
private string $authorId;
public function __construct(string $id, string $title, string $content, string $authorId)
{
$this->id = $id;
$this->title = $title;
$this->content = $content;
$this->createdAt = new DateTime();
$this->updatedAt = new DateTime();
$this->authorId = $authorId;
}
public function updateContent(string $content): void
{
$this->content = $content;
$this->updatedAt = new DateTime();
}
public function delete(): void
{
// 削除ロジック(必要なら状態遷移管理を追加)
}
}
アプリケーション層: サービスの設計
namespace App\Application\Services;
use App\Domain\Repositories\PostRepository;
class PostService
{
private PostRepository $postRepository;
public function __construct(PostRepository $postRepository)
{
$this->postRepository = $postRepository;
}
public function getAllPosts()
{
return $this->postRepository->getAll();
}
public function createPost(array $data)
{
$this->postRepository->create($data);
}
public function getPostById(int $id)
{
return $this->postRepository->findById($id);
}
public function updatePost(int $id, array $data)
{
$this->postRepository->update($id, $data);
}
public function deletePost(int $id)
{
$this->postRepository->delete($id);
}
}
インフラ層: リポジトリの実装
namespace App\Domain\Repositories;
use App\Models\Post;
class PostRepository
{
public function getAll()
{
return Post::all();
}
public function create(array $data)
{
return Post::create($data);
}
public function findById(int $id)
{
return Post::findOrFail($id);
}
public function update(int $id, array $data)
{
$post = $this->findById($id);
$post->update($data);
return $post;
}
public function delete(int $id)
{
$post = $this->findById($id);
$post->delete();
}
}
プレゼンテーション層: コントローラ
namespace App\Http\Controllers;
use App\Application\Services\PostService;
use App\Http\Requests\PostRequest;
use Illuminate\Http\Request;
class PostController extends Controller
{
private PostService $postService;
public function __construct(PostService $postService)
{
$this->postService = $postService;
}
// 記事一覧表示
public function index()
{
$posts = $this->postService->getAllPosts();
return view('posts.index', compact('posts'));
}
// 新規投稿フォーム表示
public function create()
{
return view('posts.create');
}
// 記事保存処理
public function store(PostRequest $request)
{
$this->postService->createPost($request->validated());
return redirect()->route('posts.index');
}
// 記事編集フォーム表示
public function edit(int $id)
{
$post = $this->postService->getPostById($id);
return view('posts.edit', compact('post'));
}
// 記事更新処理
public function update(PostRequest $request, int $id)
{
$this->postService->updatePost($id, $request->validated());
return redirect()->route('posts.index');
}
// 記事削除処理
public function destroy(int $id)
{
$this->postService->deletePost($id);
return redirect()->route('posts.index');
}
}
ルーティングの設定とビューの作成
こちらはユースケース駆動設計と同じです。
ドメイン駆動設計のまとめ
ドメイン駆動設計を用いることで、ブログアプリは以下の特徴を持つようになります:
- 柔軟性と拡張性: 業務ロジックがドメイン層に集中し、変更に強い。
- 可読性: ユビキタス言語に基づくコードで、関係者間の理解が統一される。
- テスト可能性: 各層が独立しているため、テストが容易。
これにより、堅牢で保守性の高いブログアプリを構築できます。
まとめ
ユースケース駆動設計とドメイン駆動設計はそれぞれのメリット/デメリットがあります。自身が作成するアプリケーションによって、設計を選択してみてください。