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

Anemic Model

model “โลหิต​จาง” ที่​มี​แต่​สถานะ ขาด​พฤติกรรม

ใน​การ​เขียน​โปรแกรม​เชิง​วัตถุ โดย​เฉพาะ​ใน DDD object ถูก​เรียก​ว่า anemic (โลหิต​จาง) หาก​มัน มี​สถานะ (state) แต่​ขาด​พฤติกรรม (behavior) ที่​ควร​เป็น​ของ​มัน object บาง​ชนิด​อย่าง DTO ถูก​คาด​หวัง​ให้​เป็น​เพียง​กลุ่ม​ข้อมูล​อยู่​แล้ว ไม่​นับ​ว่า​เป็น​ปัญหา แต่ object ที่​ควร​จำลอง​พฤติกรรม​ของ​ปัญหา​ใน application — โดย​เฉพาะ entity และ aggregate ใน domain model — ควร​ใช้ encapsulation จัดการ​สถานะ​และ​พฤติกรรม​ภายใน​ของ​ตนเอง

Martin Fowler เป็น​ผู้​บัญญัติ​คำ​นี้​ใน​บทความ​ปี 2003 ชื่อ “AnemicDomainModel” เขา​อธิบาย​ว่า​อาการ​เบื้องต้น​ของ anemic domain model คือ “เมื่อ​ดู​ผ่าน ๆ มัน​เหมือน​ของ​จริง” — มี object ที่​ตั้ง​ชื่อ​ตาม​คำ​นาม​ใน domain เชื่อม​โยง​กัน​ด้วย​ความ​สัมพันธ์​ที่​ดู​สมจริง แต่​พอ​มอง​ลึก​เข้าไป​ที่​พฤติกรรม​กลับ​พบ​ว่า​แทบ​ไม่มี​เลย เหลือ​เพียง “ถุง​ใส่ getter/setter” (bags of getters and setters) พฤติกรรม​ทั้งหมด​ถูก​ดึง​ไป​ไว้​ใน service object ชุด​หนึ่ง (มัก​เรียก​ว่า business layer) ซึ่ง​วาง​อยู่​เหนือ data model และ​ใช้​มัน​เป็น​เพียง​ข้อมูล​ดิบ

Fowler ชี้​ว่า​นี่​คือ “ความ​สยดสยอง​พื้นฐาน” (fundamental horror) ของ anti-pattern นี้ เพราะ​มัน​ขัด​กับ​แนวคิด​พื้นฐาน​ของ​การ​ออกแบบ​เชิง​วัตถุ​โดยตรง — ซึ่ง​คือ​การ​รวม data กับ process เข้า​ด้วย​กัน การ​แยก​มัน​ออก​จาก​กัน​ทำให้​ได้ design แบบ procedural ที่​แฝง​ตัว​อยู่​ใน​คราบ OO

ใน DDD tactical design เป้าหมาย​คือ​ให้​แต่ละ Bounded Context มี domain model เดียว​ที่​เชื่อม​โยง​กัน​แน่นหนา ครอบคลุม​กฎ พฤติกรรม ภาษา​ทาง​ธุรกิจ และ​ข้อ​จำกัด​ของ​บริบท​นั้น เอกสาร​ของ Microsoft ระบุ​ชัดเจน​ว่า “domain entity ใน DDD ต้อง​นำ domain logic หรือ behavior ที่​เกี่ยวข้อง​กับ​ข้อมูล​ของ entity นั้น​ไป​ใช้งาน​ด้วย” เช่น entity Order ต้อง​มี business logic และ operation อย่าง​การ​เพิ่ม​รายการ​สินค้า การ​ตรวจสอบ​ข้อมูล และ​การ​คำนวณ​ยอด​รวม เป็น method ของ​ตัว​มัน​เอง ไม่ใช่​กระจาย​กฎ​เหล่า​นั้น​ไป​ไว้​ที่ application layer

