エンティティと値オブジェクト

DDD における 2 つの基本的なドメインモデル要素 - 同一性で区別するエンティティと、値で区別する値オブジェクト

DDD設計

エンティティと値オブジェクトとは

エンティティ (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 は説明のための簡略形。実務では最小通貨単位の整数か十進数型で保持する (JavaScript0.1 + 0.20.30000000000000004 になり、加算を重ねると差額が合わなくなる)

実践的な知識は関連書籍でも得られる。

この記事は役に立ちましたか?

関連用語

関連する記事