DDDの各レイヤの実装ガイド:身近な例で学ぶ
DDDの各レイヤの実装ガイド:身近な例で学ぶ
はじめに
Domain‑Driven Design (DDD) の層アーキテクチャは、複雑なドメインを扱うソフトウェアの設計を容易にするために、コードを複数の層に分割します (microwind.medium.com, docs.appflowy.io) 。各層は特定の責務を持ち、高凝集かつ低結合を保つことで変更に強い構造になります。本記事では DDD の代表的な 4 レイヤの役割と実装ポイントをまとめ、オンライン書店という身近な例で具体的に解説します。
DDDの4レイヤとその役割
| レイヤ | 主な役割 |
|---|---|
| プレゼンテーション (UI) 層 | ユーザー入力を受け取り、情報を表示する (microwind.medium.com, docs.appflowy.io) 。ここではフロントエンドのコンポーネントや画面を実装し、ユーザーの操作をアプリケーション層に渡す。 |
| アプリケーション層 | ユースケースを実行し、ワークフローやトランザクションを調整する (www.kranio.io, www.hibit.dev) 。ビジネスロジックは持たず、ドメイン層のオブジェクトを操作するサービスが配置される。 |
| ドメイン層 | システムのコアとなるビジネスルールと状態を実装する (microwind.medium.com, www.kranio.io) 。エンティティ・値オブジェクト・集約・ドメインサービス・ドメインイベントなどを定義し、ユビキタス言語に基づいてモデルを構築する。 |
| インフラストラクチャ層 | データベース、外部 API、メッセージングなど技術的な実装を提供する (microwind.medium.com, docs.appflowy.io) 。ドメイン層のリポジトリやサービスのインタフェースを実装し、技術的な詳細を隠蔽する。 |
身近な例で見る各レイヤの実装
ここでは「オンライン書店」を題材に、各層で何を実装するのかを具体的に見てみます。
1. プレゼンテーション (UI) 層
ユーザーが本を検索したり、カートに追加したりする画面を構築します。React や Vue などのフロントエンドフレームワークで、「本一覧ページ」「カートページ」「注文確認ページ」などのコンポーネントを実装し、ユーザー操作に応じてアプリケーション層のサービスを呼び出します。UI 層ではビジネスロジックは持たず、入力の検証や表示の制御に集中します。
2. アプリケーション層
オンライン書店のユースケースを実現するサービスを用意します。例えば:
-
OrderService.createOrder(userId, cartItems)– カートの情報を受け取り、ドメイン層のOrderエンティティを生成して保存する。 -
CatalogService.searchBooks(keyword)– キーワードで本を検索し、結果を返す。 -
PaymentService.requestPayment(orderId)– 支払リクエストを外部決済サービスに送る。
アプリケーション層ではワークフローやトランザクション管理を行い、ドメイン層に定義されたリポジトリやサービスを呼び出してビジネス処理を委譲します (www.kranio.io) 。
3. ドメイン層
ビジネスルールを表現する中心的な層です。オンライン書店では次のような要素を実装します。
-
エンティティ:
Book(ISBN・タイトル・価格など)、Order(注文番号・注文者・注文項目・合計金額)、Customer(顧客 ID・名前・メール) など。 -
値オブジェクト:
Money(通貨と金額)、Address(住所)など、識別子を持たずイミュータブルな値。 -
集約:
Order集約は注文とその項目・支払い状態を一体として管理し、整合性を保証します。 - ドメインサービス: 複数のエンティティにまたがるビジネスルール、例えば「送料計算」「キャンペーン割引計算」など。
-
ドメインイベント:
OrderCreatedやPaymentCompletedなど、ドメインの状態変化を他層に通知するイベント。
ドメイン層は外部技術に依存しない純粋なビジネスコードであり (www.kranio.io) 、インタフェースを通じてインフラ層にアクセスします。
4. インフラストラクチャ層
技術的な詳細を担当する層です。オンライン書店では以下を実装します。
-
リポジトリ実装:
BookRepositoryやOrderRepositoryをデータベース(例: PostgreSQL)で実装し、ドメイン層で定義されたリポジトリインタフェースを満たします。 - 外部サービス連携: 決済 API との通信、メール送信サービス、在庫管理システムなどを扱います。
-
メッセージング: 注文完了時に
OrderCreatedイベントをメッセージキューに発行し、他のマイクロサービスが購読できるようにします。
インフラ層は技術ごとの処理に徹し、ドメインロジックを含めないようにします (www.kranio.io) 。
実装のポイントとまとめ
- 各レイヤは役割を明確に分け、内側のレイヤに依存するが外側には依存しないようにします。特にドメイン層は他の層から独立しており、インフラ層はドメイン層のインタフェースを実装することで技術的詳細を隠蔽します (www.hibit.dev) 。
- UI 層ではユーザー体験や入出力の制御に集中し、アプリケーション層ではユースケースの調整やトランザクション管理を行い、ドメイン層にビジネスロジックを集約します (www.kranio.io, microwind.medium.com) 。
-
ディレクトリ構成と実装例
ここでは、シンプルな「書籍注文」システムを例に、DDD の各レイヤに配置するコードとディレクトリ構成を示します。ユーザーが書籍を覧覧し、注文を行うユースケースを考えます。
ディレクトリ構成の例
├─┐ ui/
│ └─┐ pages/
│ └─ BookListPage.tsx
├─┐ application/
│ └─┐ services/
│ └─ OrderService.ts
├─┐ domain/
│ ├─┐ entities/
│ │ └─ Order.ts
│ ├─┐ repositories/
│ │ └─ OrderRepository.ts
│ └─┐ valueObjects/
│ └─ BookId.ts
└─┐ infrastructure/
└─┐ repositories/
└─ OrderRepositoryMemory.ts
この構成では、UI 層がページやコンポーネント、アプリケーション層がユースケースを表すサービス、ドメイン層がエンティティや値オブジェクトとリポジトリのインタフェース、インフラ層が具体的なリポジトリ実装を持ちます。
各層の実装例
- UI 層(BookListPage.tsx)
import React, { useState, useEffect } from 'react';
import { OrderService } from '../application/services/OrderService';
const BookListPage: React.FC = () => {
const [books, setBooks] = useState<BookSummary[]>([]);
const orderService = new OrderService();
useEffect(() => {
// ここでは API から書籍一覧を取得すると仮定します。
fetch('/api/books')
.then((res) => res.json())
.then((data) => setBooks(data));
}, []);
const handleOrder = (bookId: string) => {
orderService.placeOrder(bookId);
alert('ご注文ありがとうございます');
};
return (
<div>
<h1>書籍一覧</h1>
<ul>
{books.map((book) => (
<li key={book.id}>
{book.title} - {book.price}円
<button onClick={() => handleOrder(book.id)}>注文</button>
</li>
))}
</ul>
</div>
);
};
export default BookListPage;
- アプリケーション層(OrderService.ts)
import { Order } from '../../domain/entities/Order';
import { OrderRepository } from '../../domain/repositories/OrderRepository';
import { OrderRepositoryMemory } from '../../infrastructure/repositories/OrderRepositoryMemory';
export class OrderService {
private orderRepository: OrderRepository;
constructor(repo: OrderRepository = new OrderRepositoryMemory()) {
this.orderRepository = repo;
}
// ユースケース: 書籍を注文する
placeOrder(bookId: string) {
const order = Order.create(bookId);
this.orderRepository.save(order);
}
}
- ドメイン層(Order.ts と OrderRepository.ts)
// entities/Order.ts
export class Order {
constructor(
public readonly id: string,
public readonly bookId: string,
public readonly orderedAt: Date,
) {}
static create(bookId: string): Order {
return new Order(crypto.randomUUID(), bookId, new Date());
}
}
// repositories/OrderRepository.ts
import { Order } from '../entities/Order';
export interface OrderRepository {
save(order: Order): void;
findById(id: string): Order | undefined;
}
- インフラ層(OrderRepositoryMemory.ts)
import { Order } from '../../domain/entities/Order';
import { OrderRepository } from '../../domain/repositories/OrderRepository';
// メモリ上で注文を保存する簡易実装
export class OrderRepositoryMemory implements OrderRepository {
private orders: Order[] = [];
save(order: Order): void {
this.orders.push(order);
}
findById(id: string): Order | undefined {
return this.orders.find((o) => o.id === id);
}
}
身近な例を通じて、各レイヤにどのようなコードや責務を配置するかがイメージできるようになります。DDD の層アーキテクチャを理解し、プロジェクトの技術選定や設計に活かしましょう。
Discussion