Entity
object ที่มี identity เฉพาะตัว แยกจากค่าของ property
Entity คือ object ที่มี identity เฉพาะตัวในตัวมันเอง ซึ่งคงอยู่ตลอดช่วงชีวิตของมัน แยกต่างหากจากสถานะหรือ property ที่เหลือ แม้ property จะเหมือนกับ instance อื่นของชนิดเดียวกันทุกประการ Entity สองตัวก็ยังคงแตกต่างกันเพราะ identity ที่ไม่ซ้ำใคร Eric Evans นิยามไว้ตรงไปตรงมาว่า “an object primarily defined by its identity is called an Entity” — object ที่ถูกนิยามหลัก ๆ ด้วย identity ของมันเอง ไม่ใช่ด้วยค่าที่มันถืออยู่
ตัวอย่างคลาสสิกคือ class Person ที่มีชื่อ นามสกุล และวันเกิด — 2 Person อาจมีชื่อและวันเกิดตรงกันได้ แต่นั่นไม่ได้ทำให้เป็นคนคนเดียวกัน! ตรงข้ามกับที่อยู่ไปรษณีย์ ซึ่งหากทุก property เหมือนกันก็ถือเป็นสิ่งเดียวกัน จึงเหมาะจะจำลองเป็น Value Object แทน Entity มีหน้าที่ห่อหุ้มสถานะและพฤติกรรมของตน เพื่อรับประกัน domain logic ที่สอดคล้องกันและความถูกต้องของข้อมูลตลอดวงจรชีวิต
Martin Fowler อธิบายแนวคิดนี้ในการจัดหมวดหมู่ของ Evans (Evans Classification) ว่า Entity คือ object ที่มี “a distinct identity that runs through time and different representations” — identity เดียวกันอาจถูกแทนด้วยหลายรูปแบบ (แถวในฐานข้อมูล, JSON payload, หรือ object ในหน่วยความจำ) แต่ยังคงเป็นสิ่งเดียวกันเสมอ ข้อสังเกตสำคัญคือ Entity ไม่ override การเปรียบเทียบ equality ด้วยค่า — มันเปรียบเทียบด้วย identity เพียงอย่างเดียว
Entity ถูกแยกแยะด้วย Id ไม่ใช่ด้วยค่าของ property
บทบาทใน DDD
หัวข้อที่มีชื่อว่า “บทบาทใน DDD”ใน Domain-Driven Design, Entity คือหนึ่งใน building block หลักของ Domain Model ควบคู่กับ Value Object และ Domain Service เอกสารของ Microsoft Learn อธิบายว่า Entity ถูกนิยามหลักด้วย identity, ความต่อเนื่อง (continuity) และการคงอยู่ (persistence) ตลอดเวลา ไม่ใช่ด้วย attribute ที่ประกอบกันขึ้นมา และเน้นย้ำว่า Entity ใน model ที่ดีต้อง implement behavior ควบคู่กับ data ไม่ใช่แค่เก็บ property เฉย ๆ — มิฉะนั้นจะกลายเป็น Anemic Model ที่ผลักภาระ business logic ไปไว้ที่ service layer จนกลายเป็น code สไตล์ procedural
ประเด็นที่ต้องออกแบบอย่างระมัดระวังคือ กลยุทธ์การสร้าง identity Vaughn Vernon ใน Implementing Domain-Driven Design จำแนกไว้หลายแบบ เช่น ผู้ใช้เป็นคนกำหนด (user provides identity), application สร้างให้ (application generates identity), persistence mechanism สร้างให้ตอนบันทึก (เช่น auto-increment ของฐานข้อมูล), หรือ Bounded Context อื่นเป็นคนกำหนดมาให้ — การเลือกจังหวะสร้าง Id (ก่อนหรือหลังบันทึกลง repository) ส่งผลต่อว่า object จะมี identity ที่สมบูรณ์ตั้งแต่ตอนสร้างหรือไม่
Entity มักไม่ได้อยู่โดดเดี่ยว แต่ถูกจัดกลุ่มเข้ากับ Entity และ Value Object อื่น ๆ ภายใน Aggregate โดยมี Entity ตัวหนึ่งทำหน้าที่เป็น Aggregate Root — จุดเดียวที่อนุญาตให้เข้าถึงและแก้ไขสมาชิกภายใน aggregate ได้ เพื่อรักษา invariant ให้สอดคล้องกันเสมอ Vernon อธิบายไว้ใน Effective Aggregate Design ว่าการเลือกว่าอะไรควรเป็น Entity ลูกภายใน aggregate กับอะไรควรแยกเป็น aggregate ของตัวเองต่างหาก ขึ้นอยู่กับ consistency boundary ที่แท้จริง ของ business invariant ไม่ใช่ความสะดวกในการจัดกลุ่ม object
ข้อควรระวังอีกอย่างคือ identity ของ Entity เดียวกันอาจปรากฏข้าม Bounded Context ได้ (เช่น buyer ในระบบสั่งซื้อ กับ user ในระบบ identity อาจใช้ Id เดียวกัน) แต่ attribute และ behavior ที่ context แต่ละแห่งสนใจอาจต่างกันโดยสิ้นเชิง — Entity จึงถูกจำกัดขอบเขตให้มีเฉพาะสิ่งที่ Bounded Context นั้นต้องใช้จริง
classDiagram
class Entity {
+Id
+Equals by Id
}
class ValueObject {
+Equals by value
+Immutable
}
class AggregateRoot
class Order
class OrderItem
class Address
Entity <|-- AggregateRoot
AggregateRoot <|-- Order
Entity <|-- OrderItem
Order o-- OrderItem
Order o-- Address
ValueObject <|-- Address
แผนภาพนี้แสดงว่า Order เป็น Aggregate Root ที่สืบทอดคุณสมบัติ Entity มา ภายในมันคุม OrderItem (Entity ลูก มี Id ของตัวเอง) และ Address (Value Object เปรียบเทียบด้วยค่า ไม่มี identity)
ตัวอย่าง
หัวข้อที่มีชื่อว่า “ตัวอย่าง”public class ToDoItem : EntityBase{ public int Id { get; private set; } // identity เฉพาะตัว public string Title { get; private set; } public bool IsDone { get; private set; }
public void MarkComplete() => IsDone = true; // พฤติกรรมอยู่ในตัว entity}ตัวอย่างที่ลึกขึ้น — Entity ที่ override equality ให้เปรียบเทียบด้วย Id เท่านั้น พร้อมป้องกัน invariant ผ่าน constructor และ method (แนวทางนี้สอดคล้องกับที่ eShopOnContainers ของ Microsoft ใช้จริงใน domain การสั่งซื้อ):
public abstract class EntityBase{ public Guid Id { get; protected set; }
// equality ของ entity ตัดสินด้วย Id เท่านั้น ไม่ใช่ด้วยค่า property public override bool Equals(object obj) { if (obj is not EntityBase other) return false; if (ReferenceEquals(this, other)) return true; if (GetType() != other.GetType()) return false; return Id != Guid.Empty && Id == other.Id; }
public override int GetHashCode() => Id.GetHashCode();}
public class Order : EntityBase{ private readonly List<OrderItem> _orderItems = new();
public OrderStatus Status { get; private set; } public IReadOnlyCollection<OrderItem> OrderItems => _orderItems.AsReadOnly();
public Order(Guid id) { Id = id; Status = OrderStatus.Draft; }
// behavior อยู่ในตัว entity ไม่ใช่ใน service ภายนอก public void AddItem(Guid productId, int quantity, decimal unitPrice) { if (Status != OrderStatus.Draft) throw new InvalidOperationException("แก้ไข order ที่ยืนยันแล้วไม่ได้");
_orderItems.Add(new OrderItem(Guid.NewGuid(), productId, quantity, unitPrice)); }
public void Confirm() { if (_orderItems.Count == 0) throw new InvalidOperationException("ยืนยัน order ที่ไม่มีรายการไม่ได้");
Status = OrderStatus.Confirmed; }}
public class OrderItem : EntityBase{ public Guid ProductId { get; private set; } public int Quantity { get; private set; } public decimal UnitPrice { get; private set; }
public OrderItem(Guid id, Guid productId, int quantity, decimal unitPrice) { Id = id; ProductId = productId; Quantity = quantity; UnitPrice = unitPrice; }}สังเกตว่า Order ทำหน้าที่เป็น Aggregate Root ที่ควบคุมการเข้าถึง OrderItem ทั้งหมดผ่าน method อย่าง AddItem แทนที่จะ expose List<OrderItem> ตรง ๆ ให้ client แก้ไขเองได้ตามอำเภอใจ ซึ่งเป็นวิธีป้องกัน invariant ที่ Microsoft Learn เน้นย้ำในเอกสารสถาปัตยกรรม microservice ของตน
ความสัมพันธ์กับแนวคิดอื่น
หัวข้อที่มีชื่อว่า “ความสัมพันธ์กับแนวคิดอื่น”- Value Object — ตรงข้ามกับ Entity โดยสิ้นเชิงในแง่ความหมายของ equality: Value Object เปรียบเทียบด้วยค่า attribute ทั้งหมด และควร immutable ส่วน Entity เปรียบเทียบด้วย Id เพียงอย่างเดียวและเปลี่ยนสถานะได้ตลอดชีวิต การตัดสินใจว่า concept หนึ่งควรเป็น Entity หรือ Value Object ขึ้นอยู่กับ Bounded Context — ที่อยู่อาจเป็น Value Object ในระบบ e-commerce แต่เป็น Entity ในระบบสาธารณูปโภคที่ต้องผูก billing เข้ากับที่อยู่นั้นโดยตรง
- Aggregate — Entity มักถูกจัดกลุ่มเป็น cluster ภายใน Aggregate โดยมี Entity หนึ่งตัวเป็น Aggregate Root ที่ทำหน้าที่เป็นจุดเข้าถึงเดียวเพื่อรักษาความสอดคล้องของ invariant ทั้งกลุ่ม
- Anemic Model — เมื่อ Entity ถูกลดทอนเหลือแค่ getter/setter โดยไม่มี behavior ห่อหุ้มอยู่เลย มันจะเสื่อมสภาพกลายเป็น Anemic Model ซึ่ง Martin Fowler วิจารณ์ว่าเป็นเพียง object เชิงกระบวนการ ไม่ใช่ domain model ที่แท้จริง
- Encapsulation — Entity ที่ดีต้องห่อหุ้มสถานะภายในผ่าน private setter และเปิด method สาธารณะที่บังคับใช้กฎของ domain แทนที่จะปล่อยให้ code ภายนอกเปลี่ยนค่า property ได้โดยตรง
- Domain Model — Entity คือหนึ่งใน building block พื้นฐานที่ประกอบกันขึ้นเป็น Domain Model ร่วมกับ Value Object และ Domain Service