บทที่ 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

อ่านเพิ่มเติม