概要
ドメインモデルを正しく設計する上で避けて通れないのが、
**「エンティティ」と「バリューオブジェクト」の峻別、
そしてそれらを束ねる「集約ルート」**という概念である。
これらは単なるOOPの構文ではなく、
意味の境界を設計し、整合性を守るための設計構造である。
本稿では、エンティティとバリューの違い、集約ルートの責務、
そしてそれらがどのようにシステムの秩序を守るのかを、具体的に示す。
1. エンティティとバリューオブジェクトの違い
観点 | エンティティ(Entity) | バリューオブジェクト(VO) |
---|---|---|
同一性 | IDで識別される(id が重要) |
値がすべて(equals で比較) |
可変性 | 状態変化を前提とする | 原則イミュータブル(不変) |
役割 | ビジネス的に「存在するもの」 | 概念・属性・単位などを表現 |
保存方法 | データベースに永続化される対象 | 主にエンティティの属性として従属 |
2. 実装例:ユーザーと氏名
// バリューオブジェクト
class Name {
constructor(public readonly first: string, public readonly last: string) {}
get fullName(): string {
return `${this.first} ${this.last}`
}
}
// エンティティ
class User {
constructor(
public readonly id: string,
public name: Name,
public email: string
) {}
changeName(newName: Name) {
this.name = newName
}
}
-
User
は ID によって同一性を保つエンティティ -
Name
は中身が同じであれば同一とみなすVO
3. 集約と集約ルート
**集約(Aggregate)**とは、一貫性を保つべきドメインオブジェクトのまとまり
**集約ルート(Aggregate Root)**とは、外部との接点を制御する唯一の入り口
✅ 責務
- 内部の一貫性・整合性を保証
- 外部からはルート越しにのみアクセス可能(カプセル化)
✅ 集約ルートのイメージ構造
[ Order ] ← 集約ルート
├─ [ OrderItem ] ← 内部VO or 子エンティティ
└─ [ DiscountPolicy ] ← VO
→ OrderItem
を直接操作せず、必ず Order.addItem()
経由で更新する
→ 集約の「整合性」や「不変条件」を1か所で管理可能にする
4. 集約ルートによる整合性の担保例
class Order {
private items: OrderItem[] = []
addItem(product: Product, quantity: number) {
if (this.isClosed) throw new Error("Order already closed.")
this.items.push(new OrderItem(product, quantity))
}
}
-
items
を直接 expose しない - 追加・変更のインターフェースを限定することでビジネスルールを中央集権化
5. 設計判断フロー
① このオブジェクトはビジネス的に識別されるか? → YES → エンティティ
② 同じ値なら完全に等しいと見なせるか? → YES → バリューオブジェクト
③ 整合性の単位として設計されているか? → YES → 集約ルートを設ける
④ 外部との接点を誰が管理するか? → 集約ルートに集約させる
6. よくある誤解と対策
❌ VOはただの型やラッパーと見なす
→ ✅ VOは意味・制約・単位を表現するドメイン知識の塊
→ 単なる string 型では表現しきれない「意味」を封じ込める
❌ 子エンティティに直接アクセスしてもいい
→ ✅ 集約の内部整合性を壊すリスクがある
→ 集約ルートの意図的な設計=保守性の高い構造
❌ 集約が大きすぎる/細かすぎる
→ ✅ 一貫性の境界を設計判断の単位にする。技術ではなくビジネスルールで切り分ける
結語
エンティティとバリューオブジェクト、そして集約ルートは、
ドメインの意味と整合性を構造として制御するための設計基準である。
- エンティティは「誰」であるかを定義し
- VOは「何」であるかを明確にし
- 集約は「何を一貫して保つか」を保証する
それらを設計に取り入れるとは、
“ソフトウェアに意味を宿し、変化と整合性を両立させる構造を与えること” に他ならない。