Value Object
type ที่ immutable และแยกแยะได้ด้วยค่าของ property เท่านั้น
Value Object คือ type ที่ immutable (เปลี่ยนแปลงสถานะภายในไม่ได้หลังสร้างเสร็จ) และแยกแยะได้ด้วยค่าของ property เท่านั้น ต่างจาก Entity ที่มี identity เฉพาะตัวติดตามข้ามเวลา — Value Object สองตัวที่มี property เหมือนกันทุกประการถือว่า “เท่ากัน” (equal) โดยไม่สนใจว่าจะเป็นคนละ instance ในหน่วยความจำหรือไม่ แนวคิดนี้ Eric Evans อธิบายไว้เป็นครั้งแรกอย่างเป็นระบบในหนังสือ Domain-Driven Design และ Martin Fowler ก็บันทึกไว้ใน PoEAA (Patterns of Enterprise Application Architecture) ในชื่อเดียวกันตั้งแต่ก่อนหน้านั้น โดยนิยามสั้น ๆ ว่า “a small object that represents an entity whose equality is not based on identity”
ตัวอย่างที่พบบ่อย: จำนวนเงิน (Money — ตัวเลข + สกุลเงิน), ที่อยู่ (Address), ช่วงวันที่ (DateRange), พิกัด (Coordinate), เบอร์โทรศัพท์ Money สองก้อนที่มีจำนวน 100 บาทเท่ากัน ถือว่าเป็นค่าเดียวกันเสมอ ไม่ว่าจะมาจากธุรกรรมไหน — นี่คือสิ่งที่แยก Value Object ออกจาก Entity อย่าง Order หรือ Customer ที่แม้ property จะเหมือนกันทุกตัวแต่ก็ยังถือเป็นคนละรายการถ้า identity ต่างกัน
บทบาทใน DDD
หัวข้อที่มีชื่อว่า “บทบาทใน DDD”ใน DDD tactical patterns Value Object เป็นหน่วยประกอบสำคัญของ Aggregate คู่กับ Entity: Aggregate root มักเป็น Entity ที่มี identity ส่วน attribute จำนวนมากภายในควรถูก “ยก” ออกมาเป็น Value Object แทนที่จะปล่อยเป็น primitive กระจัดกระจาย (ดู Primitive Obsession) เช่น Order entity ที่มี ShippingAddress เป็น Address value object แทนที่จะมี string ห้าตัวลอย ๆ
เหตุผลที่ DDD ผลักดันให้ใช้ Value Object ให้มากที่สุดเท่าที่จะทำได้ (Vaughn Vernon เรียกแนวทางนี้ว่า “prefer Value Objects to Entities”):
- ปลอดภัยจากการแก้ไขข้ามที่ (aliasing bug) — เพราะ immutable, การส่ง Value Object ไปให้ code ส่วนอื่นใช้ร่วมกันจะไม่มีทางถูกแก้ไขจนกระทบที่อื่นโดยไม่ตั้งใจ ต่าง Entity ที่ reference เดียวกันอาจถูกแก้จากหลายจุด
- Side-effect-free function — method บน Value Object ที่ “ดูเหมือนแก้ไข” ที่จริงคืนค่าเป็น instance ใหม่เสมอ (เหมือน
string.ToUpper()ใน .NET ที่ไม่แก้ string เดิม) ทำให้ทดสอบและให้เหตุผลได้ง่ายกว่ามาก เพราะไม่มี state เปลี่ยนแปลงแอบแฝง - Equality ที่ตรงไปตรงมา — เปรียบเทียบด้วยค่ารวมของทุก property ไม่ใช่ reference จึงเทียบ, เก็บใน
HashSet, หรือใช้เป็น key ได้อย่างปลอดภัย - บังคับ invariant ให้ถูกต้องตั้งแต่สร้าง — การรับค่าทั้งหมดผ่าน constructor เดียวเปิดโอกาสให้ validate ค่าที่ผิด (เช่น ZipCode รูปแบบผิด) ตั้งแต่จุดกำเนิด ทำให้ state ที่ผิดกฎ “สร้างไม่ได้เลย” (ดู Make Illegal States Unrepresentable)
ผลคือ model domain ที่มี Entity น้อยลง มี Value Object มากขึ้น ซึ่งมักทำให้ domain model แสดงเจตนาชัดเจนขึ้นและมี bug จากการ mutate โดยไม่ตั้งใจน้อยลง
ตัวอย่าง
หัวข้อที่มีชื่อว่า “ตัวอย่าง”แผนภาพต่อไปนี้แสดง Order (Entity ซึ่งมี identity) ที่ประกอบด้วย Value Object สองชนิด คือ Address และ Money — สังเกตว่า OrderItem ก็ใช้ Money ซ้ำได้เพราะ Value Object ไม่มีเจ้าของแต่ผู้เดียว:
classDiagram class Order class Address class OrderItem class Money Order o-- Address Order o-- OrderItem OrderItem o-- Money Order o-- Money
โครงร่างมาตรฐานของ Value Object ใน C# ต้องรับสถานะทั้งหมดเข้ามาตอนสร้าง (constructor) และ property ต้องเป็น read-only (private set หรือ get-only) เมื่อ immutable แล้ว การ “แก้ไข” value object จึงเทียบเท่ากับการทิ้งของเก่าแล้วสร้างใหม่:
public class Address : ValueObject{ public string Street { get; } public string City { get; }
public Address(string street, string city) // รับค่าครบตอนสร้าง { Street = street; City = city; }}ในทางปฏิบัติ C# ไม่มี equality-by-value ให้ฟรีสำหรับ class ธรรมดา (ต่างจาก record ใน C# 9 ขึ้นไปที่ generate ให้อัตโนมัติ) จึงมักเขียน base class กลางให้ subclass ทุกตัว inherit เพื่อไม่ต้องเขียน Equals/GetHashCode ซ้ำทุกที่ — แนวทางนี้ปรากฏใน eShopOnContainers ของ Microsoft:
public abstract class ValueObject{ protected abstract IEnumerable<object> GetEqualityComponents();
public override bool Equals(object obj) { if (obj == null || obj.GetType() != GetType()) return false;
var other = (ValueObject)obj; return GetEqualityComponents() .SequenceEqual(other.GetEqualityComponents()); }
public override int GetHashCode() { return GetEqualityComponents() .Select(x => x != null ? x.GetHashCode() : 0) .Aggregate((x, y) => x ^ y); }}
public class Money : ValueObject{ public decimal Amount { get; } public string Currency { get; }
public Money(decimal amount, string currency) { Amount = amount; Currency = currency; }
// side-effect-free function: คืนค่าใหม่เสมอ ไม่แก้ this public Money Add(Money other) { if (other.Currency != Currency) throw new InvalidOperationException("สกุลเงินไม่ตรงกัน");
return new Money(Amount + other.Amount, Currency); }
protected override IEnumerable<object> GetEqualityComponents() { yield return Amount; yield return Currency; }}เมื่อ persist ด้วย Entity Framework Core (ตั้งแต่ 2.0 ขึ้นไป) Value Object อย่าง Address มักถูก map เป็น “owned entity type” ผ่าน OwnsOne(o => o.Address) — EF จะสร้าง shadow key ให้เองภายใต้ผ้าคลุม แต่จากมุมมองของ domain model จะไม่มี Id field ปรากฏให้เห็นเลย ซึ่งตรงกับเจตนาของ pattern นี้
ความสัมพันธ์กับแนวคิดอื่น
หัวข้อที่มีชื่อว่า “ความสัมพันธ์กับแนวคิดอื่น”- Entity — คู่ตรงข้ามที่นิยามซึ่งกันและกัน: Entity แยกแยะด้วย identity ที่คงอยู่ตลอดวงจรชีวิตแม้ attribute จะเปลี่ยน ส่วน Value Object แยกแยะด้วยค่าเท่านั้นและไม่ควรมี identity เลย เวลาออกแบบ model คำถามคือ “ต้องติดตาม identity ของสิ่งนี้ข้ามเวลาไหม” ถ้าไม่ต้อง ให้เป็น Value Object
- Aggregate — Aggregate คือกลุ่มของ Entity และ Value Object ที่ถูกปฏิบัติเป็นหน่วยเดียวเพื่อ consistency; Value Object มักเป็นส่วนใหญ่ของ attribute ภายใน aggregate
- Primitive Obsession — code smell ที่ใช้ primitive (string, int) แทนแนวคิด domain ที่ควรห่อเป็น Value Object; การ “เลื่อนขั้น” primitive ให้เป็น Value Object คือวิธีแก้ตรงจุด
- Make Illegal States Unrepresentable — constructor ของ Value Object ที่ validate ครบเป็นกลไกหลักที่ทำให้หลักการนี้เกิดขึ้นจริงใน code
- immutable object (แนวคิดทั่วไปนอก DDD) — Value Object คือการประยุกต์แนวคิด immutability ของ functional programming เข้ากับ domain model; หลักการเดียวกับที่ทำให้
stringใน .NET หรือ tuple ใน F# ปลอดภัยต่อการ share
ที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “ที่เกี่ยวข้อง”- Entity
- Aggregate
- Make Illegal States Unrepresentable
- Single Point of Enforcement
- Encapsulation
- Primitive Obsession
แหล่งอ้างอิง
หัวข้อที่มีชื่อว่า “แหล่งอ้างอิง”- ที่มา · deviq.com/domain-driven-design/value-object
- Domain-Driven Design — Eric Evans
- bliki: Value Object — Martin Fowler
- Value Object — Martin Fowler (PoEAA catalog)
- Implementing value objects — .NET Microservices Architecture, Microsoft Learn
- Implementing Domain-Driven Design (บทที่ 6: Value Objects) — Vaughn Vernon