เมื่อ logic ทั้งหมด​ถูก​ดึง​ออก​จาก entity ไป​ไว้​ที่ service class แทน นั่น​คือ​สัญญาณ​ของ anemic domain model ผล​ที่​ตาม​มา​คือ:

  • สูญเสีย​ประโยชน์​ของ domain model แต่​ยัง​แบก​ต้นทุน — ทีม​ยัง​ต้อง​จัดการ​ความ​ซับซ้อน​ของ O/R mapping และ database abstraction แต่​ไม่​ได้​พลัง​ของ​องค์กร​ที่ domain model ที่แท้​จริง​ควร​มอบ​ให้
  • กลาย​เป็น Transaction Script โดย​ไม่รู้ตัว — เมื่อ logic ทั้งหมด​อยู่​ใน service ก็​แทบ​ไม่​ต่าง​จาก​การ​เขียน script เชิง​กระบวนการ​ที​ละ use case ซึ่ง​เป็น​สไตล์​ที่​ตรง​ข้าม​กับ domain model
  • ละเมิด invariant ได้​ง่าย — เพราะ​ไม่มี object ใด​รับผิดชอบ​รักษา​ความ​ถูกต้อง​ของ​ตัวเอง (self-validating) ผู้​เรียก​ภายนอก​สามารถ set สถานะ​ผ่าน setter ให้​อยู่​ใน​สภาพ​ที่​ผิด​กฎ​ธุรกิจ​ได้​ตลอด​เวลา

อย่างไร​ก็ตาม Microsoft Learn ก็​ยอมรับ​ว่า anemic model ไม่ใช่ anti-pattern เสมอ​ไป — ถ้า microservice หรือ Bounded Context นั้น​เป็น​เพียง CRUD service ง่าย ๆ ที่​ไม่มี​กฎ​ธุรกิจ​ซับซ้อน การ​มี entity ที่​เป็น​แค่ data property ล้วน ๆ ก็​เพียงพอแล้ว และ​ถือ​เป็น persistence model ที่​ตั้งใจ​ออกแบบ​มา​แบบ​นั้น สถาปัตยกรรม microservices จึง​มัก​ผสม​ทั้ง​สอง​แบบ​ใน​ระบบ​เดียวกัน​ได้​ตาม​ความ​ซับซ้อน​ของ​แต่ละ Bounded Context

แผนภาพ​ต่อ​ไป​นี้​แสดง​ความ​สัมพันธ์​ระหว่าง Anemic Model กับ​แนวคิด​ข้าง​เคียง

graph TD
  RichDomainModel -->|opposite of| AnemicModel
  AnemicModel -->|behavior moves to| ServiceLayer
  AnemicModel -->|violates| TellDontAsk
  AnemicModel -->|resembles| DataClass
  RichDomainModel --> Entity
  RichDomainModel --> Aggregate
  Entity -->|relies on| Encapsulation

code ต่อ​ไป​นี้​เปรียบเทียบ Order แบบ anemic กับ​แบบ rich domain model ใน​ภาษา C#

// ===== แบบ Anemic: มีแต่สถานะ ไม่มีพฤติกรรมของตัวเอง =====
public class Order
{
public List<OrderItem> Items { get; set; } = new();
public decimal Total { get; set; }
public OrderStatus Status { get; set; }
}
// logic ทั้งหมดถูกดึงออกไปไว้ใน service แทน
public class OrderService
{
public void AddItem(Order order, OrderItem item)
{
// ไม่มีใครห้ามเรียก AddItem ซ้ำสองครั้งโดยไม่คำนวณ Total ใหม่
order.Items.Add(item);
order.Total = order.Items.Sum(i => i.Price * i.Quantity);
}
public void Ship(Order order)
{
// กฎธุรกิจกระจายอยู่ใน service แทนที่จะอยู่ใน Order เอง
if (order.Items.Count == 0)
throw new InvalidOperationException("ไม่สามารถจัดส่ง order ที่ไม่มีสินค้าได้");
order.Status = OrderStatus.Shipped;
}
}
// ===== แบบ Rich Domain Model: encapsulate สถานะและพฤติกรรมไว้ด้วยกัน =====
public class Order
{
private readonly List<OrderItem> _items = new();
public IReadOnlyCollection<OrderItem> Items => _items;
public decimal Total { get; private set; }
public OrderStatus Status { get; private set; }
public void AddItem(OrderItem item)
{
_items.Add(item);
Total = _items.Sum(i => i.Price * i.Quantity);
}
public void Ship()
{
// Order รับผิดชอบรักษา invariant ของตัวเอง — เรียก order.Ship() ตรง ๆ
if (_items.Count == 0)
throw new InvalidOperationException("ไม่สามารถจัดส่ง order ที่ไม่มีสินค้าได้");
Status = OrderStatus.Shipped;
}
}

