🎃

DDDにおけるValue ObjectとEntityについて

2025/02/03に公開

1. Value Object

IDなどの識別子を持たず、値そのものが本質的な特徴を表すオブジェクトのこと。

特徴

  • 等価性がある。全ての属性が等しい場合、2つのValue Objectは等しい。
  • 不変性が推奨。1度作成されたValue Objectはその値を変更せず、変更が必要な場合は新しくインスタンスを作成する。

具体例

  • Money(通貨と金額を表すクラス・構造体)
  • Address(郵便番号、住所、都市)
type Money struct {
    Amount   float64
    Currency string
}

2. Entity

固有の識別子(ID)を持ち、ライフサイクルを通じて一意に特定されるオブジェクト。
(ライフサイクルとはEntityが生成されてから削除されるまでの一連のプロセスを指す。Entityに存在期間中にその状態や属性がどのように変化するか、どのように扱われるかを指す。)

特徴

  • 同一性。同じIDを持つ場合、2つのEntityは同一。
  • 状態が変化することがある(可変性)。
  • 業務ロジックや振る舞いを持つことが多い。

具体例

  • Order(注文IDで管理)
  • User(ユーザーIDによって識別)
type Order struct {
    ID          string
    TotalAmount *valueObject.Money
    CreatedAt   time.Time
}

3. ユースケース

ECサイトにおける「注文」と「金額」を例に考える。

  • Entity:Order(注文は一意の識別子を持ち、ライフサイクルを持つ)
  • Value Object:Money(金額と通貨情報を持ち、値そのものが重要)

Value Object:Money

package valueobject

type Money struct {
    Amount   float64
    Currency string
}

// コンストラクタ
func NewMoney(amount float64, currency string) *Money {
    return &Money{
        Amount:   amount,
        Currency: currency,
    }
}

// 金額を加算
func (m *Money) Add(other *Money) *Money {
    if m.Currency != other.Currency {
        panic("Currency mismatch")
    }
    return NewMoney(m.Amount+other.Amount, m.Currency)
}

Entity:Order

package entity

type Order struct {
    ID           string
    Items        []OrderItem
    TotalAmount  *valueobject.Money
    CreatedAt    time.Time
}

// Orderに紐づくアイテム
type OrderItem struct {
    ID        string
    Quantity  int
    Price     *valueobject.Money
}

// コンストラクタ
func NewOrder(id string, items []OrderItem) *Order {
    total := calculateTotalAmount(items)
    return &Order{
        ID:          id,
        Items:       items,
        TotalAmount: total,
        CreatedAt:   time.Now(),
    }
}

// 合計金額を計算
func calculateTotalAmount(items []OrderItem) *valueobject.Money {
    total := valueobject.NewMoney(0, "JPY")
    for _, item := range items {
        total = total.Add(item.Price)
    }
    return total
}

// 新しいアイテムを注文に追加
func (o *Order) AddItem(item OrderItem) {
    o.Items = append(o.Items, item)
    o.TotalAmount = o.TotalAmount.Add(item.Price)
}

使用例

package main

func main() {
    item1 := entity.OrderItem{
        ID:        "item1",
        Quantity:  1,
        Price:     valueobject.NewMoney(100, "JPY"),
    }
    item2 := entity.OrderItem{
        ID:        "item2",
        Quantity:  2,
        Price:     valueobject.NewMoney(200, "JPY"),
    }

    order := entity.NewOrder("order1", []entity.OrderItem{item1, item2})

    // 新しいアイテムを追加
    newItem := entity.OrderItem{
        ID:        "item3",
        Quantity:  3,
        Price:     valueobject.NewMoney(300, "JPY"),
    }
    order.AddItem(newItem)
}

この設計のポイント

  1. Value Object(Money)の役割
    • 金額や通貨の正当性を保証(例:異なる通貨の加算を禁止)
    • 計算ロジックをカプセル化
    • OrderItemOrderで再利用可能
  2. Entity(Order)の役割
    • 一意の識別子を持つ
    • ライフサイクルを通じて変化(アイテムの追加、合計金額の更新)
    • ドメインロジックを扱う(合計金額の計算など)

Discussion