ドメインモデルをコードで表現するためのパターン集(ビルディングブロック)。
エンティティ(Entity)¶
属性ではなく**同一性(identity)**によって識別されるオブジェクト。属性が変わっても同じものとして追跡される(例:ユーザー、注文)。
値オブジェクト(Value Object)¶
同一性を持たず、属性の値そのものが意味を持つ不変(immutable)なオブジェクト(例:金額、住所、期間)。プリミティブ型をそのまま使う代わりに値オブジェクトを定義することで、ドメインの概念とバリデーションをコードに表現できる。
集約(Aggregate)¶
整合性を保つ単位としてまとめられたエンティティ・値オブジェクトのかたまり。外部からは集約ルート(root entity)を経由してのみアクセスし、トランザクションの境界も集約単位にする(例:「注文」集約は注文明細を内包し、注文経由でのみ明細を操作する)。
ドメインサービス(Domain Service)¶
特定のエンティティや値オブジェクトに属さないドメインロジックを置く場所(例:口座間の送金処理)。ロジックを何でもサービスに置くとドメインモデル貧血症(anemic domain model)になるため、まずエンティティ・値オブジェクトに置けないか検討する。
リポジトリ(Repository)¶
集約の永続化と再構築を抽象化するインターフェース。ドメイン層からデータベースの詳細を隠蔽する。
ファクトリ(Factory)¶
複雑なオブジェクト(集約)の生成処理をカプセル化する。
ドメインイベント(Domain Event)¶
ドメインで起きた出来事をオブジェクトとして表現する(例:「注文が確定された」)。コンテキスト間の連携やイベントソーシングの基礎となる。Evans本の初版にはなく、後に体系化されたパターン。
代数的データ型(Algebraic Data Type, ADT)¶
型を「かけ算」(直積型)と「足し算」(直和型)の組み合わせで作るという考え方。関数型言語(Haskell, F#, OCaml)由来の概念だが、TypeScriptやRuby(3.0以降のパターンマッチング)など多くの言語で実践できる。値オブジェクトや状態のモデリングと相性が良い。
直積型(Product Type)= AND¶
複数の値を同時に持つ型(タプル、構造体、レコードなど)。値オブジェクトはほぼ直積型(例:金額 = {amount, currency})。
直和型(Sum Type)= OR¶
複数の候補のうち1つだけを持つ型(タグ付き共用体 / discriminated union とも呼ばれる)。DDDでは「注文のステータス」のように排他的な状態をモデリングするのに向いている。
利点¶
不正な状態を表現不可能にする(make illegal states unrepresentable):「発送済みなのに追跡番号がない」といった組み合わせを型レベルで排除できる。
網羅性チェック(exhaustiveness check):状態の分岐漏れを静的に検出できる(言語によって強さは異なる)。
以下では「注文ステータス」を直和型でモデリングする例を、TypeScript・Python・Rubyで示す。
TypeScript¶
判別可能なUnion型(discriminated union)で表現する。never型を使うと、分岐漏れをコンパイルエラーとして検出できる。
type OrderStatus =
| { kind: "pending" }
| { kind: "shipped"; trackingNumber: string }
| { kind: "cancelled"; reason: string };
function describe(status: OrderStatus): string {
switch (status.kind) {
case "pending":
return "処理待ち";
case "shipped":
return `発送済み(追跡番号: ${status.trackingNumber})`;
case "cancelled":
return `キャンセル(理由: ${status.reason})`;
default:
// 分岐漏れがあるとここで型エラーになる
const _exhaustive: never = status;
return _exhaustive;
}
}Python¶
3.10以降のmatch文とdataclassを組み合わせて、タグ付き共用体を表現できる。mypyとtyping.assert_neverを併用すると静的な網羅性チェックも可能。
from dataclasses import dataclass
from typing import Union
@dataclass(frozen=True)
class Pending:
pass
@dataclass(frozen=True)
class Shipped:
tracking_number: str
@dataclass(frozen=True)
class Cancelled:
reason: str
OrderStatus = Union[Pending, Shipped, Cancelled]
def describe(status: OrderStatus) -> str:
match status:
case Pending():
return "処理待ち"
case Shipped(tracking_number=tn):
return f"発送済み(追跡番号: {tn})"
case Cancelled(reason=r):
return f"キャンセル(理由: {r})"
for status in [Pending(), Shipped("ABC123"), Cancelled("在庫切れ")]:
print(describe(status))処理待ち
発送済み(追跡番号: ABC123)
キャンセル(理由: 在庫切れ)
Ruby¶
Ruby 3.2以降のData.defineでイミュータブルな値を定義し、3.0以降のパターンマッチング(case/in)で分岐する。静的な網羅性チェックはないが、どのパターンにもマッチしない場合はNoMatchingPatternErrorが実行時に発生する。
Pending = Data.define
Shipped = Data.define(:tracking_number)
Cancelled = Data.define(:reason)
def describe(status)
case status
in Pending
"処理待ち"
in Shipped(tracking_number:)
"発送済み(追跡番号: #{tracking_number})"
in Cancelled(reason:)
"キャンセル(理由: #{reason})"
end
end
[Pending.new, Shipped.new("ABC123"), Cancelled.new("在庫切れ")].each do |status|
puts describe(status)
end