ตอนที่ 2 ของซีรีส์ Domain-Driven Design ฉบับระบบธุรกิจจริง
Theme หลัก: ระบบบริหารพนักงาน (Staff / Employee Management System)
ในตอนที่แล้ว เราคุยกันว่า DDD คือการเปลี่ยนมุมมองการออกแบบระบบ โดยให้ Domain เป็นศูนย์กลาง
ตอนนี้เราจะลงลึกขึ้นอีกขั้น ด้วยการดูว่า
วิธีคิดในการเขียนโค้ดของเราเอง ส่งผลยังไงต่อการหายไปของ domain
และทำไมการเปลี่ยนจาก Imperative → Declarative → Domain Thinking ถึงสำคัญมาก
จุดเริ่มต้นของปัญหา: Imperative Thinking
Imperative thinking คือการเขียนโค้ดแบบ:
“ทำทีละขั้น ให้ได้ผลลัพธ์ตามที่ต้องการ”
ตัวอย่างที่พบได้บ่อยในระบบพนักงาน:
void approveShiftRequest(String requestId, String approverId) {
final request = repository.findById(requestId);
if (request == null) return;
if (request.status != 'PENDING') return;
if (!userService.isManager(approverId)) return;
request.status = 'APPROVED';
request.approvedBy = approverId;
request.approvedAt = DateTime.now();
repository.save(request);
}
โค้ดนี้ ทำงานได้ และดูเหมือนจะโอเค
แต่ลองถามตัวเองว่า:
- กฎของธุรกิจอยู่ตรงไหน?
- อะไรคือสิ่งที่ “ห้าม” เกิดขึ้น?
- ถ้ากฎเปลี่ยน ใครเป็นคนรับผิดชอบ?
คำตอบคือ: ไม่มีที่ชัดเจน
เมื่อโค้ดบอก “ยังไง” แต่ไม่บอก “อะไร”
โค้ดแบบ Imperative มักมีปัญหาเหล่านี้:
- อ่านแล้วรู้ลำดับ แต่ไม่รู้ความหมาย
- Business rule ซ่อนอยู่ใน if-else
- ชื่อ method ไม่สะท้อน intent ของธุรกิจ
ในตัวอย่างก่อนหน้า:
status != 'PENDING'คือกฎ หรือแค่ technical check?- ใครควรมีสิทธิ์ approve จริง ๆ?
- อนุมัติซ้ำได้ไหม?
คำถามเหล่านี้ ไม่ถูกตอบใน domain แต่กระจายอยู่ใน service
Declarative Thinking: บอก “อะไร” แทน “ยังไง”
Declarative thinking คือการเขียนโค้ดแบบ:
“ระบบควรอนุญาตให้เกิดอะไรได้บ้าง”
ลองปรับมุมมองใหม่:
shiftRequest.approve(by: approver);
ประโยคนี้สั้น แต่มีความหมายทางธุรกิจชัดเจนมาก
คำถามสำคัญคือ:
แล้วกฎทั้งหมดหายไปไหน?
คำตอบคือ: มันควรไปอยู่ใน Domain
Domain Thinking: ให้ Domain ปกป้องกฎของตัวเอง
ลองดูตัวอย่าง Domain Model ของ ShiftRequest
class ShiftRequest {
ShiftRequestStatus status;
String? approvedBy;
DateTime? approvedAt;
void approve(Staff approver) {
if (status != ShiftRequestStatus.pending) {
throw Exception('Request is not pending');
}
if (!approver.isManager) {
throw Exception('Only manager can approve');
}
status = ShiftRequestStatus.approved;
approvedBy = approver.id;
approvedAt = DateTime.now();
}
}
ตอนนี้:
- กฎอยู่กับข้อมูลที่มันควบคุม
- ไม่มีใครข้ามกฎนี้ได้
- อ่านโค้ดแล้วเข้าใจธุรกิจทันที
นี่คือ Domain Thinking
เปลี่ยนบทบาทของ Service และ Use Case
เมื่อ domain แข็งแรง:
- Service / Use Case ไม่ต้องรู้รายละเอียดกฎ
- ทำหน้าที่แค่ ประสานงาน (orchestration)
ตัวอย่าง Use Case:
void approveShiftRequest(String requestId, String approverId) {
final request = repository.findById(requestId);
final approver = staffRepository.findById(approverId);
request.approve(approver);
repository.save(request);
}
Use Case อ่านเหมือนขั้นตอนธุรกิจ ไม่ใช่ logic ยิบย่อย
ทำไม mindset นี้ถึงสำคัญกับระบบธุรกิจ
ระบบธุรกิจมีลักษณะร่วมกัน:
- กฎเปลี่ยนบ่อย
- Exception เยอะ
- ข้อยกเว้นสำคัญกว่ากรณีปกติ
ถ้า logic กระจัดกระจาย:
- แก้กฎหนึ่งครั้ง = เสี่ยงทั้งระบบ
- Test ยาก
- ไม่มีใครมั่นใจว่า logic ครบจริง
Domain Thinking ทำให้:
- กฎมีเจ้าของชัดเจน
- Test domain ได้โดยไม่ต้องพึ่ง UI หรือ DB
- ระบบโตโดยไม่เสียรูป
สรุปตอนที่ 2
- Imperative: บอกระบบว่า ทำยังไง
- Declarative: บอกระบบว่า อะไรควรเกิด
- Domain Thinking: ให้ domain เป็นผู้ตัดสิน
ถ้า Domain ของคุณไม่มี behavior
นั่นแปลว่ากฎของธุรกิจอาจกำลังลอยอยู่ผิดที่
ตอนต่อไป
ตอนที่ 3: Ubiquitous Language – ภาษาที่โค้ดกับธุรกิจพูดตรงกัน
เราจะเริ่มจากการตั้งชื่อผิด ๆ ในระบบพนักงาน แล้วค่อยแก้ให้โค้ดกลายเป็นภาษาของธุรกิจจริง ๆ

