🎃
DDDにおけるValue ObjectとEntityについて
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
(金額と通貨情報を持ち、値そのものが重要)
Money
Value Object: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)
}
Order
Entity: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)
}
この設計のポイント
- Value Object(
Money
)の役割- 金額や通貨の正当性を保証(例:異なる通貨の加算を禁止)
- 計算ロジックをカプセル化
-
OrderItem
やOrder
で再利用可能
- Entity(
Order
)の役割- 一意の識別子を持つ
- ライフサイクルを通じて変化(アイテムの追加、合計金額の更新)
- ドメインロジックを扱う(合計金額の計算など)
Discussion