จาก Imperative → Declarative → Domain Thinking

ตอนที่ 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 – ภาษาที่โค้ดกับธุรกิจพูดตรงกัน

เราจะเริ่มจากการตั้งชื่อผิด ๆ ในระบบพนักงาน แล้วค่อยแก้ให้โค้ดกลายเป็นภาษาของธุรกิจจริง ๆ