บทที่ 18 · Part 4 — Model Behavior in Code
Entities and Value Objects
แยก identity กับ value semantics และทำ invalid state ให้สร้างยากผ่าน Money, Currency และ Transfer examples
Entities and Value Objects
ORM ทำให้ทุก table มี ID จนทีมเรียกทุก struct ว่า Entity แต่ identity ใน domain ไม่ได้มาจาก primary key อย่างเดียว Transfer Intent ยังเป็นสิ่งเดิมเมื่อ status เปลี่ยน ขณะที่ Money 100 THB ก้อนหนึ่งแทนค่าเท่ากับ อีกก้อนโดยไม่ต้องตามว่า object instance ใด Entity กับ Value Object ช่วย model identity และ value semantics
จบบทนี้คุณจะ
แยก Entity จาก Value Object ด้วย identity/lifecycle/equality ออกแบบ immutable values ที่ทำ invalid state ให้สร้างยาก model Money/Currency โดยไม่ใช้ float และตัดสินว่าความเท่ากัน/identity มีขอบเขต ต่างกันอย่างไร
Entity: Continuity สำคัญ
Entity มี identity ต่อเนื่องตลอด lifecycle แม้ attributes เปลี่ยน:
type TransferIntent struct {
id TransferIntentID
status IntentStatus
beneficiary Beneficiary
amount Money
}
คำถาม identity:
- ใครสร้างและ scope unique ที่ไหน
- identity survive migration/context translation หรือไม่
- external/provider ID เป็น identity เดียวกันหรือ reference
- duplicate request หมายถึง Entity เดิมหรือคนละ Entity
อย่าใช้ provider reference เป็น Transfer Intent ID โดยอัตโนมัติ intent หนึ่งอาจมีหลาย attempts/providers
Value Object: ความหมายอยู่ในค่า
Value Object ไม่มี lifecycle identity ของตน equality มาจาก attributes และมัก immutable
type Money struct {
minorUnits int64
currency Currency
}
func NewMoney(minorUnits int64, currency Currency) (Money, error) {
if currency == "" {
return Money{}, ErrMissingCurrency
}
return Money{minorUnits: minorUnits, currency: currency}, nil
}
func (m Money) Add(other Money) (Money, error) {
if m.currency != other.currency {
return Money{}, ErrCurrencyMismatch
}
return Money{minorUnits: m.minorUnits + other.minorUnits, currency: m.currency}, nil
}
จำนวนเงินใช้ integer minor unit พร้อม Currency ห้าม float และห้ามสมมติทุก currency มี 2 decimals ดู ISO 4217 และ vendor contract ปัจจุบัน
Invalid State ให้สร้างยาก
Value Object เหมาะกับ validation ที่เป็น intrinsic:
- Currency code อยู่ในชุดที่ระบบรองรับ
- Beneficiary reference มีรูปแบบ/context ถูก
- DateRange end ไม่ก่อน start
- Percentage อยู่ใน domain-defined range
แยก intrinsic validity จาก policy eligibility Amount เป็น Money ที่ valid ได้แต่ transfer amount อาจ เกิน policy ซึ่งต้อง owner/version/context ไม่ควรฝัง threshold ปัจจุบันใน Money
Valid Value ไม่ได้แปลว่า Allowed Action
Money{500000, THB} อาจ structurally valid แต่สิทธิ์ส่งจำนวนนี้ขึ้นกับ policy, actor, jurisdiction
และเวลา ให้ policy owner กำหนด source/version/date ไม่ใส่ใน generic constructor
Equality มี Context
Customer identity ใน Onboarding กับ Billing reference อาจใช้ UUID เดียวกันแต่ Entities ต่าง model Value equality ก็มี semantics เช่น Money เท่ากันเมื่อ amount/currency เท่ากัน แต่ FX Quote ต้องรวม rate, pair, source และ timestamp
อย่า implement equality จากทุก persistence field เช่น createdAt อาจไม่ใช่ value semantics
Value Object ไม่ใช่ DTO
DTO ขนข้อมูลและอาจ mutable/invalid ชั่วคราว Value Object ปกป้องความหมายและ behavior
HTTP DTO amount=10000,currency="THB"
-> validate/translate
-> Money value
-> domain behavior
adapter ไม่ควรส่ง DTO vendor เข้า model ตรง ๆ
Entity Behavior ไม่ใช่ Getter Bag
Entity ควรเปิด operations ตาม Ubiquitous Language เช่น Confirm, Expire, CancelRequest
ไม่ใช่ setters ที่สร้าง state combination ผิดได้ หาก behavior อยู่ service หมด Entity อาจ anemic
หรือ domain อาจเรียบง่ายพอไม่ต้อง Rich Model—ให้ดู force
แบบฝึกปฏิบัติ: Identity and Value Table
จำแนก Transfer, TransferIntent, ExecutionAttempt, ProviderReference, Money, Currency,
Beneficiary, PolicyVersion, ReconciliationCase:
Entity / Value / Reference / DTO?
Identity or equality rule:
Lifecycle:
Invalid states prevented:
Context:
External translation:
Counterexample:
เขียน Go ของ Money และ TransferIntent พร้อม tests currency mismatch, repeated confirm และ provider ID ไม่เท่ากับ intent ID
รายการตรวจสอบ
- Entity มี domain identity/lifecycle
- Value equality มาจาก meaningful attributes
- Value Object immutable เมื่อทำได้
- intrinsic validity แยกจาก policy eligibility
- Money ใช้ integer minor unit + Currency
- external ID เป็น reference จนพิสูจน์ว่าเป็น identity
- Entity เปิด behavior แทน unrestricted setters
สรุปบทนี้
Entity กับ Value Object ทำให้ model บอกว่าอะไรต้องตามต่อเนื่องและอะไรมีความหมายด้วยค่า การแยกนี้ช่วยป้องกัน invalid state และ identity confusion โดยเฉพาะ money/provider boundaries แต่ต้องใช้ตาม domain semantics ไม่ใช่ ORM shape
อ่านเพิ่มเติม
- DDD Reference — Entity และ Value Object
- Money pattern — Martin Fowler
- ISO 4217 — currency/minor-unit authority