ข้าม​ไป​ยัง​เนื้อหา

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

ใน 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