【DDD】ドメイン駆動設計 完全ガイド - OOPから関数型まで
DDD(Domain-Driven Design、ドメイン駆動設計)は、複雑なビジネスロジックを持つシステムの設計に適した手法です。本記事では、DDDの概念から実装方法まで、OOP(オブジェクト指向)と関数型の両方のアプローチで解説します。
DDDとは
DDD(Domain-Driven Design、ドメイン駆動設計)は、Eric Evansが2003年に提唱したソフトウェア設計手法です。
核心的な考え方
ビジネスドメイン(業務領域)を中心に設計するという思想です。技術ではなく、ビジネスの問題領域を深く理解し、それをコードに反映させます。
主要な概念
戦略的設計(大きな構造)
| 概念 | 説明 |
|---|---|
| ユビキタス言語 | 開発者とドメインエキスパートが共通で使う用語 |
| 境界づけられたコンテキスト | モデルが適用される明確な境界 |
| コンテキストマップ | 複数のコンテキスト間の関係を図示 |
戦術的設計(実装パターン)
| パターン | 説明 |
|---|---|
| エンティティ | 一意のIDを持つオブジェクト(例: ユーザー、注文) |
| 値オブジェクト | IDを持たず、属性で同一性を判断(例: 住所、金額) |
| 集約 | 関連するエンティティ・値オブジェクトのまとまり |
| リポジトリ | 集約の永続化を担当 |
| ドメインサービス | エンティティに属さないビジネスロジック |
| ファクトリ | 複雑なオブジェクト生成を担当 |
詳しく知りたい場合は、Eric Evansの「Domain-Driven Design」(通称: 青本)や、Vaughn Vernonの「実践ドメイン駆動設計」(通称: 赤本)が参考になります。
DDDに適した言語
DDDは言語に依存しない設計手法です。どの言語でも適用できます。
相性が良い言語
| 言語 | 理由 |
|---|---|
| Java / Kotlin | クラスベースOOP、豊富なDDDライブラリ・フレームワーク |
| C# | 同上、.NETエコシステムでDDD実践例が多い |
| TypeScript | 型システムでドメインモデルを表現しやすい |
| Scala | 関数型+OOPのハイブリッドで表現力が高い |
適用可能だが工夫が必要な言語
| 言語 | 備考 |
|---|---|
| Python | 動的型付けだが、dataclassやPydanticで値オブジェクトを表現 |
| Ruby | 同様に可能、Rails外の設計が必要になることも |
| Go | 構造体とインターフェースで実装、継承がないので工夫が必要 |
| Rust | 所有権システムと組み合わせて堅牢な設計が可能 |
関数型言語でのDDD
F#、Haskell、Elixirなどでも適用可能です。「関数型ドメインモデリング」というアプローチがあり、代数的データ型で不正な状態を型レベルで防ぐ設計ができます。
OOPスタイルでのDDD実装
シンプルな「ECサイトの注文」を例に解説します。
フォルダ構造
src/
├── domain/ # ドメイン層(ビジネスロジック)
│ ├── entities/ # エンティティ
│ │ ├── order.py
│ │ └── user.py
│ ├── value_objects/ # 値オブジェクト
│ │ ├── money.py
│ │ └── email.py
│ ├── repositories/ # リポジトリ(インターフェース)
│ │ └── order_repository.py
│ └── services/ # ドメインサービス
│ └── pricing_service.py
│
├── application/ # アプリケーション層(ユースケース)
│ └── use_cases/
│ └── create_order.py
│
├── infrastructure/ # インフラ層(技術的な実装)
│ └── repositories/
│ └── order_repository_impl.py
│
└── main.py
各層の役割
| 層 | 役割 | 依存方向 |
|---|---|---|
| domain | ビジネスロジック。外部に依存しない | なし |
| application | ユースケースの実行 | domain |
| infrastructure | DB、API等の技術的実装 | domain, application |
値オブジェクト(Value Object)
IDを持たず、値で同一性を判断します。イミュータブルにするのがポイントです。
# domain/value_objects/money.py
from dataclasses import dataclass
@dataclass(frozen=True) # frozen=True でイミュータブルに
class Money:
amount: int
currency: str = "JPY"
def __post_init__(self):
if self.amount < 0:
raise ValueError("金額は0以上である必要があります")
def add(self, other: "Money") -> "Money":
if self.currency != other.currency:
raise ValueError("通貨が異なります")
return Money(self.amount + other.amount, self.currency)
使用例:
price1 = Money(1000)
price2 = Money(500)
total = price1.add(price2) # Money(1500, "JPY")
# 同じ値なら等価
Money(100) == Money(100) # True
エンティティ(Entity)
一意のIDを持ち、IDで同一性を判断します。
# domain/entities/order.py
from dataclasses import dataclass, field
from typing import List
from uuid import UUID, uuid4
from enum import Enum
from domain.value_objects.money import Money
class OrderStatus(Enum):
PENDING = "pending"
CONFIRMED = "confirmed"
CANCELLED = "cancelled"
@dataclass
class OrderItem:
product_id: str
product_name: str
price: Money
quantity: int
@property
def subtotal(self) -> Money:
return Money(self.price.amount * self.quantity)
@dataclass
class Order:
id: UUID
customer_id: UUID
items: List[OrderItem] = field(default_factory=list)
status: OrderStatus = OrderStatus.PENDING
@classmethod
def create(cls, customer_id: UUID) -> "Order":
"""ファクトリメソッド"""
return cls(id=uuid4(), customer_id=customer_id)
def add_item(self, item: OrderItem) -> None:
"""商品を追加"""
if self.status != OrderStatus.PENDING:
raise ValueError("確定済みの注文には追加できません")
self.items.append(item)
def confirm(self) -> None:
"""注文を確定"""
if not self.items:
raise ValueError("商品がありません")
self.status = OrderStatus.CONFIRMED
@property
def total(self) -> Money:
"""合計金額"""
if not self.items:
return Money(0)
result = Money(0)
for item in self.items:
result = result.add(item.subtotal)
return result
def __eq__(self, other):
"""IDで同一性を判断"""
if not isinstance(other, Order):
return False
return self.id == other.id
リポジトリ(Repository)インターフェース
ドメイン層では「何ができるか」だけ定義します。
# domain/repositories/order_repository.py
from abc import ABC, abstractmethod
from typing import Optional
from uuid import UUID
from domain.entities.order import Order
class OrderRepository(ABC):
"""注文リポジトリのインターフェース"""
@abstractmethod
def save(self, order: Order) -> None:
pass
@abstractmethod
def find_by_id(self, order_id: UUID) -> Optional[Order]:
pass
リポジトリの実装(Infrastructure)
技術的な実装はインフラ層で行います。
# infrastructure/repositories/order_repository_impl.py
from typing import Optional
from uuid import UUID
from domain.entities.order import Order
from domain.repositories.order_repository import OrderRepository
class InMemoryOrderRepository(OrderRepository):
"""メモリ上で保存する実装(テスト用)"""
def __init__(self):
self._orders: dict[UUID, Order] = {}
def save(self, order: Order) -> None:
self._orders[order.id] = order
def find_by_id(self, order_id: UUID) -> Optional[Order]:
return self._orders.get(order_id)
ユースケース(Application)
アプリケーション層でビジネスフローを組み立てます。
# application/use_cases/create_order.py
from dataclasses import dataclass
from uuid import UUID
from domain.entities.order import Order, OrderItem
from domain.value_objects.money import Money
from domain.repositories.order_repository import OrderRepository
@dataclass
class CreateOrderInput:
customer_id: UUID
items: list[dict]
@dataclass
class CreateOrderOutput:
order_id: UUID
total: int
class CreateOrderUseCase:
def __init__(self, order_repository: OrderRepository):
self._order_repository = order_repository
def execute(self, input_data: CreateOrderInput) -> CreateOrderOutput:
# 1. 注文を作成
order = Order.create(customer_id=input_data.customer_id)
# 2. 商品を追加
for item in input_data.items:
order_item = OrderItem(
product_id=item["product_id"],
product_name=item["product_name"],
price=Money(item["price"]),
quantity=item["quantity"],
)
order.add_item(order_item)
# 3. 注文を確定
order.confirm()
# 4. 保存
self._order_repository.save(order)
return CreateOrderOutput(
order_id=order.id,
total=order.total.amount,
)
実行例
# main.py
from uuid import uuid4
from application.use_cases.create_order import CreateOrderUseCase, CreateOrderInput
from infrastructure.repositories.order_repository_impl import InMemoryOrderRepository
# 依存性の注入
repository = InMemoryOrderRepository()
use_case = CreateOrderUseCase(order_repository=repository)
# 注文を作成
result = use_case.execute(
CreateOrderInput(
customer_id=uuid4(),
items=[
{"product_id": "p1", "product_name": "Tシャツ", "price": 2000, "quantity": 2},
{"product_id": "p2", "product_name": "パンツ", "price": 3000, "quantity": 1},
],
)
)
print(f"注文ID: {result.order_id}")
print(f"合計: {result.total}円")
# 出力:
# 注文ID: 550e8400-e29b-41d4-a716-446655440000
# 合計: 7000円
- 値オブジェクト:
@dataclass(frozen=True) - エンティティ:
@dataclass+ IDで__eq__定義 - リポジトリ:
ABCで抽象クラス定義 - 依存性の注入: コンストラクタで渡す
関数型スタイルでのDDD実装
関数型DDDは、データと関数を分離し、イミュータブルなデータを扱うアプローチです。
OOPとの構造比較
| OOP | 関数型 |
|---|---|
entities/ | types/(データ定義のみ) |
services/ | operations/(純粋関数) |
repositories/ | ports/ + adapters/ |
use_cases/ | workflows/(関数合成) |
フォルダ構造
src/
├── domain/
│ ├── types/ # 型定義(データのみ)
│ │ ├── order.py
│ │ ├── money.py
│ │ └── errors.py
│ ├── operations/ # 純粋関数(ロジック)
│ │ ├── order_ops.py
│ │ └── pricing_ops.py
│ └── ports/ # 副作用のインターフェース
│ └── order_port.py
│
├── application/
│ └── workflows/ # ワークフロー(関数の合成)
│ └── create_order.py
│
├── infrastructure/
│ └── adapters/ # 副作用の実装
│ └── order_adapter.py
│
└── main.py
型定義(イミュータブルなデータ)
# domain/types/order.py
from dataclasses import dataclass
from typing import Tuple
from uuid import UUID
from enum import Enum
from domain.types.money import Money
class OrderStatus(Enum):
PENDING = "pending"
CONFIRMED = "confirmed"
CANCELLED = "cancelled"
@dataclass(frozen=True)
class OrderItem:
product_id: str
product_name: str
price: Money
quantity: int
@dataclass(frozen=True)
class Order:
id: UUID
customer_id: UUID
items: Tuple[OrderItem, ...] # イミュータブルなtuple
status: OrderStatus
エラー型の定義:
# domain/types/errors.py
from dataclasses import dataclass
from typing import Union
@dataclass(frozen=True)
class OrderError:
message: str
code: str
# エラー型の定義
EmptyOrderError = OrderError("商品がありません", "EMPTY_ORDER")
AlreadyConfirmedError = OrderError("既に確定済みです", "ALREADY_CONFIRMED")
# Result型(成功 or エラー)
type Result[T] = Union[T, OrderError]
純粋関数(ドメインロジック)
データを受け取り、新しいデータを返します。副作用なし。
# domain/operations/order_ops.py
from uuid import UUID, uuid4
from domain.types.order import Order, OrderItem, OrderStatus
from domain.types.money import Money
from domain.types.errors import Result, OrderError, EmptyOrderError, AlreadyConfirmedError
def create_order(customer_id: UUID) -> Order:
"""新しい注文を作成"""
return Order(
id=uuid4(),
customer_id=customer_id,
items=(),
status=OrderStatus.PENDING,
)
def add_item(order: Order, item: OrderItem) -> Result[Order]:
"""商品を追加(新しいOrderを返す)"""
if order.status != OrderStatus.PENDING:
return AlreadyConfirmedError
return Order(
id=order.id,
customer_id=order.customer_id,
items=order.items + (item,),
status=order.status,
)
def confirm(order: Order) -> Result[Order]:
"""注文を確定"""
if not order.items:
return EmptyOrderError
if order.status != OrderStatus.PENDING:
return AlreadyConfirmedError
return Order(
id=order.id,
customer_id=order.customer_id,
items=order.items,
status=OrderStatus.CONFIRMED,
)
def calculate_total(order: Order) -> Money:
"""合計金額を計算"""
total = sum(item.price.amount * item.quantity for item in order.items)
return Money(total)
OOPと関数型の違い
新しいOrderを返す(イミュータブル)
new_order = add_item(order, item)
# 元のorderは変更されない自分自身を変更する(ミュータブル)
order.add_item(item)
# orderが変更されるエラーハンドリングの違い
OOPでは例外を投げます:
class Order:
def confirm(self) -> None:
if not self.items:
raise ValueError("商品がありません")
self.status = OrderStatus.CONFIRMED
関数型では型でエラーを表現します:
def confirm(order: Order) -> Union[Order, OrderError]:
if not order.items:
return OrderError("商品がありません")
return Order(
id=order.id,
items=order.items,
status=OrderStatus.CONFIRMED,
)
# 使用(エラーチェックが強制される)
result = confirm(order)
if isinstance(result, OrderError):
print(result.message)
else:
save(result)
関数型DDDの利点
テストが簡単(モック不要):
def test_add_item():
order = create_order(uuid4())
item = OrderItem("p1", "商品", Money(100), 1)
result = add_item(order, item)
assert isinstance(result, Order)
assert len(result.items) == 1
assert order.items == () # 元のorderは変更されない
def test_confirm_empty_order():
order = create_order(uuid4())
result = confirm(order)
assert result == EmptyOrderError
OOP vs 関数型 DDDの比較
| 観点 | OOP DDD | 関数型 DDD |
|---|---|---|
| エンティティ | クラス(データ+メソッド) | データ型 + 関数群 |
| 状態変更 | order.add_item(item) | new_order = add_item(order, item) |
| エラー | 例外を投げる | Result[T]型で返す |
| リポジトリ | 抽象クラス+実装クラス | Protocol + 関数注入 |
| ユースケース | クラスのexecuteメソッド | 関数の合成 |
| テスト | モック必要 | 純粋関数は入出力のみ |
どちらを選ぶ?
| 状況 | 推奨 |
|---|---|
| チームがOOPに慣れている | OOP |
| 並行処理が多い | 関数型 |
| テストしやすさ重視 | 関数型 |
| Python/Java/C# | OOP(言語の特性を活かす) |
| Haskell/Elixir/F# | 関数型 |
実際にはハイブリッドも多く、イミュータブルなデータクラスとOOPを組み合わせるのが現実的です。
DDDの選定基準
DDDが向いているケース
- 複雑なビジネスロジックがある
- 長期間(数年以上)運用する
- 頻繁に仕様変更がある
- ドメインエキスパートと連携できる
- チーム規模が中〜大(5人以上)
- マイクロサービス化を検討している
具体例:
- ECサイト(注文、在庫、配送、決済の複雑な連携)
- 金融システム(取引、口座、規制対応)
- 予約システム(空き状況、料金計算、キャンセルポリシー)
- 医療システム(患者、診療、処方の複雑なルール)
DDDが向いていないケース
- シンプルなCRUDアプリ
- 短期プロジェクト(数ヶ月で終了)
- 仕様が固定で変更が少ない
- 1〜2人の小規模チーム
- プロトタイプ・MVP
- ドメインエキスパートがいない
判断フローチャート
ビジネスロジックは複雑?
├─ No → DDDは不要(シンプルなMVCで十分)
└─ Yes
↓
長期運用(2年以上)?
├─ No → DDDは過剰(軽量なクリーンアーキテクチャ程度)
└─ Yes
↓
ドメインエキスパートと連携可能?
├─ No → 戦術的DDDのみ採用(パターンだけ使う)
└─ Yes
↓
チームにDDD経験者がいる?
├─ No → 学習期間を設けてから段階的に導入
└─ Yes → フルDDDを採用
段階的な採用戦略
| レベル | 採用範囲 | 適用ケース |
|---|---|---|
| Level 0 | なし | シンプルなアプリ |
| Level 1 | 値オブジェクトのみ | データの型安全性を上げたい |
| Level 2 | エンティティ + リポジトリ | ビジネスロジックを整理したい |
| Level 3 | 集約 + ドメインサービス | 複雑なルールを管理したい |
| Level 4 | 境界づけられたコンテキスト | マイクロサービス化 |
| Level 5 | フルDDD + イベント駆動 | 大規模エンタープライズ |
まず値オブジェクトとエンティティだけ導入し、必要に応じて拡張する「段階的採用」がおすすめです。
DDDのメリット・デメリット
- ビジネスロジックが整理される
- 変更に強い設計になる
- チーム間の認識が揃う
- テストしやすい
- 長期保守がしやすい
- 学習コストが高い
- 初期の開発速度が遅い
- 小規模プロジェクトには過剰
- ドメインエキスパートの協力が必要
- 設計の合意形成に時間がかかる
代替・併用できるアーキテクチャ
| アーキテクチャ | 複雑さ | 適用ケース |
|---|---|---|
| MVC | 低 | シンプルなWebアプリ |
| クリーンアーキテクチャ | 中 | 依存関係の整理が必要 |
| ヘキサゴナル | 中 | 外部依存を分離したい |
| DDD | 高 | 複雑なドメインロジック |
| イベント駆動 | 高 | 非同期・分散システム |
まとめ
DDDは複雑なビジネスロジックを持つシステムの設計に適した手法です。
| 質問 | DDDを採用 |
|---|---|
| ビジネスルールを説明するのに30分以上かかる? | ✅ |
| 「この場合はどうなる?」の質問が多い? | ✅ |
| 仕様変更で広範囲のコード修正が発生する? | ✅ |
| データベースのテーブル設計から始める? | ❌ |
| ほぼCRUD操作だけ? | ❌ |
言語よりもチームの理解と設計方針が重要です。OOPスタイルと関数型スタイルのどちらを選ぶかは、チームの経験や要件に応じて判断してください。