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
หัวข้อที่มีชื่อว่า “บทบาทใน DDD”ใน 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
ที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “ที่เกี่ยวข้อง”แหล่งอ้างอิง
หัวข้อที่มีชื่อว่า “แหล่งอ้างอิง”- ที่มา · deviq.com/domain-driven-design/anemic-model
- AnemicDomainModel — Martin Fowler
- Designing a microservice domain model — Microsoft Learn
- Anemic domain model — Wikipedia
- Vaughn Vernon, Implementing Domain-Driven Design (2013) — บทวิจารณ์ N-layer architecture ที่ใช้ anemic model คู่กับ ORM/DTO