Data Class
class ที่มีแต่ข้อมูล ไร้พฤติกรรมที่มีความหมาย
กลิ่นนี้คืออะไร
หัวข้อที่มีชื่อว่า “กลิ่นนี้คืออะไร”Data Class คือ class ที่ ถือข้อมูลแต่ไร้พฤติกรรมที่มีความหมาย — มีแต่ field (หรือ property) พร้อม getter/setter ยาวเหยียด ไม่มี operation ที่ตรวจสอบ invariant ไม่มี method ที่บังคับกฎธุรกิจ ทุกอย่างเปิดให้ใครก็ได้อ่าน-เขียนตามใจชอบ
Martin Fowler อธิบายไว้ใน Refactoring (ฉบับพิมพ์ที่ 2) สั้น ๆ ว่า Data Class คือ class ที่มี field (mutable state) แต่ไม่มีพฤติกรรมอื่นเลย อาจมี get/set สำหรับ field หรือเปิด field เป็น public ตรงๆ ก็ได้ — สัญญาณสำคัญคือ มี function ที่อื่นในระบบคอยดำเนินการกับข้อมูลก้อนนี้ ทั้งที่ตรรกะเหล่านั้นควรจะย้ายเข้ามาอยู่ใน class เอง
ไม่ใช่ทุก class ที่ถือข้อมูลจะเป็นกลิ่นเสมอไป — value object ที่ immutable, DTO ที่ยืนอยู่ตรงขอบระบบ (เช่น request/response model, JSON payload), หรือ record ที่จงใจออกแบบให้เป็นข้อมูลล้วนๆ (persistence entity แบบ ORM) ล้วนถูกต้องตามบทบาทของมัน กลิ่นนี้เกิดเมื่อ domain object ที่ควรมีพฤติกรรม กลับกลายเป็นถุงใส่ข้อมูล — นี่คือรูปแบบเดียวกับที่ระดับสถาปัตยกรรมเรียกว่า Anemic Model นั่นเอง
วิธีสังเกต
หัวข้อที่มีชื่อว่า “วิธีสังเกต”- class มีแต่ field/property กับ getter/setter แทบทุกตัว ไม่มี method ที่ทำอะไรมากกว่าคืนหรือกำหนดค่า
- ไม่มี constructor ที่ตรวจสอบความถูกต้อง (validation) ของข้อมูล — สร้าง object ในสถานะที่ไม่สมเหตุสมผลได้เสมอ
- ทุก field เป็น public หรือมี setter สาธารณะ ไม่มีการปิดกั้น (encapsulation) เลย
- code ที่เรียกใช้ class นี้ต้องดึงค่าหลาย field ออกมาคำนวณเอง แล้วค่อยยัดผลกลับเข้าไปใน field อื่น
- ตรรกะทางธุรกิจแบบเดียวกันถูก copy วางซ้ำในหลายที่ที่ใช้ class นี้ (เพราะไม่มีที่ “บ้าน” ให้ตรรกะนั้นอยู่)
- เวลาแก้กฎธุรกิจที่เกี่ยวกับข้อมูลนี้ ต้องไล่แก้หลาย file ที่กระจายอยู่ทั่ว codebase
ทำไมถึงเป็นปัญหา
หัวข้อที่มีชื่อว่า “ทำไมถึงเป็นปัญหา”จุดแข็งของการออกแบบเชิงวัตถุ (OOP) คือการรวมข้อมูลกับพฤติกรรมที่ทำงานกับข้อมูลนั้นไว้ในที่เดียว Data Class ละทิ้งโอกาสนี้ไปโดยสิ้นเชิง ผลที่ตามมามีหลายชั้น:
- ตรรกะกระจัดกระจาย — เมื่อ class ไม่มีพฤติกรรม ใครก็ตามที่ต้องการทำอะไรกับข้อมูลนั้นต้องเขียน logic เองใน class ของตัวเอง ทำให้กฎธุรกิจเดียวกันถูกเขียนซ้ำในหลายจุด (ผิดหลัก DRY)
- ดึงดูด Feature Envy — method ใน class อื่นที่ต้อง “ล้วง” field ของ Data Class มาคำนวณ คือ method ที่ “อิจฉา” ข้อมูลของ class อื่น มันควรอยู่ใน class ที่ข้อมูลนั้นอาศัยอยู่ต่างหาก
- ไม่มี invariant ที่รับประกันได้ — เมื่อไม่มี constructor หรือ method ควบคุมการเปลี่ยนสถานะ ไม่มีใครรับประกันได้ว่า object จะอยู่ในสถานะที่ valid เสมอ (เช่น start date ที่มากกว่า end date, ยอดเงินติดลบ)
- แก้กฎธุรกิจยาก และเสี่ยงต่อ bug — เพราะกฎเดียวกันกระจายอยู่หลายที่ พอแก้จุดหนึ่งแล้วลืมแก้อีกจุด ระบบก็จะพฤติกรรมไม่สอดคล้องกัน (inconsistent)
- เชื่อมโยงกับ Anemic Model — เมื่อ Data Class เกิดขึ้นทั่วทั้ง domain ระบบทั้งชุดจะกลายเป็น anemic domain model ที่ domain layer ไม่มีความหมายอะไรมากกว่าที่เก็บข้อมูล ส่วน business logic ทั้งหมดไปกองอยู่ที่ “service” หรือ “manager” ชั้นบน — ขัดกับหลักการพื้นฐานของ OOP ที่ว่าให้รวม data กับ process ไว้ด้วยกัน
ตัวอย่างและการ refactor
หัวข้อที่มีชื่อว่า “ตัวอย่างและการ refactor”ตัวอย่างที่มีกลิ่น
หัวข้อที่มีชื่อว่า “ตัวอย่างที่มีกลิ่น”Invoice ด้านล่างมีแต่ field กับ getter/setter ส่วนตรรกะคำนวณยอดรวม-ส่วนลด-สถานะเกินกำหนด ถูกเขียนแยกไว้ใน InvoiceService ซึ่งต้องดึงทุก field ออกมาคำนวณเอง (อาการของ Feature Envy):
// Data Class: มีแต่ field กับ getter/setter ไม่มีพฤติกรรมpublic class Invoice{ public decimal SubTotal { get; set; } public decimal DiscountPercent { get; set; } public DateTime DueDate { get; set; } public bool IsPaid { get; set; }}
// ตรรกะทั้งหมดถูกยัดไว้นอก classpublic class InvoiceService{ public decimal CalculateTotal(Invoice invoice) { var discount = invoice.SubTotal * (invoice.DiscountPercent / 100m); return invoice.SubTotal - discount; }
public bool IsOverdue(Invoice invoice) { return !invoice.IsPaid && invoice.DueDate < DateTime.UtcNow; }}ปัญหา: ไม่มีอะไรกันไม่ให้ DiscountPercent เป็น 150 หรือ SubTotal เป็นค่าติดลบ และถ้ามีอีกจุดในระบบต้องคำนวณยอดรวมใบแจ้งหนี้ ก็มีโอกาสสูงที่จะเขียนสูตรเดิมซ้ำอีกรอบ (หรือเขียนผิดจากต้นฉบับ)
หลัง refactor — Encapsulate Field + Move Method
หัวข้อที่มีชื่อว่า “หลัง refactor — Encapsulate Field + Move Method”ย้ายพฤติกรรมเข้าไปอยู่ใน class (Move Method / Extract Method ตามคำเรียกของ Fowler) พร้อม encapsulate field ผ่าน constructor ที่ตรวจสอบ invariant และปิด setter สาธารณะ (Remove Setting Method):
public class Invoice{ public decimal SubTotal { get; } public decimal DiscountPercent { get; } public DateTime DueDate { get; } public bool IsPaid { get; private set; }
public Invoice(decimal subTotal, decimal discountPercent, DateTime dueDate) { if (subTotal < 0) throw new ArgumentException("SubTotal ต้องไม่ติดลบ"); if (discountPercent is < 0 or > 100) throw new ArgumentException("DiscountPercent ต้องอยู่ระหว่าง 0-100");
SubTotal = subTotal; DiscountPercent = discountPercent; DueDate = dueDate; }
// พฤติกรรมย้ายเข้ามาอยู่กับข้อมูลของมันเอง public decimal CalculateTotal() { var discount = SubTotal * (DiscountPercent / 100m); return SubTotal - discount; }
public bool IsOverdue(DateTime asOf) => !IsPaid && DueDate < asOf;
public void MarkAsPaid() => IsPaid = true;}ตอนนี้ Invoice รับประกันได้เองว่าค่าที่ถืออยู่สมเหตุสมผลเสมอ (invariant ถูกบังคับใน constructor) และไม่ว่าจะเรียกใช้จากกี่ที่ สูตรคำนวณยอดรวมก็มีที่มาเดียว ไม่มีทางเขียนซ้ำผิดเพี้ยนได้อีก code ฝั่งเรียกใช้จาก “ถาม state แล้วคำนวณเอง” กลายเป็น “สั่งให้ object ทำงานให้” ตรงตามหลัก Tell, Don’t Ask
แผนภาพสรุปทิศทางการย้ายความรับผิดชอบ:
flowchart LR
Client[Client code]
Before[Invoice data only]
Service[InvoiceService with logic]
After[Invoice with data plus behavior]
Client --> Before
Client --> Service
Before -.refactor.-> After
Service -.merge into.-> After
ข้อควรระวัง
หัวข้อที่มีชื่อว่า “ข้อควรระวัง”ไม่ต้องรีบยัดพฤติกรรมทุกอย่างเข้า class จน class บวมกลายเป็น God Class อีกกลิ่นหนึ่ง — เป้าหมายคือย้าย method ที่ “ควรอยู่ที่นี่จริงๆ” (เพราะมันทำงานกับ field ของ class นี้เป็นหลัก) ไม่ใช่ทุก utility ที่เคยเรียกผ่านมันบังเอิญรับ parameter เป็น Invoice และสำหรับ DTO/entity ที่ตั้งใจให้เป็นข้อมูลล้วนๆ ที่ขอบระบบ (serialization, ORM mapping) การเป็น Data Class ก็ไม่ใช่กลิ่นแต่อย่างใด
ที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “ที่เกี่ยวข้อง”- Anemic Model — เมื่อ Data Class เกิดกับ domain object ทั่วทั้งระบบ
- Feature Envy — อาการคู่กันที่ method ภายนอกคอยล้วงข้อมูลของ Data Class
- Primitive Obsession — Data Class มักถือ field เป็น primitive ล้วนแทนที่จะห่อเป็น value object
- Duplicate Code — ผลพลอยได้จากตรรกะที่ต้องเขียนซ้ำเพราะไม่มีบ้านให้อยู่
- Tell, Don’t Ask — หลักการที่ตรงข้ามกับพฤติกรรมของ Data Class
- Encapsulation — หลักการพื้นฐานที่ Data Class ละเมิด