エンティティと値オブジェクト
DDD における 2 つの基本的なドメインモデル要素 - 同一性で区別するエンティティと、値で区別する値オブジェクト
エンティティと値オブジェクトとは
エンティティ (Entity) と値オブジェクト (Value Object) は、ドメイン駆動設計 (DDD) における 2 つの基本的なモデル要素である。エンティティは同一性 (ID) で区別し、値オブジェクトは属性の値で区別する。
比較
主な違いを以下にまとめる。
| 観点 | エンティティ | 値オブジェクト |
|---|---|---|
| 同一性 | ID で区別 | 値で区別 |
| 可変性 | 属性は変わり得る | 不変として扱う |
| ライフサイクル | 生成 → 変更 → 削除 | 生成 → 破棄 (変更は新規作成) |
| 等価性 | ID が同じなら同一 | 全属性が同じなら等価 |
| 例 | ユーザー、注文、商品 | メールアドレス、金額、住所 |
可変性の違いは結果であって定義ではない。Eric Evans の DDD Reference (2015 年版) はエンティティを「属性が変わり得るのに、時間をまたいで、ときには異なる表現形をまたいで同一性の筋が通るもの」と定義し、区別の基準を属性ではなく同一性に置いている (別名 Reference Object)。値オブジェクトについては「不変として扱い、すべての操作を可変な状態に依存しない副作用のない関数にする」と明示的に指示している。
したがって、エンティティを不変オブジェクトとして実装しても DDD から外れるわけではない。状態変更のたびに新しいインスタンスを返す設計は成り立つ。分岐点は「同一性を ID が担っているか」であり、可変か不変かは実装スタイルの選択に過ぎない。
TypeScript での実装
エンティティ
class User {
constructor(
readonly id: string,
private name: string,
private email: Email, // 値オブジェクト
) {}
changeName(newName: string) { this.name = newName; }
equals(other: User): boolean {
return this.id === other.id; // ID で比較
}
}
値オブジェクト
class Email {
readonly value: string;
constructor(value: string) {
if (!value.includes('@')) throw new Error('Invalid email');
this.value = value; // バリデーション + 不変
}
equals(other: Email): boolean {
return this.value === other.value; // 値で比較
}
}
class Money {
constructor(readonly amount: number, readonly currency: string) {}
add(other: Money): Money {
if (this.currency !== other.currency) throw new Error('Currency mismatch');
return new Money(this.amount + other.amount, this.currency); // 新しいインスタンスを返す
}
equals(other: Money): boolean {
return this.amount === other.amount && this.currency === other.currency;
}
}
Money.add が自身を書き換えず新しい Money を返すのは、上記の「副作用のない関数」の原則をそのまま実装した形である。呼び出し元が保持している Money は決して変化しないため、複数箇所で同じインスタンスを共有しても安全になる。
判断基準
判断基準を以下にまとめる。
| 質問 | エンティティ | 値オブジェクト |
|---|---|---|
| ID で追跡する必要があるか? | 必要。ID が同じなら同じものとして扱う | 不要。値がすべて等しければ同じものとして扱う |
| 属性が変わっても同じものか? | 同じ。名前が変わっても同じユーザーである | 別物。1 つでも値が変われば別の値になる |
| 交換可能か? | 交換できない。個体そのものを指している | 交換できる。同じ値なら差し替えても影響しない |
DynamoDB での表現
DynamoDB での表現の例を示す。
// エンティティ: ID で管理
{
"pk": "USER#123",
"sk": "PROFILE",
"name": "Alice",
"email": "alice@example.com", // 値オブジェクトは属性として埋め込む
"address": { // 値オブジェクト
"prefecture": "東京都",
"city": "渋谷区"
}
}
値オブジェクトは ID を持たないため、独立したアイテムにせずエンティティの属性として埋め込むのが基本になる。
ただし埋め込みが常に正しいわけではない。件数の上限が読めない集合 (注文明細、住所の履歴など) を抱え込むと、1 アイテム 400 KB という DynamoDB のアイテムサイズ上限に近づいていく。さらに UpdateItem の書き込み容量は更新前後のアイテムサイズの大きい方で決まるため、埋め込んだ属性の 1 つを書き換えるだけでもアイテム全体分の容量を消費する。増え続ける集合は sk を分けた別アイテムへ切り出し、モデル上は値オブジェクトのまま扱うのが現実的な落としどころになる。
よくある間違い
- 全てをエンティティにする → 値オブジェクトにすべきものまで ID を振る
- 値オブジェクトを可変にする → バグの温床になる
- プリミティブ型で済ませる →
stringではなくEmail型にすることでバリデーションを強制 - 金額を浮動小数点で持つ → 上の
Moneyは説明のための簡略形。実務では最小通貨単位の整数か十進数型で保持する (JavaScript の0.1 + 0.2は0.30000000000000004になり、加算を重ねると差額が合わなくなる)
実践的な知識は関連書籍でも得られる。
この記事は役に立ちましたか?
関連用語
ドメイン駆動設計 (DDD)
ビジネスドメインの知識を中心に据え、ドメインエキスパートと開発者が共通言語で協働しながらソフトウェアを設計する手法
集約
ドメイン駆動設計において、一貫性を保つべきオブジェクト群をまとめ、単一のルートエンティティ経由でアクセスする設計パターン
クリーンアーキテクチャ
ビジネスロジックを外部の技術的詳細から分離し、依存関係を内側に向けることで変更に強い設計を実現するアーキテクチャ原則
S3 ライフサイクル
S3 オブジェクトのストレージクラスを自動的に移行し、コストを最適化する機能
オブジェクト指向
データと操作をオブジェクトとしてまとめ、現実の概念に近づけて設計する考え方
Flyweight パターン
多数のオブジェクト間で共有可能な状態を分離し、メモリ使用量を削減するデザインパターン