ใน version rich model ผู้​เรียก​ไม่มี​ทาง set collection หรือ Total ตรง ๆ จาก​ภายนอก​ได้​เลย — ต้อง “บอก” (tell) ให้ Order ทำงาน​ผ่าน method ของ​มัน​เอง​เท่านั้น ตรง​ตาม​หลัก Tell, Don’t Ask

Anemic model มัก​ถูก​ระบุ​ด้วย code smell สอง​อย่าง คือ​การ​เปิด collection property ออก​มา​ตรง ๆ ให้​แก้ไข​ได้​จาก​ภายนอก และ​การ​มี property (โดย​เฉพาะ setter) แบบ​เปิด​กว้าง​มาก​เกิน​ไป​จน​ไม่มี​การ​ควบคุม​ค่า​เลย ซึ่ง​ตรง​กับ​ลักษณะ​ของ code smell Data Class — class ที่​มี​แต่ field, getter และ setter โดย​ไม่มี behavior ที่​มี​ความหมาย

ประเด็น​นี้​ยัง​เป็น​ที่​ถกเถียง​ใน​วงการ​อยู่​ไม่​น้อย ฝ่าย​ที่​มอง​ว่า anemic model ไม่ใช่​ปัญหา​เสมอ​ไป​ให้​เหตุผล​ว่า​มัน​แยก logic ออก​จาก​ข้อมูล​อย่าง​ชัดเจน เหมาะ​กับ application ง่าย ๆ, ทำให้ logic ไร้​สถานะ (stateless) จึง scale ได้​ง่าย​ขึ้น​ใน​แนว​ราบ, และ​หลีก​เลี่ยง​ความ​ซับซ้อน​ของ​การ mapping object-database บาง​ฝ่าย​ถึง​กับ​อ้าง​ว่า​มัน​สอดคล้อง​กับ Single Responsibility Principle (SRP) เพราะ​แยก “ข้อมูล” กับ “logic” ออก​จาก​กัน​เป็น​คนละ class แต่ Robert C. Martin ผู้​บัญญัติ SRP ได้​ชี้แจง​ว่า​นี่​เป็น​ความ​เข้าใจ​ผิด — SRP หมาย​ถึง “module ควร​รับผิดชอบ​ต่อ​ผู้​มี​ส่วน​ได้​ส่วน​เสีย (actor) เพียง​หนึ่ง​เดียว” ไม่ใช่ “แต่ละ module ต้อง​ทำ​แค่​หนึ่ง​อย่าง” การ​แยก​ข้อมูล​ออก​จาก​พฤติกรรม​ของ​มัน​เอง​จึง​ไม่ใช่​สิ่ง​ที่ SRP เรียกร้อง

Anemic model ตรง​ข้าม​กับ rich domain model ที่ entity และ aggregate จำลอง​ทั้ง​สถานะ​และ​พฤติกรรม​ของ​แนวคิด​ใน domain ไว้​ด้วย​กัน มัน​ยัง​มัก​ละเมิด​หลัก Tell, Don’t Ask เพราะ object ทำงาน​กับ​สถานะ​ของ​ตัวเอง​ไม่​ได้ ต้อง​ถูก object อื่น​ดึง​ข้อมูล​ไป​ประมวล​ผล​แทน​แบบ​ไม่ใช่ OO Vaughn Vernon ใน​หนังสือ Implementing Domain-Driven Design ก็​ยก​ตัวอย่าง N-layer architecture ที่​ใช้ dumb (anemic) model คู่​กับ ORM และ DTO เป็น​หนึ่ง​ใน​กับดัก​ที่​ทีม​มัก​ตก​และ​ต้อง​คิด​ทบทวน​แนวทาง​ออกแบบ​ใหม่​ด้วย​หลัก DDD