Domain Service – เมื่อ logic ไม่ควรอยู่ใน Entity

ตอนที่ 6 ของซีรีส์ Domain-Driven Design ฉบับระบบธุรกิจจริง

Theme หลัก: ระบบบริหารพนักงาน (Staff / Employee Management System)

ในตอนที่ 5 เราใช้ Aggregate เพื่อกำหนดขอบเขตและปกป้อง invariant ของ domain

แต่เมื่อเริ่มออกแบบจริง คุณจะเจอสถานการณ์แบบนี้:

logic นี้เป็นกฎของธุรกิจแน่ ๆ … แต่ไม่รู้จะใส่ไว้ใน entity ตัวไหนดี

นี่คือจุดที่ Domain Service เข้ามามีบทบาท


ปัญหาที่เจอบ่อยในระบบพนักงาน

ลองดู use case ต่อไปนี้:

  • พนักงานขอเปลี่ยนกะ
  • ระบบต้องตรวจสอบว่า
    • ไม่ชนกับกะอื่นของพนักงาน
    • ไม่ชนกับกะที่ได้รับอนุมัติแล้ว
    • ไม่เกินนโยบายที่ HR กำหนด

คำถามคือ:

  • logic นี้ควรอยู่ใน ShiftRequest?
  • หรืออยู่ใน Staff?

คำตอบคือ: ไม่ชัดเจนทั้งคู่


ความเข้าใจผิดเกี่ยวกับ Service

ในระบบทั่วไป เรามักเห็น:

  • ShiftService
  • StaffService
  • RequestService

ที่ภายในเต็มไปด้วย if-else

DDD แยก Service ออกเป็น 3 ประเภท:

  1. Application Service (Use Case)
  2. Domain Service
  3. Infrastructure Service

ตอนนี้เราจะโฟกัสที่ข้อ 2


Domain Service คืออะไร

Domain Service คือ:

class ที่รวม logic ทางธุรกิจ

ซึ่ง ไม่เป็นเจ้าของข้อมูลเอง

และ ไม่เหมาะจะอยู่ใน entity ใด entity หนึ่ง

คุณสมบัติสำคัญ:

  • อยู่ใน layer ของ domain
  • ใช้ภาษาเดียวกับธุรกิจ (Ubiquitous Language)
  • ไม่มี state ระยะยาว

ตัวอย่าง Domain Service: ShiftPolicy

class ShiftPolicy {
  bool canRequestShift(
    Staff staff,
    WorkingPeriod period,
    List<ShiftRequest> existingRequests,
  ) {
    if (staff.status != StaffStatus.active) return false;

    for (final request in existingRequests) {
      if (request.period.overlaps(period)) {
        return false;
      }
    }

    return true;
  }
}

จุดสำคัญ:

  • ไม่เก็บข้อมูลเอง
  • รับทุกอย่างผ่าน parameter
  • สื่อความหมายเชิงธุรกิจ

Domain Service ไม่ใช่ Utility

ข้อผิดพลาดที่พบบ่อยคือ:

class DateUtils {
  static bool overlap(DateTime a, DateTime b) {}
}

นี่คือ Technical Utility

แต่ Domain Service:

  • ต้องสะท้อนกฎของธุรกิจ
  • ชื่อบอก intent ชัดเจน

ShiftPolicy ดีกว่า ShiftHelper


Domain Service ทำงานร่วมกับ Aggregate

Aggregate Root ยังคงเป็นผู้ตัดสินขั้นสุดท้าย

ตัวอย่างใน ShiftRequest:

class ShiftRequest {
  void request(
    Staff staff,
    WorkingPeriod period,
    ShiftPolicy policy,
    List<ShiftRequest> existing,
  ) {
    if (!policy.canRequestShift(staff, period, existing)) {
      throw Exception('Shift request not allowed');
    }

    // สร้าง request
  }
}

Policy ช่วยตัดสิน แต่ aggregate รับผิดชอบ invariant


สัญญาณว่าคุณควรใช้ Domain Service

คุณควรพิจารณา Domain Service เมื่อ:

  • logic ใช้หลาย entity
  • logic ไม่ belong กับ entity เดียว
  • การยัด logic ลง entity ทำให้มันอ้วนเกินไป

แต่ถ้า logic เป็นพฤติกรรมของ entity โดยตรง:

อย่ารีบใช้ Domain Service


สิ่งที่ Domain Service ไม่ควรทำ

  • เรียก database
  • รู้จัก framework
  • มี side effect ภายนอก

Domain Service ควร:

  • pure
  • test ง่าย
  • ย้ายได้โดยไม่กระทบ infrastructure

สรุปตอนที่ 6

  • Domain Service = logic ธุรกิจที่ไม่มีตัวตน
  • ใช้เมื่อ entity ไม่เหมาะจะรับภาระนั้น
  • อย่าใช้แทน entity แบบพร่ำเพรื่อ

ถ้า service ของคุณรู้จัก database

นั่นไม่ใช่ Domain Service


ตอนต่อไป

ตอนที่ 7: Application Service / Use Case – หัวใจของการไหลของระบบ

เราจะประกอบ domain ทั้งหมดเข้าด้วยกัน และดูว่า controller ควรบางแค่ไหน