Value Objects
ถ้า EntityEntityอ็อบเจ็กต์ที่มี “ตัวตน” (identity) ต่อเนื่องตามเวลา แม้ค่าภายในเปลี่ยน ก็ยังเป็นสิ่งเดิม เช่น ลูกค้า, คำสั่งซื้อ — แยกแยะด้วย id ไม่ใช่ด้วยค่าTactical Design เปรียบเหมือน “ตัวละคร” ในหนัง สิ่งที่ทำให้ตัวละครน่าสนใจมักเป็น คุณลักษณะ ของมัน — James Bond มีเสน่ห์สูง, BankAccount น่าสนใจเพราะมี Balance ก้อนโต การจำลองคุณลักษณะเชิงพรรณนาเหล่านี้คือหน้าที่ของ building block ตัวหนึ่งของ DDD ที่ชื่อว่า Value ObjectValue Objectอ็อบเจ็กต์ที่สำคัญที่ “ค่า” ของมัน ไม่มีตัวตนเฉพาะ สองตัวที่ค่าเท่ากันถือว่าเท่ากัน ควรเป็น immutable (เปลี่ยนค่าไม่ได้) เช่น เงิน, วันที่, พิกัดTactical Design
Value ObjectValue Objectอ็อบเจ็กต์ที่สำคัญที่ “ค่า” ของมัน ไม่มีตัวตนเฉพาะ สองตัวที่ค่าเท่ากันถือว่าเท่ากัน ควรเป็น immutable (เปลี่ยนค่าไม่ได้) เช่น เงิน, วันที่, พิกัดTactical Design คืออ็อบเจ็กต์ที่ ไม่มี identity มันมีไว้เพื่ออธิบายคุณลักษณะของ entity ที่เกี่ยวข้องกับ domain ซึ่งมักอยู่ในรูปของ “ปริมาณ” อย่างใดอย่างหนึ่ง การไม่มี identity ให้ต้องคอยจัดการ ทำให้การทำงานกับ value object เบาและตรงไปตรงมา — โดยเฉพาะคุณสมบัติสองข้อคือ เปลี่ยนค่าไม่ได้ (immutableImmutableเปลี่ยนแปลงไม่ได้หลังสร้าง ถ้าจะ “เปลี่ยนค่า” ต้องสร้างอ็อบเจ็กต์ใหม่แทน หลักการสำคัญของ Value Object เพื่อเลี่ยง bug จากการใช้อ้างอิงร่วมกันTactical Design) และ นำมารวมกันได้ (combinable) บทนี้จะพาคุณรู้จักคุณสมบัติเหล่านี้ พร้อม modeling pattern ที่ใช้บ่อย และแนวทางการ persist ลงฐานข้อมูล
เมื่อไรจึงควรใช้ Value Object
หัวข้อที่มีชื่อว่า “เมื่อไรจึงควรใช้ Value Object”Value ObjectValue Objectอ็อบเจ็กต์ที่สำคัญที่ “ค่า” ของมัน ไม่มีตัวตนเฉพาะ สองตัวที่ค่าเท่ากันถือว่าเท่ากัน ควรเป็น immutable (เปลี่ยนค่าไม่ได้) เช่น เงิน, วันที่, พิกัดTactical Design คือ สถานะ ของ entity — มันพรรณนาบางอย่างเกี่ยวกับ entity หรือสิ่งที่ entity นั้นครอบครอง เรือมี “ความจุบรรทุกสูงสุด”, ร้านค้ามี “ระดับสินค้าคงคลัง”, รายงานการเงินมี “ยอดหมุนเวียนรายไตรมาส” สังเกตว่าแต่ละตัวอย่างแทน แนวคิดที่มีการวัด ขนาด หรือค่า — จึงเรียกว่า value object เหตุผลหลักที่ทำให้มันสำคัญมีสองข้อ
1. แทนแนวคิดเชิงพรรณนาที่ไม่มี identity
หัวข้อที่มีชื่อว่า “1. แทนแนวคิดเชิงพรรณนาที่ไม่มี identity”ลองมองอย่างตั้งใจ คุณจะเห็นว่าแนวคิดอย่าง Money หรือ Temperature นั้น “ไม่สมเหตุสมผลที่จะมี identity” — มีแต่ entity เท่านั้นที่ควรมี identity ตัวอย่างคลาสสิกคือบัญชีธนาคาร: คุณค้นหา BankAccount ด้วย ID แน่นอน แต่คุณจะค้นหา “ยอดเงิน (balance)” ของมันด้วย ID ไหม? ในเกือบทุก domain คำตอบคือไม่ เพราะยอดเงินไม่มีความหมายเมื่ออยู่โดดเดี่ยว
เมื่อแนวคิดหนึ่งดู ไม่มี identity ที่ชัดเจน นั่นคือเบาะแสสำคัญว่ามันควรเป็น value object ใน model ของคุณ ตามปกติเราจะ ค้นพบ entity ก่อน แล้วจึงค่อยมองออกว่ามี value object ใดบ้างที่เกี่ยวข้องกับมัน — value object ต้องอาศัย context ของ entity จึงจะมีความหมาย
2. ทำให้ code สื่อความชัดเจนขึ้น (explicitness)
หัวข้อที่มีชื่อว่า “2. ทำให้ code สื่อความชัดเจนขึ้น (explicitness)”DDD คือเรื่องของการ สื่อกฎธุรกิจและตรรกะ domain อย่างชัดแจ้ง ในทางกลับกัน primitive type อย่าง int หรือ string เดี่ยวๆ ทำสิ่งนี้ได้แย่มาก ลองดูราคาประมูลที่แทนด้วย int:
public class WinningBid{ public int Price { get; private set; } // ราคาคืออะไรกันแน่? หน่วยอะไร? ช่วงค่าที่ยอมรับเท่าไร?}การใช้ int แทนราคาเป็นการออกแบบที่อ่อน เพราะมันไม่บอกว่า “ราคา” ใน domain นี้คืออะไร ไม่จำกัดช่วงค่าที่อนุญาต ไม่บอกหน่วยหรือสกุลเงิน — กลายเป็นแหล่งความกำกวมขนาดใหญ่ที่ซ่อนรายละเอียดสำคัญของ domain เอาไว้ ยิ่งกว่านั้น คุณ เพิ่มพฤติกรรมให้ primitive ไม่ได้ ตรรกะที่เกี่ยวข้องกับราคาจึงกระจัดกระจายไปทั่ว แทนที่จะอยู่รวมกันเป็นที่เป็นทาง
การห่อ primitive ด้วย value object ช่วยให้ระบบ type ทำงานแทนเรา และดึงพฤติกรรมที่เกี่ยวข้องมารวมกันอย่าง cohesive:
public class Price{ public Price(Money amount) { if (amount == null) throw new ArgumentNullException("Amount cannot be null"); Amount = amount; }
public Money Amount { get; private set; }
// กฎการเพิ่มราคาประมูล (bid increment) ขึ้นกับช่วงราคาปัจจุบัน public Money BidIncrement() { /* ... */ }
// ราคาใหม่ที่เสนอมา “เกิน” ราคาปัจจุบันพอจะแซงได้ไหม public bool CanBeExceededBy(Money offer) { return offer.IsGreaterThanOrEqualTo(BidIncrement()); }}ทุกครั้งที่มีการพูดถึง “ราคา” นักพัฒนาและ domain expert ต้องดูแค่ class เดียวก็เข้าใจว่าราคาคืออะไรและมีกฎอะไรบังคับอยู่
ในระบบ e-commerce ราคาสินค้า ยอดรวมตะกร้า ค่าจัดส่ง และส่วนลด ล้วนเป็น Money — เราไม่เก็บเป็น decimal ลอยๆ เพราะนั่นเปิดช่องให้ bug เช่น “บวกเงินบาทกับเงินดอลลาร์” การห่อด้วย Money ที่รู้จักสกุลเงินของตัวเอง ทำให้กฎ “บวกได้เฉพาะสกุลเดียวกัน” ถูกบังคับในที่เดียว
ในบทถัดไป (Entities) เราจะเห็นว่าการดันพฤติกรรมลงใน value object ช่วยให้ entity โฟกัส — ถ้าไม่ทำ พฤติกรรมเรื่องราคาทั้งหมดคงถูกยัดเข้าไปใน WinningBid จนมันบวมและรับผิดชอบหลายอย่างเกินไป
คุณสมบัติที่นิยาม Value Object
หัวข้อที่มีชื่อว่า “คุณสมบัติที่นิยาม Value Object”สิ่งที่ทำให้ value object ทำงานด้วยง่าย คือมันค่อนข้าง self-contained เช่นเดียวกับผู้ที่นิยม functional programming ชาว DDD ก็ชอบ value object เพราะมัน immutable, ไม่มี side effect และทดสอบง่าย มาดูคุณสมบัติเหล่านี้ทีละข้อ
ไม่มี identity (Identity-Less)
หัวข้อที่มีชื่อว่า “ไม่มี identity (Identity-Less)”รายละเอียดสำคัญที่สุดที่ต้องจำคือ value object ไม่มี identityIdentity“ตัวตน” ที่ทำให้ Entity เป็นสิ่งเดิมตลอดอายุ แม้ค่าเปลี่ยน เช่น เลขที่คำสั่งซื้อ หรือ id ที่ระบบสร้างให้Tactical Design มันสำคัญเพราะมันบอกบางอย่างเกี่ยวกับอ็อบเจ็กต์อื่น: Person สูงเท่าไร? InsurancePolicy ไม่มีเคลมมากี่ปีแล้ว? Fruit หนักเท่าไร? — ลองคิดดูว่าการให้ค่าเหล่านี้มี identity นั้นไม่สมเหตุสมผลเลย
บาง strategy การ persist อาจใส่ ID ให้ value object ในฐานข้อมูล แต่นั่นเป็น รายละเอียดเชิงเทคนิคของการจัดเก็บ ล้วนๆ ไม่ได้แปลว่ามันมี identity ใน domain อย่าสับสนสองเรื่องนี้
ความเท่ากันแบบ attribute-based
หัวข้อที่มีชื่อว่า “ความเท่ากันแบบ attribute-based”Entity ถือว่าเท่ากันเมื่อมี ID เดียวกัน ในทางตรงข้าม value object ถือว่าเท่ากันเมื่อมีค่าเดียวกัน ไม่ว่าตัวแปรสองตัวจะชี้ไปยังอ็อบเจ็กต์เดียวกันหรือคนละตัวก็ตาม ถ้า Temperature สองตัวแทน 30 องศาเซลเซียส ก็ถือว่าเท่ากัน
public override bool Equals(object obj){ var m = obj as Meters; if (m == null) return false; // เทียบ “ค่า” ไม่ใช่ reference — ปัดเศษสองตำแหน่งตามกฎ domain return ToTwoDecimalPlaces(m.DistanceInMeters) == ToTwoDecimalPlaces(DistanceInMeters);}โดยปริยาย C# (และ Java) ถือว่าสองอ็อบเจ็กต์เท่ากันเมื่อ reference ชี้ที่เดียวกัน แต่กับ value object เราไม่สนใจ reference — เราสนใจว่ามันแทน ค่าเดียวกันใน domain หรือไม่ การ override Equals() ซ้ำในทุก class นั้นน่าเบื่อและเสี่ยงผิดพลาด จึงนิยมทำ base class มาเก็บ boilerplate ของ equality, GetHashCode(), และ operator == / != ไว้ที่เดียว แล้วให้ class ลูกบอกแค่ว่า “เอา attribute ไหนมาเทียบ”:
public abstract class ValueObject<T> where T : ValueObject<T>{ // class ลูกบอกว่าจะนำ field ใดมาร่วมตรวจความเท่ากัน protected abstract IEnumerable<object> GetAttributesToIncludeInEqualityCheck();
public bool Equals(T other) => other != null && GetAttributesToIncludeInEqualityCheck() .SequenceEqual(other.GetAttributesToIncludeInEqualityCheck()); // == , != , GetHashCode() ทำให้ครบในที่นี้ครั้งเดียว}บางภาษารองรับ class แบบ value object มาในตัว เช่น case class ของ Scala มี attribute-based equality ในตัว, record ใน C#/Java ก็ทำนองเดียวกัน — คุ้มค่าที่จะสำรวจว่าภาษาที่คุณใช้มีอะไรให้บ้าง
Behavior-rich
หัวข้อที่มีชื่อว่า “Behavior-rich”value object ควร เปิดเผยพฤติกรรมที่สื่อภาษา domain และห่อหุ้มสถานะไว้ เท่าที่ทำได้ ดู class Meters ที่เปิด method ToYards(), ToKilometers(), Add(), IsLongerThan() ขณะเดียวกันก็ซ่อน field DistanceInMeters ไว้ กฎทั่วไปคือ primitive ทุกตัวควรเป็น private/protected โดยปริยาย จะเปิด public ก็ต่อเมื่อมีเหตุผลที่ดีจริงๆ แต่ก่อนอื่นให้ลองเพิ่ม method ที่ให้ function ที่ต้องการแทน — การโฟกัสที่พฤติกรรมเพื่อสร้าง domain model ที่อุดมด้วยพฤติกรรมRich Domain Modelmodel domain ที่ “รวยพฤติกรรม” — object ที่เก็บทั้งข้อมูลและ business logic ไว้ด้วยกัน เป็นคู่ตรงข้ามของ Anemic Domain Model (ที่มีแต่ getter/setter แล้วผลัก logic ไปไว้ที่อื่น) tactical patterns ของ DDD จะคุ้มค่าก็ต่อเมื่อ model “รวย” พอเช่นนี้Tactical Design คือหัวใจของ DDD
Cohesive
หัวข้อที่มีชื่อว่า “Cohesive”ในฐานะแนวคิดเชิงพรรณนาที่มักอธิบาย “ปริมาณ” value object มักห่อ ค่าที่วัด กับ หน่วยที่วัด ไว้ด้วยกันอย่าง cohesive เช่น Height ห่อขนาดกับหน่วย, Money ห่อจำนวนเงินกับสกุลเงิน แต่ก็ไม่จำเป็นต้องเป็นคู่ค่า-หน่วยเสมอไป — Color อาจมี Red, Green, Blue รวมกันเป็นแนวคิดเดียว
Immutable
หัวข้อที่มีชื่อว่า “Immutable”เมื่อสร้างแล้ว value object เปลี่ยนค่าไม่ได้อีก หากต้องการ “เปลี่ยนค่า” ให้สร้าง instance ใหม่ที่มีค่าตามต้องการแทน เพราะ immutabilityImmutableเปลี่ยนแปลงไม่ได้หลังสร้าง ถ้าจะ “เปลี่ยนค่า” ต้องสร้างอ็อบเจ็กต์ใหม่แทน หลักการสำคัญของ Value Object เพื่อเลี่ยง bug จากการใช้อ้างอิงร่วมกันTactical Design ทำให้ใช้เหตุผลกับ code ได้ง่ายและมี side effect อันตรายน้อยลง ตัวอย่างคลาสสิกคือ DateTime ของ .NET — AddMonths() และ AddYears() ดูเหมือนจะแก้ค่า แต่จริงๆ คืนอ็อบเจ็กต์ใหม่ ขณะที่ตัวเดิมไม่เปลี่ยน
public class Money : ValueObject<Money>{ protected readonly decimal Value;
public Money(decimal value) { Value = value; }
// ดูเหมือนแก้ค่า แต่ที่จริงคืน instance ใหม่ ตัวเดิมไม่ถูกแตะ public Money Add(Money money) => new Money(Value + money.Value);
public Money Subtract(Money money) => new Money(Value - money.Value);}Combinable
หัวข้อที่มีชื่อว่า “Combinable”ค่ามักถูกแทนด้วยตัวเลข จึงนำมา รวมกันเป็นค่าใหม่ ได้บ่อย ดังที่เห็น Money + Money ได้จำนวนใหม่ ความสามารถนี้เป็นลักษณะเด่นของ value object — เวลา domain expert พูดถึง “การเอาสองสิ่งมารวมกัน” นั่นเป็นสัญญาณชัดว่าคุณอาจต้องจำลองมันเป็น value object หลายภาษาเปิดให้คุณ override operator + - * ได้ ทำให้ code สื่อความชัดเจนยิ่งขึ้น (แต่ผลลัพธ์ยังเป็น instance ใหม่เสมอ ตามกฎ immutable)
ในระบบ Cargo Shipping มี value object อยู่หลายตัว: Route SpecificationRoute SpecificationValue Object ที่ระบุ “ความต้องการของลูกค้า”: ต้นทาง ปลายทาง และกำหนดเวลาถึง — มี method isSatisfiedBy() ตรวจว่าแผนเดินทางตรงตามนี้หรือไม่Tactical Design (ต้นทาง/ปลายทาง/กำหนดถึง) และ ItineraryItineraryValue Object ที่เป็น “แผนการเดินทางจริง” ของ Cargo ประกอบด้วยลำดับของ Leg (ช่วงการขนส่งแต่ละช่วง)Tactical Design (แผนเดินทางจริงที่ประกอบด้วยลำดับของ Leg) ทั้งคู่ ไม่มี identity — เราไม่ค้นหา “แผนเดินทาง” ด้วย ID แต่สนใจว่าแผนสองอันมีค่าเหมือนกันไหม และมันเป็น immutable: เมื่อ re-route เราสร้าง Itinerary ใหม่ ไม่ใช่ไปแก้ของเดิม
Self-validating
หัวข้อที่มีชื่อว่า “Self-validating”value object ต้องไม่อยู่ในสถานะที่ไม่ถูกต้องเด็ดขาด และมันรับผิดชอบรับประกันสิ่งนี้เอง ในทางปฏิบัติคือ constructor ต้องโยน exception ถ้า argument ผิดกฎ domain เช่น Money ในระบบ e-commerce อาจมีกฎว่า “ทศนิยมไม่เกินสองตำแหน่ง” และ “ต้องไม่ติดลบ”:
public Money(decimal value){ // ตรวจตั้งแต่แรกเกิด — ผิดกฎเมื่อไร abort ทันที if (value % 0.01m != 0) throw new MoreThanTwoDecimalPlacesInMoneyValueException(); if (value < 0) throw new MoneyCannotBeANegativeValueException(); Value = value;}อีกแนวทางคือตรวจใน factory methodFactoryตัวที่ห่อหุ้มตรรกะการ “สร้าง” Aggregate ที่ซับซ้อน และรับประกันว่าอ็อบเจ็กต์ใหม่ถูกต้องตาม invariant ตั้งแต่แรกเกิด — “สร้างของใหม่” (repository หา “ของเก่า”)Tactical Design แบบ static Money Create(decimal amount) ซึ่งเหมาะเมื่อ value object สร้างได้ในหลายสถานะตาม context (เช่นบาง context ยอมให้เงินติดลบได้) แต่ข้อควรระวังคือ factory method ทำให้ เลี่ยงการ validate ได้ ด้วยการเรียก new ตรงๆ — ถ้ากฎ validate ต่างกันตาม context จริงๆ ควรพิจารณา micro types (ด้านล่าง)
Testable
หัวข้อที่มีชื่อว่า “Testable”immutability + cohesion + combinability ทำให้ value object ทดสอบง่ายในภาษาที่สื่อ domain ตรงๆ — immutability ตัดความจำเป็นต้องใช้ mock หรือตรวจ side effect, cohesion ให้ทดสอบแนวคิดเดียวแบบ isolation ได้, combinability ให้แสดงความสัมพันธ์ระหว่างค่าได้
ในโลก functional programming คำหรูที่ใช้เรียก method ที่ไร้ side effect คือ referential transparency — value object โดยเนื้อแท้แล้วเป็นแนวคิดเชิง functional (หรืออย่างน้อยก็ใกล้เคียงมาก) การศึกษา F#, Scala หรือ Haskell คุ้มค่าต่อการเข้าใจแนวคิดนี้ลึกขึ้น
Modeling Pattern ที่พบบ่อย
หัวข้อที่มีชื่อว่า “Modeling Pattern ที่พบบ่อย”ตลอดหลายปี ชาว DDD รวบรวม pattern เล็กๆ ที่ช่วยให้ทำงานกับ value object ได้ดีขึ้น ส่วนใหญ่เพื่อเพิ่มความชัดเจนและสื่อความ มาดูสามตัวที่ใช้ได้ทันที
Static Factory Methods
หัวข้อที่มีชื่อว่า “Static Factory Methods”ใช้ static factory method ห่อความซับซ้อนของการสร้างอ็อบเจ็กต์ไว้หลัง interface ที่อ่านง่ายกว่า ตัวอย่างเยี่ยมคือ TimeSpan ของ .NET ที่มี FromDays(), FromHours(), FromMilliseconds() ซึ่งสื่อความและกำกวมน้อยกว่า constructor ที่รับ int ห้าตัว
public static Height FromFeet(int feet) => new Height(feet, MeasurementUnit.Feet);public static Height FromMetres(int metres) => new Height(metres, MeasurementUnit.Metres);นอกจากอ่านง่ายแล้ว ยังดูแลรักษาง่ายขึ้น เพราะผู้เรียกไม่ต้องผูกตัวเองกับ enum MeasurementUnit อีกต่อไป
Micro Types (Tiny Types)
หัวข้อที่มีชื่อว่า “Micro Types (Tiny Types)”pattern นี้ดันหลัก “เลี่ยง primitive” ไปอีกขั้น โดยห่อ type ที่สื่อความอยู่แล้ว ด้วย type ที่สื่อความยิ่งกว่า เพื่อเพิ่มความชัดเจนเชิง context ดู domain service คำนวณ OT:
public class OvertimeCalculator{ // ระบบ type บังคับให้ส่ง “ชั่วโมงที่ทำ” กับ “ชั่วโมงตามสัญญา” ถูกตำแหน่ง public OvertimeHours Calculate(HoursWorked worked, ContractedHours contracted) { var overtime = worked.Hours - contracted.Hours; return new OvertimeHours(overtime); }}HoursWorked, ContractedHours, OvertimeHours ล้วนเป็น micro type ที่ห่อ value object Hours อีกที — มันไม่เพิ่มพฤติกรรมหรือสถานะใหม่เลย แต่ทำให้ระบบ type กันความผิดพลาดของมนุษย์ เช่นการสลับลำดับ parameter
micro types ไม่ใช่ best practice ที่ทุกคนเห็นพ้อง บางคนมองว่าเป็นบันไดสู่ code ที่ชัดและประกอบกันได้ดี แต่บางคนมองว่าเป็น “ชั้น indirection ที่ซ้ำซ้อนเกินจำเป็น” — คุณต้องชั่งน้ำหนักเองว่าจะใช้หรือไม่
Collection Aversion
หัวข้อที่มีชื่อว่า “Collection Aversion”ชาว DDD บางคนเชื่อว่าคุณ ไม่ควรมี collection ของ value object เลย เหตุผลคือ collection ของ primitive ไม่สื่อแนวคิด domain และการมี collection ของ value object มักหมายความว่าคุณต้องหยิบสมาชิกเฉพาะตัวด้วย “identity” บางรูปแบบ — ซึ่งขัดกับหลักที่ว่า value object ไม่มี identity
ดูตัวอย่าง Customer ที่มี IEnumerable<PhoneNumber>:
public class Customer{ public IEnumerable<PhoneNumber> PhoneNumbers { get; set; } // เบอร์ไหนเป็นบ้าน? มือถือ? ที่ทำงาน?}มันไม่ชัดว่าเบอร์แต่ละตัวคืออะไร วิธีจำลองที่ตรงกับเจตนามากกว่าคือใช้ โครงสร้าง สื่อแนวคิด domain ที่แท้จริง: ลูกค้ามีเบอร์บ้าน เบอร์มือถือ และเบอร์ที่ทำงาน:
public class PhoneBook : ValueObject<PhoneBook>{ public readonly PhoneNumber HomeNumber, MobileNumber, WorkNumber; // เข้าถึงแต่ละเบอร์ได้โดยไม่ต้อง lookup ด้วย ID — เพราะ value object ไม่มี identity}เจตนาและแนวคิด domain ชัดเจนขึ้นมาก โครงสร้างถูกเข้ารหัสลงในระบบ type และบังคับใช้โดย type system
การ Persist Value Object
หัวข้อที่มีชื่อว่า “การ Persist Value Object”แง่มุมที่ยากที่สุดของการทำงานกับ value object คือการ persist มัน เพราะมันไม่มี identity ของตัวเอง
NoSQL — เก็บฝังไปกับ entity
หัวข้อที่มีชื่อว่า “NoSQL — เก็บฝังไปกับ entity”ฐานข้อมูลแบบ document (เช่น RavenDB, EventStore) ใช้ denormalization ซึ่งเข้ากันได้ดีกับ DDD เพราะ entity ทั้งตัว — บางทีทั้ง AggregateAggregateกลุ่มของ Entity และ Value Object ที่ถูกมองเป็นหนึ่งหน่วยเดียวเพื่อรักษาความถูกต้องของข้อมูล มีขอบเขตชัดเจน และเป็นหน่วยของ transactionTactical Design — ถูกจำลองเป็น document เดียวได้ ปัญหาเรื่อง join, normalize, lazy loading ผ่าน ORM จึงหายไป สำหรับ value object หมายความว่ามันถูกเก็บ ฝังไปกับ entity เลย เช่น Customer กับ Name กลายเป็น JSON document เดียว:
{ "Customer": { "Id": "…", "Name": { "FirstName": "…", "Surname": "…" } }}การฝัง value object ลง document เป็นธรรมเนียมทั่วไป แต่ก็ยังเป็นตัวเลือก — คุณเก็บเป็น document แยกเพื่อเหตุผลด้าน performance ได้ถ้าจำเป็น
SQL — สองทางเลือกใหญ่
หัวข้อที่มีชื่อว่า “SQL — สองทางเลือกใหญ่”ฝั่ง SQL มีธรรมเนียม normalization ที่แข็งแรง ทำให้มีทางเลือกหลากหลายกว่า สรุปเป็นสองแนวทางหลัก: denormalize แบบแบน (เก็บค่าตรงๆ ในตารางของ entity) หรือ normalize แยกตาราง
Flat Denormalization — เก็บค่าของ value object ตรงๆ ด้วยรูปแบบที่เรากำหนดเอง เหมาะเมื่อไม่อยากมีตารางและ join เพิ่ม ตัวอย่างต้นแบบคือ DateTime ที่ SQL เก็บเป็นข้อความ ไม่มีตาราง DateTime แยก เราทำกับ value object ของเราเองได้เช่นกัน โดยสร้าง persistence format ที่ระบุค่าได้ไม่ซ้ำ วิธีหนึ่งคือ override ToString():
public override string ToString() => String.Format("firstName:{0};;surname:{1}", FirstName, Surname);// -> "firstName:Sally;;surname:Smith"เวลาบันทึก คุณบอก ORM ให้เก็บค่าในรูปแบบนี้ (ใน NHibernate ใช้ IUserType.NullSafeSet()) และเวลาโหลด ก็ parse กลับเป็น value object (IUserType.NullSafeGet()) — โดยควรให้ logic การ parse อยู่ใน component กลาง ไม่ปนเข้าไปใน value object เพื่อไม่ให้เรื่อง framework รั่วเข้าไปใน domain

Normalizing into Separate Tables — denormalization เป็น strategy โดยพฤตินัย แต่บางครั้งมาตรฐานบริษัทบังคับให้ normalize หรือเหมาะกว่าด้วยเหตุผล performance (โดยเฉพาะเมื่อ value object มีขนาดใหญ่ที่ไม่อยากโหลดทุกครั้งที่โหลด entity) ในกรณีนี้ entity กับ value object มีตารางของตัวเอง เชื่อมด้วย foreign key:
Join("CustomerName", join =>{ join.KeyColumn("Id"); // ใช้ Id ของ Customer เป็น foreign key join.Component(x => x.Name, c => { c.Map(x => x.FirstName); c.Map(x => x.Surname); });});

NHibernate มีแนวคิด Component, EF Core มี Owned Types ที่ช่วยจัดการ value object ให้โดยไม่ต้องคิด custom format เอง — คุ้มค่าที่จะสำรวจการรองรับเหล่านี้ก่อน เพราะมันประหยัดเวลาได้มาก
สรุปประเด็นสำคัญ
หัวข้อที่มีชื่อว่า “สรุปประเด็นสำคัญ”Value ObjectValue Objectอ็อบเจ็กต์ที่สำคัญที่ “ค่า” ของมัน ไม่มีตัวตนเฉพาะ สองตัวที่ค่าเท่ากันถือว่าเท่ากัน ควรเป็น immutable (เปลี่ยนค่าไม่ได้) เช่น เงิน, วันที่, พิกัดTactical Design คือ building block ที่แทน ค่าเชิงพรรณนา (ขนาด, ปริมาณ, การวัด) โดย ไม่มี identityIdentity“ตัวตน” ที่ทำให้ Entity เป็นสิ่งเดิมตลอดอายุ แม้ค่าเปลี่ยน เช่น เลขที่คำสั่งซื้อ หรือ id ที่ระบบสร้างให้Tactical Design — เท่ากันด้วยค่า (immutableImmutableเปลี่ยนแปลงไม่ได้หลังสร้าง ถ้าจะ “เปลี่ยนค่า” ต้องสร้างอ็อบเจ็กต์ใหม่แทน หลักการสำคัญของ Value Object เพื่อเลี่ยง bug จากการใช้อ้างอิงร่วมกันTactical Design + attribute-based equality), รวมกันได้ (combinable), ตรวจสอบความถูกต้องของตัวเอง (self-validating) และควร behavior-rich โดยห่อ primitive ให้สื่อแนวคิด domain จำไว้ว่าสิ่งที่เป็น value object ใน domain หนึ่ง อาจเป็น entity ในอีก domain หนึ่งได้ และเลือก persist ได้ทั้งแบบ denormalize (ฝังกับ entity) หรือ normalize (แยกตาราง/document)
เจาะลึกแนวคิดในบทนี้ต่อได้ที่คลังอ้างอิง DevIQ:
- Value Object — นิยามและคุณสมบัติของ value object โดยตรง ครอบคลุมประเด็นเดียวกับที่บทนี้อธิบาย
- Entity — ตัวเทียบเคียงที่สำคัญที่สุดของ value object เข้าใจ identity ของ entity ให้ชัด จะช่วยให้แยกได้ทันทีว่าแนวคิดไหนควรเป็น value object
- Factory Method — pattern เบื้องหลัง static factory method (เช่น
Height.FromFeet()) ที่บทนี้แนะนำให้ใช้แทน constructor ตรงๆ
เช็กความเข้าใจ — บทที่ 15
ข้อ 1 / 4อะไรคือความแตกต่างหลักระหว่าง Entity กับ Value Object?