ข้าม​ไป​ยัง​เนื้อหา

Domain Services

เวลา​สร้าง Domain ModelDomain ModelModel ของ Domain ที่​ฝัง​กฎ​และ​กระบวนการ​ทาง​ธุรกิจ​ไว้​ใน code จริง หัวใจ​ของ DDD คือ​การ​พัฒนา domain model ที่ “เข้าใจ​ธุรกิจ​อย่าง​ลึกซึ้ง”Strategic Design คุณ​จะ​เจอ “แนวคิด” หรือ “พฤติกรรม” บาง​อย่าง​ที่​ลอง​วาง​ลง​ใน EntityEntityอ็อบเจ็กต์ที่​มี “ตัวตน” (identity) ต่อ​เนื่อง​ตาม​เวลา แม้​ค่า​ภายใน​เปลี่ยน ก็​ยัง​เป็น​สิ่ง​เดิม เช่น ลูกค้า, คำ​สั่ง​ซื้อ — แยกแยะ​ด้วย id ไม่ใช่​ด้วย​ค่าTactical Design หรือ AggregateAggregateกลุ่ม​ของ Entity และ Value Object ที่​ถูก​มอง​เป็น​หนึ่ง​หน่วย​เดียว​เพื่อ​รักษา​ความ​ถูกต้อง​ของ​ข้อมูล มี​ขอบเขต​ชัดเจน และ​เป็น​หน่วย​ของ transactionTactical Design ตัว​ไหน​ก็​รู้สึก​ฝืน ไม่​เข้า​ที่ — นี่​คือ​สัญญาณ​ว่า​คุณ​อาจ​กำลัง​ต้องการ Domain ServiceDomain Serviceบริการ​ที่​ถือ “ตรรกะ​ธุรกิจ” ซึ่ง​ไม่​เข้า​กับ Entity หรือ Value Object ตัว​ใด​ตัว​หนึ่ง​โดย​ธรรมชาติ เช่น การ​โอน​เงิน​ที่​เกี่ยวข้อง​กับ​สอง​บัญชี — ไร้​สถานะ (stateless)Tactical Design

ปัญหา​คือ​คำ​ว่า “service” เป็น​คำ​ที่​ถูก​ใช้​ปน​กัน​มาก (overloaded) ทั้ง SOA, Application ServiceApplication Serviceบริการ​ที่ “ประสาน​งาน” use case — จัดการ transaction, ความ​ปลอดภัย, การ​บันทึก, แปลง DTO — แต่​ไม่มี​กฎ​ธุรกิจ​อยู่​ใน​ตัว​มัน​เองTactical Design และ Domain Service จน​นัก​พัฒนา​สับสน​กัน​บ่อยๆ แต่​โชค​ดี​ที่ Domain Service แยกแยะ​ได้​ง่าย​ด้วย​ลักษณะ​เด่น​สอง​ข้อ คือ มัน​แทน​แนวคิด​ของ domain และ มัน​ไร้​สถานะ (stateless) บท​นี้​จะ​พา​ดู​ว่า​ควร​ใช้ Domain Service เมื่อใด แยก​มัน​ออก​จาก service ชนิด​อื่น​อย่างไร และ​จะ​เรียก​ใช้​มัน​จาก​ทั้ง service layer และ​ภายใน domain model ได้​ด้วย​เทคนิค​ใด​บ้าง


ใน​เชิง​แนวคิด Domain Service คือ พฤติกรรม​ที่​มี​อยู่​จริง​ใน problem domain — มัน​ปรากฏ​ขึ้น​ใน​บทสนทนา​กับ domain expert และ​เป็น​ส่วน​หนึ่ง​ของ Ubiquitous LanguageUbiquitous Languageภาษา​กลาง​ที่​ทุก​คนใน​ทีม (ทั้ง​นัก​พัฒนา​และ​ผู้เชี่ยวชาญ​ธุรกิจ) ใช้​ร่วม​กัน อิง​กับ domain model โดยตรง คำ​เดียวกัน​ต้อง​หมาย​ถึง​สิ่ง​เดียวกัน และ​สะท้อน​ลง​ใน codeStrategic Design อย่าง​ชัดเจน ถ้า​คุณ​ยึด​ลักษณะ​นี้​ไว้​เป็น​หลัก คุณ​จะ​ใช้ Domain Service ได้​อย่าง​มี​เป้าหมาย ไม่​หลง​ไป​สร้าง service ขึ้น​มา​อย่าง​ไร้​ทิศทาง

ใน​ทาง​เทคนิค Domain Service มี​ลักษณะ​สำคัญ​สาม​ข้อ:

  • แทน “พฤติกรรม” จึง​ไม่มี identity — ต่าง​จาก Entity ที่​ต้อง​มี​ตัวตน​ต่อ​เนื่อง
  • ไร้​สถานะ (stateless) — มี collaborator ที่​ฉีด​เข้า​มา​ได้ แต่​ตัว​มัน​เอง​ไม่​เก็บ state; ผลลัพธ์​ขึ้น​กับ input ล้วนๆ
  • มัก​ทำ​หน้าที่ orchestrate — ประสาน​งาน​หลาย Entity หรือ Value Object เข้า​ด้วย​กัน
หัวใจ​ของ Domain Service

Domain Service ใช้​เพื่อ ประสาน​งาน entity และ​ห่อ​หุ้ม​นโยบาย​ธุรกิจ (business policy) ไม่ใช่​เพื่อ​ทำงาน “เดินสาย​ท่อ” ทาง​เทคนิค (infrastructural plumbing) — งาน​เดินสาย​ท่อ​ให้​ปล่อย​เป็น​หน้าที่​ของ Application Service

คุณ​คุย​กับ domain expert เรื่อง​แนวคิด​หนึ่ง​ที่ “เกี่ยวข้อง​กับ​หลาย Entity” แต่​ไม่​แน่ใจ​ว่า Entity ตัว​ไหน “เป็น​เจ้าของ” พฤติกรรม​นั้น มัน​ไม่​เข้า​กับ​ใคร​เลย และ​พอ​พยายาม​ฝืน​ใส่​ลง​ไป​ก็​รู้สึก​ไม่​ลงตัว — รูปแบบ​ความคิด​เช่น​นี้​คือ​ตัว​บ่ง​ชี้​ที่​ชัดเจน​ว่า​ถึง​เวลา​ของ Domain Service


ลอง​นึกถึง​ระบบ​เกม​ออนไลน์​ที่​มี Entity ชื่อ Competitor (ผู้​แข่งขัน) สอง​คน และ OnlineDeathmatch (การ​แข่ง​หนึ่ง​แมตช์) เมื่อ​จบ​แมตช์ คะแนน​ของ​ผู้​เล่น​แต่ละ​คน​ต้อง​ถูก​อัปเดต​ตาม​ผล​การ​แข่ง

ทาง​เลือก​แรก​คือ​เอา “ตรรกะ​คำนวณ​คะแนน​และ​รางวัล” ไป​วาง​ไว้​ใน OnlineDeathmatch เลย แต่​นั่น​มี​ปัญหา​ใหญ่: กฎ​เหล่า​นี้​ยืดหยุ่น​มาก — ธุรกิจ​อาจ​อยาก​แจก​คะแนน​สอง​เท่า​ใน​บาง​ช่วง หรือ​แจก​รางวัล​พิเศษ​ตาม​แคมเปญ และ​โปรโมชัน​ชุด​เดียวกัน​มัก​ถูก​นำ​ไป​ใช้​กับ​เกม​หลาย​ประเภท เช่น TeamBattle, CaptureTheFlag, GangCrusade ฯลฯ

นี่​คือ​สัญญาณ​ว่า “การ​คิด​คะแนน” และ “การ​ให้​รางวัล” เป็น​แนวคิด​สำคัญ​ใน​ตัว​มัน​เอง ควร​แยก​ออก​มา​เป็น Domain Service ที่​ถือ single responsibility ของนโยบายนั้นๆ — แล้ว​ฉีด (inject) เข้าไป​ใช้​ใน OnlineDeathmatch หรือ object อื่น​ที่​ต้องการ​ได้

IGamingScorePolicy.cs
// interface ของ domain service — เป็นส่วนหนึ่งของ Ubiquitous Language
public interface IGamingScorePolicy
{
void Apply(IGame game);
}
public interface IGamingRewardPolicy
{
void Apply(IGame game);
}
OnlineDeathmatch.cs
public class OnlineDeathmatch : IGame
{
private Competitor player1;
private Competitor player2;
// domain services ที่ถูกฉีดเข้ามา — สลับเข้าออกได้ตามโปรโมชันล่าสุด
private IEnumerable<IGamingScorePolicy> scorers;
private IEnumerable<IGamingRewardPolicy> rewarders;
private void UpdateScoresAndRewards()
{
foreach (var scorer in scorers) scorer.Apply(this);
foreach (var rewarder in rewarders) rewarder.Apply(this);
}
}

ข้อดี​คือ​คุณ สลับ​นโยบาย​คะแนน/รางวัล​เข้า​ออก​ได้ ตาม​แคมเปญ และ​ที่​สำคัญ​ไม่​แพ้​กัน — คุณ​ทำให้​แนวคิด​เหล่า​นี้ ปรากฏ​ชัด​ใน domain เช่น เมื่อ​ธุรกิจ​รัน​โปรโมชัน “สมาชิก​ฟรี 12 เดือน​สำหรับ​ผู้​เล่น​คะแนน​สูง” คุณ​ก็​จำลอง​มันได้ตรงๆ เป็น Domain Service ชื่อ Free12MonthSubscriptionForHighScoresReward ที่​นิยาม​แนวคิด​นี้​ออก​มา​ให้​เห็น​ใน Ubiquitous Language

Domain Service ตัว​นี้​ถือ​กฎ​ธุรกิจ​สำคัญ​และ​อาศัย​อยู่ ภายใน domain model ทั้งหมด เรา​จึง​เรียก​มัน​ว่า​เป็น pure domain service

🛒 เทียบ​กับ e-commerce — นโยบาย​ส่วนลด

ใน​ร้าน​ค้า​ออนไลน์ “วิธี​คิด​ราคา​หลัง​หัก​ส่วนลด” ก็​เป็น​แนวคิด​ที่​ยืดหยุ่น​แบบ​เดียวกัน — บาง​สัปดาห์​เป็น “ลด 10%” บาง​ช่วง​เป็น “ซื้อ 1 แถม 1” การ​ฝัง​ตรรกะ​นี้​ลง​ใน Order ตรงๆ จะ​ทำให้ Aggregate รก​และ​แบก​ความ​รับผิดชอบ​เกิน​ตัว เรา​จึง​แยก​เป็น Domain Service เช่น IDiscountPolicy แล้ว​ฉีด​เข้าไป ทำให้​สลับ​โปรโมชัน​ได้​โดย​ไม่​ต้อง​แก้ Order และ​ทำให้ “นโยบาย​ส่วนลด” กลาย​เป็น​แนวคิด​ที่​มอง​เห็น​ได้​ใน domain


อีก​กรณี​ใช้​งา​นก​ว้างๆ ของ Domain Service คือ​การ​เป็น contract — แนวคิด​นั้น​สำคัญ​ต่อ domain แต่​ตัว implementation ต้อง​พึ่ง infrastructure ที่ ใช้​ใน domain model ไม่​ได้

ตัวอย่าง​คลาสสิก​คือ ShippingRouteFinder ที่​ต้อง​ยิง HTTP request ไป​ยัง routing API เพื่อ​หา​เส้นทาง​ที่​เป็น​ไป​ได้​ของ​การ​เดินทาง

IShippingRouteFinder.cs
// interface ของ domain service — อยู่ใน domain model (นี่คือ "สัญญา")
public interface IShippingRouteFinder
{
Route FindFastestRoute(Location departing, Location destination, DateTime departureDate);
}
// implementation — อยู่ใน service layer เพราะยิง HTTP เป็นเรื่อง infrastructure
public class ShippingRouteFinder : IShippingRouteFinder
{
public Route FindFastestRoute(Location departing, Location destination, DateTime departureDate)
{
var response = QueryRoutingApi(departing, destination, departureDate); // HTTP call
return ParseRoute(response);
}
}

ตรรกะ​การ​ยิง HTTP เป็น​เรื่อง infrastructure จึง​อยู่​ใน domain ไม่​ได้ — ดังนั้น implementation ต้อง​อยู่​ใน service layer แต่ แนวคิด​เป็น​ส่วน​หนึ่ง​ของ Ubiquitous Language จึง​ต้อง​มี interface อยู่​ใน domain model นี่​คือ​หลักการ​เดียว​กับ PortPortinterface ที่ domain นิยาม​ไว้​เพื่อ​บอกว่า “ต้องการ​อะไร” เช่น CargoRepository โดย​ไม่​สน​ว่า​ใคร​จะ​มา​ทำให้ — โลก​ภายนอก​ต้อง​ปรับ​ตัว​เข้าหา ไม่ใช่​กลับ​กันArchitecture และ AdapterAdapterตัว​ที่ “ทำให้” port เป็น​จริง เช่น PostgresCargoRepository หรือ REST controller อยู่​ชั้น​นอก​และ​พึ่งพา domain ทำให้​สลับ​เทคโนโลยี​ได้​โดย​ไม่​แตะ domainArchitecture คือ domain นิยาม​ว่า “ต้องการ​อะไร” แล้ว​โลก​ภายนอก​ค่อย​มา​ทำให้​สำเร็จ

สถานการณ์​อื่น​ที่​ใช้ Domain Service เป็น contract ได้ เช่น การ​พิสูจน์​ตัวตน entity, การ​ดึง​อัตรา​แลกเปลี่ยน, การ​คำนวณ​ภาษี และ​การ​แจ้ง​เตือน​แบบ​เรียลไทม์

🚢 ตัวอย่าง​การ​ขนส่ง — หา​เส้นทาง​ที่​เร็ว​ที่สุด

ใน​ระบบ​ขนส่ง​ตู้ container “การ​หา​เส้นทาง​เดิน​เรือ​ที่​เร็ว​ที่สุด” คือ​แนวคิด​ที่ domain expert พูด​ถึง​ตลอด มัน​จึง​ควร​อยู่​ใน Ubiquitous Language ใน​รูป interface IShippingRouteFinder แต่​การ​คำนวณ​จริง​ต้อง​เรียก API ภายนอก​ที่​รู้​ตาราง​เดิน​เรือ​และ​สภาพ​อากาศ — ซึ่ง​เป็น infrastructure ที่​ต้อง​อยู่​นอก domain model เรา​จึง​ได้​รูปแบบ “interface ใน domain, implementation ใน service layer” พอดี


ลอง​ดู​ตัวอย่าง RomanceOMeter จาก​ระบบ​เว็บ​หา​คู่ ที่​ใช้​ประเมิน​ว่า​คน​สอง​คน​เข้า​กัน​ได้​แค่​ไหน:

RomanceOMeter.cs
// domain service — เป็นส่วนหนึ่งของ Ubiquitous Language
public class RomanceOMeter
{
// ไร้สถานะ (มี collaborator ได้) และมีแต่พฤติกรรม
public CompatibilityRating AssessCompatibility(LoveSeeker seeker1, LoveSeeker seeker2)
{
var rating = new CompatibilityRating(0);
// orchestrate entities: เทียบประวัติเดต, กรุ๊ปเลือด, ไลฟ์สไตล์ ฯลฯ
if (seeker1.BloodType == seeker2.BloodType)
rating = rating + new CompatibilityRating(250);
// ...
return rating; // คืน value object
}
}

RomanceOMeter ไม่มี ID และ​ไม่มี state ที่​เกี่ยว​กับ​ตัวตน​เลย มัน​เป็น พฤติกรรม​ล้วนๆ มี​เพียง method AssessCompatibility() ที่​รับ Entity สอง​ตัว​เข้า​มา แล้ว​คำนวณ​จาก input ทั้งหมด ก่อน​คืน​ค่า​เป็น Value Object — ครบ​ทั้ง​สาม​ลักษณะ​ของ Domain Service คือ ไร้ identity, ไร้​สถานะ, และ orchestrate object หลาย​ตัว


เมื่อยอมรับ​แล้ว​ว่า​ไม่ใช่​ทุก​ตรรกะ​ที่​ต้อง​อยู่​บน Entity และ Domain Service เป็น​เครื่องมือ​ที่​มี​ประโยชน์ — สิ่ง​ที่​ต้อง​ระวัง​คือ อย่า​ผลัก​ตรรกะ​มาก​เกิน​ไป​ลง​ใน Domain Service เพราะ​จะ​ทำให้ model กลาย​เป็น Anemic Domain ModelAnemic Domain Modelanti-pattern ที่อ็อบเจ็กต์มี​แต่ getter/setter ไร้​พฤติกรรม ตรรกะ​ถูก​ดึง​ไป​ไว้​ใน service ทั้งหมด — จ่าย​ค่า domain model แต่​ไม่​ได้​ประโยชน์​ของ​มัน​เลยTactical Design ที่​มี conceptual cohesion ต่ำ ซึ่ง​สวน​ทาง​กับ​ประโยชน์​หลัก​ของ DDD

การ​หา “จุด​สมดุล” ระหว่าง​มี​น้อย​ไป​กับ​มาก​ไป​นั้น​ง่าย​ขึ้น​เมื่อ​มี​ประสบการณ์ แต่​หลัก​คิด​ง่ายๆ ช่วย​ได้:

  • ถ้า​การ เพิ่ม​พฤติกรรม​ลง​ใน Entity แล้ว​รู้สึก​ฝืน ไม่​ตรง​กับ​ที่ domain expert พูด → ลอง​พิจารณา Domain Service แล้ว​สังเกต​ว่า​เขา​พูด​ถึง​แนวคิด​นั้น​แบบ “เป็น​เอกเทศ” หรือ​พูด​ผูก​กับ Entity เสมอ
  • ถ้า​คุณ​กำลัง สร้าง Domain Service เต็ม​ไป​หมด → แปล​ว่า​กระบวนการ​คิด​อาจ​หละหลวม​เกิน​ไป กำลัง​ดึง​ตรรกะ​ออก​จาก Entity มาก​เกิน​จำเป็น
กับดัก​สอง​ด้าน

มาก​ไป: Domain Service เยอะ​เกิน​ไป​จะ​ดึง​พฤติกรรม​ออก​จาก Entity จน​เหลือ​แต่ getter/setter — กลาย​เป็น Anemic Domain Model

น้อย​ไป: ฝืน​ยัด​ตรรกะ​ที่​ไม่​เข้า​กันลง​บน Entity/Value Object จน​แนวคิด​ที่​ต่าง​กัน​ปะปน​กัน บั่นทอน​ความ​ชัดเจน​ของ model


นี่​คือ​จุด​ที่​นัก​พัฒนา​สับสน​กัน​มาก​ที่สุด แต่​พอ​เข้าใจ​บทบาท​เชิง​แนวคิด​ของ​ทั้ง​คู่​แล้ว​จะ​ไม่​สับสน​อีก​เลย:

Domain ServiceApplication Service
แทน​แนวคิด​ของ domain?ใช่ไม่
มี​กฎ​ธุรกิจ?มีไม่มี
อยู่​ใน domain model?อย่าง​น้อย interface อยู่ไม่​อยู่​เลย แม้แต่ interface
บทบาทประสาน​งาน domain logicประสาน​งาน use case + infrastructure (transaction, security)

Application ServiceApplication Serviceบริการ​ที่ “ประสาน​งาน” use case — จัดการ transaction, ความ​ปลอดภัย, การ​บันทึก, แปลง DTO — แต่​ไม่มี​กฎ​ธุรกิจ​อยู่​ใน​ตัว​มัน​เองTactical Design อยู่​ใน service layer ล้วนๆ ทำ​หน้าที่​รวบรวม​เรื่อง infrastructure เช่น transaction เพื่อ​ให้ business use case หนึ่ง​ทำงาน​ครบ — โดย​ไม่มี​กฎ​ธุรกิจ​อยู่​ใน​ตัวเอง

คำถาม​ที่​พบ​บ่อย​ใน​ฟอรัม DDD คือ “จะ​จัดการ authentication/authorization ใน Domain Service อย่างไร​ดี​ที่สุด?” คำ​ตอบสั้นๆ คือ อย่า​ทำ เพราะ​นั่น​เป็น​ความ​รับผิดชอบ​ของ service layer คำถาม​นี้​เอง​สะท้อน​ความ​สับสน​รอบๆ คำ​ว่า service ได้​เป็น​อย่าง​ดี

Domain Service พึ่ง infrastructure เพื่อ “ป้อน​ข้อมูล​ให้ domain logic” ส่วน infrastructure ใน Application Service มี​ไว้​เพื่อ “ทำให้ domain model รัน​ได้​ถูกต้อง”

— Scott Millett & Nick Tune

นั่น​คือ Domain Service อาจ​ยิง HTTP หรือ​เขียน​ลง disk ใน​ฐานะ​ส่วน​หนึ่ง​ของ domain logic ขณะ​ที่ Application Service ห่อ domain model ด้วย transaction หรือ​สร้าง database connection เพื่อ​ให้ code รัน​เป็น use case เดียว​จบ


กรณี​ง่าย​ที่สุด: Application Service ดึง Entity ที่​ต้องการ​ออก​จาก RepositoryRepositoryตัว​ที่​ทำให้​เรา​เข้าถึง Aggregate ราวกับ​เป็น collection ใน​หน่วย​ความ​จำ ซ่อน​รายละเอียด​ฐาน​ข้อมูล มี1 repository ต่อ1 Aggregate RootTactical Design แล้ว​ส่ง​เข้า Domain Service

MultiMemberInsurancePremium.cs
// application service
public class MultiMemberInsurancePremium
{
private IPolicyRepository policyRepository;
private IMemberRepository memberRepository;
private IMultiMemberPremiumCalculator calculator; // domain service
public Quote GetQuote(int policyId, IEnumerable<int> memberIds)
{
var mainPolicy = policyRepository.Get(policyId);
var members = memberRepository.Get(memberIds);
// ส่ง entity เข้า domain service
return calculator.CalculatePremium(mainPolicy, members);
}
}

Application Service ทำ​หน้าที่ “ประกอบ” Domain Service กับ Entity เข้า​ด้วย​กัน — ตรง​ไป​ตรง​มา แต่​ความ​สะดวก​นี้​ไม่​ได้​มี​ให้​เสมอ โดย​เฉพาะ​เมื่อ Entity เอง​ดูเหมือน​ต้อง “พึ่ง” Domain Service และ​ต้อง​ประสาน​งาน​กัน​ภายใน domain model

บาง​ครั้ง Entity ต้อง​ใช้ Domain Service เพื่อ​ทำ​พฤติกรรม​ของ​มัน​เอง จน​ไม่​อาจ​ประกอบ​กัน​ใน Application Service ได้ ตัวอย่าง​คลาสสิก​คือ RestaurantBooking ที่​เมื่อ​ลูกค้า​ยืนยัน​การ​จอง ต้อง​เรียก IRestaurantNotifier เพื่อ​แจ้ง​ทาง​ร้าน

RestaurantBooking.cs
public class RestaurantBooking
{
public void ConfirmBooking()
{
// restaurantNotifier คือ domain service
// จะให้มันอยู่ใน scope ได้อย่างไร โดยเฉพาะเมื่อใช้ ORM โหลด entity?
restaurantNotifier.NotifyBookingConfirmation(Restaurant, Customer, Id, BookingDetails);
}
}

ความ​ท้าทาย​คือ​ทำให้ restaurantNotifier อยู่​ใน scope ของ ConfirmBooking() ถ้า​เรา​คุม​การ​สร้าง object เอง​ก็​ฉีด​ผ่าน constructor ได้​ง่าย แต่​ใน​ชีวิต​จริง​มัก​มี ORM มา​จัดการ life cycle ของ Entity ทำให้​เรา​ไม่​ได้​เป็น​ผู้​สร้าง object เอง มี​หลาย​เทคนิค​ที่​ใช้​แก้​ปัญหา​นี้:

1. ต่อ​สาย​ด้วย​มือ (Manually Wiring Up) — ใช้ FactoryFactoryตัว​ที่​ห่อ​หุ้ม​ตรรกะ​การ “สร้าง” Aggregate ที่​ซับซ้อน และ​รับประกัน​ว่าอ็อบเจ็กต์ใหม่​ถูกต้อง​ตาม invariant ตั้งแต่​แรก​เกิด — “สร้าง​ของ​ใหม่” (repository หา “ของ​เก่า”)Tactical Design method สร้าง Entity เอง​แล้ว​ส่ง Domain Service เข้า constructor ตรงๆ วิธี​นี้​สะอาด​ดี​เมื่อ​ไม่มี ORM แต่​ถ้า ORM เป็น​ผู้​โหลด Entity จาก​ฐาน​ข้อมูล จะ​ต้อง​ทำ two-phase construction (เรียก Init() หลัง ORM สร้าง​เสร็จ) ซึ่ง​เสี่ยง​ต่อ​การ​ลืม​เรียก จน​เกิด state ไม่​สมบูรณ์​ใน production

2. Dependency Injection — ใส่ Domain Service เป็น constructor parameter แล้ว​ให้ IoC container ต่อ​สาย​ให้ ประหยัด​งาน​สร้าง object เอง แต่​ปัญหา​เรื่อง ORM ก็​ยัง​อาจ​ค้าง​อยู่

3. Service Locator — ให้ Entity ดึง dependency จาก IoC container เอง แก้​ปัญหา ORM ได้ แต่​ต้อง​แลก​ด้วย​ราคา​ที่​สูง คือ Entity ผูก​แน่น​กับ ServiceLocator (infrastructure ที่​ไม่​ควร​ปน​กับ domain), ทดสอบ​ยาก​เพราะ​ต้อง mock ซ้อน mock, และ​ซ่อน dependency ไม่​ให้​เห็น​ผ่าน constructor — เป็น pattern ที่​ค่อน​ข้าง​เป็น​ที่​ถกเถียง

4. Double Dispatch — แทนที่​จะ​ส่ง Domain Service เข้า constructor ก็​ส่ง​เป็น argument ของ method แล้ว Entity ส่งตัวเอง​กลับ​เข้าไป​ใน method ของ Domain Service

DoubleDispatch.cs
public void ConfirmBooking(IRestaurantNotifier restaurantNotifier)
{
Confirmed = restaurantNotifier.NotifyBookingConfirmation(this);
}

วิธี​นี้​ไม่​เป็น​ที่​นิยม​นัก เพราะ​บาง​คน​มอง​ว่าการ​บังคับ​ให้​ผู้​เรียก​ต้อง​จัดหา dependency เป็น​ความ​รับผิดชอบ​ที่​ไม่​ควร​ตก​เป็น​ของ​เขา และ dependency ควร​ส่ง​ผ่าน constructor ไม่ใช่​กลาย​มา​เป็น​ส่วน​หนึ่ง​ของ method signature ที่​ควร​คงที่

Entity ควร​รู้จัก Domain Service ด้วย​ซ้ำ​หรือ​ไม่?

เทคนิค​ข้อ 1–4 ล้วน​พยายาม “ทำให้ Entity พึ่ง Domain Service ได้” ซึ่ง​เป็น​แนวทาง​ที่​ถกเถียง​กัน​มาก​ใน​ชุมชน DDD — หลาย​คน​มอง​ว่า​มัน​เป็น​ไอเดีย​ที่​ไม่​ดี​ตั้งแต่​ต้น สุดท้าย​คุณ​ต้อง​ใช้ context, ความ​ชอบ​ส่วนตัว และ​ประสบการณ์​ใน​การ​เลือก

อีก pattern หนึ่ง​ที่ เลี่ยง​การ​ฉีด Domain Service เข้า Entity ได้​ทั้งหมด คือ Domain EventDomain Eventสิ่ง​ที่ “เกิด​ขึ้น​แล้ว” ใน domain ซึ่ง​ส่วน​อื่น​สนใจ แทน​ด้วย​อ็อบเจ็กต์ที่​เปลี่ยนแปลง​ไม่​ได้ ตั้ง​ชื่อ​เป็น​อดีต เช่น CargoWasRouted ใช้​สื่อสาร​ข้าม AggregateTactical Design — เมื่อ​เกิด​เหตุการณ์​สำคัญ Entity แค่ “ปล่อย event” ออก​มา แล้ว subscriber (event handler) ที่​ลง​ทะเบียน​ไว้​จะ​เป็น​ผู้​เรียก Domain Service เอง

BookingWithDomainEvent.cs
// entity แค่ปล่อย event — ไม่รู้จัก domain service เลย
public class RestaurantBooking
{
public void ConfirmBooking()
{
DomainEvents.Raise(new BookingConfirmedByCustomer(this));
}
}
// event handler (subscriber) เป็นที่อยู่ของ domain service
public class NotifyRestaurantOnCustomerBookingConfirmation
: IHandleEvents<BookingConfirmedByCustomer>
{
private IRestaurantNotifier restaurantNotifier;
public void Handle(BookingConfirmedByCustomer @event)
{
var booking = @event.RestaurantBooking;
booking.Confirmed = restaurantNotifier.NotifyBookingConfirmation(
booking.Restaurant, booking.Customer, booking.Id, booking.BookingDetails);
}
}

ข้อดี​คือ Entity ไม่รู้จัก Domain Service เลย จึง​แยก​ขาดกัน​อย่าง​แท้จริง ส่วน​ข้อ​เสีย​ที่​เห็น​ชัด​คือ ตรรกะ​ถูก​กระจาย​ไป​อยู่​สอง​ที่ — ระหว่าง Entity กับ event handler ขณะ​ที่ pattern อื่น​จะ​เป็น code ที่​ไหล​ต่อ​เนื่อง​เป็น​ลำดับ​เดียว​จบ เรา​จะ​ลง​ลึก​เรื่อง Domain Event ใน​บท​ถัด​ไป

เครื่องมือ: เลือก​ตาม​บริบท

ไม่มี​ทางออก​ใด​สมบูรณ์​แบบ — manual wiring, DI, service locator, double dispatch ต่าง​มี​ข้อ​แลกเปลี่ยน ส่วน Domain Event ให้การ​แยก​ขาด​ที่​ดี​ที่สุด​แต่​ก็​กระจาย​ตรรกะ เลือก​ตาม context ของ​ทีม​และ project คุณ


  • บาง​ครั้ง​พฤติกรรม ไม่​เป็น​ของ Entity หรือ Value Object ตัว​ใด​เลย แต่​ยัง​เป็น​แนวคิด​สำคัญ​ของ domain — นั่น​คือ​สัญญาณ​ของ Domain Service
  • Domain Service แทน​แนวคิด​ของ domain และ​เป็น​ส่วน​หนึ่ง​ของ Ubiquitous Language
  • Domain Service มัก orchestrate entity/value object ใน​รูป​ของ operation ที่​ไร้​สถานะ
  • มาก​ไป → Anemic Domain Model ที่​ไม่​ตรง​กับ problem domain; น้อย​ไป → ตรรกะ​ไป​อยู่​ผิด​ที่​บน Entity จน​แนวคิด​ปะปน​กัน
  • Domain Service ใช้​เป็น contract ได้ โดย interface อยู่​ใน domain model แต่ implementation อยู่​ใน service layer
  • เมื่อ Entity ต้อง​พึ่ง Domain Service มี​หลาย​ทาง​เลือก: DI, double dispatch, และ Domain Event
หัวใจ​ของ​บท​นี้

Domain Service = พฤติกรรม​ที่ ไร้​สถานะ และ แทน​แนวคิด​ของ domain ใช้​เมื่อ​ตรรกะ​ไม่​เข้า​กับ Entity/Value Object ตัว​ใด​เลย จุด​ที่​ต่าง​จาก Application Service คือ Domain Service มี​กฎ​ธุรกิจ​และ​มี​อย่าง​น้อย interface อยู่​ใน domain model — ใช้​แต่​พอดี อย่า​มาก​จน​กลาย​เป็น anemic และ​อย่า​น้อย​จน​ต้อง​ยัด​ตรรกะ​ผิด​ที่


🔗 อ้างอิง​เพิ่มเติม​ใน DevIQ

เจาะ​ลึก​แนวคิด​ใน​บท​นี้​ต่อ​ได้ที่​คลัง​อ้างอิง DevIQ:

  • Anemic Domain Model — ขยาย​ความ​กับดัก​ที่​บท​นี้​เตือน​ไว้ เมื่อ​ผลัก​ตรรกะ​เข้า Domain Service มาก​เกิน​ไป​จน Entity เหลือ​แต่ getter/setter
  • Strategy — รูปแบบ​เบื้องหลัง IGamingScorePolicy/IDiscountPolicy ที่​ฉีด​นโยบาย​เข้าไป​แทน​กัน​ได้​ตาม​แคมเปญ
  • Domain Events — ทาง​เลือก​ที่​ให้ Entity ปล่อย event แทน​การ​เรียก Domain Service ตรงๆ ตาม​หัวข้อ “แยก​ขาด​ด้วย Domain Event” ใน​บท​นี้
  • Service Locator — หนึ่ง​ใน​สี่​เทคนิค​ที่​บท​นี้​เปรียบเทียบ สำหรับ​ดึง Domain Service มา​ใช้​ภายใน Entity พร้อม​ข้อ​แลกเปลี่ยน​ที่​ต้อง​ระวัง
  • Factory Method — เทคนิค “ต่อ​สาย​ด้วย​มือ” ใน​บท​นี้​ใช้ factory สร้าง Entity แล้ว​ฉีด Domain Service เข้า constructor ตรงๆ

เช็กความเข้าใจ — บทที่ 17

ข้อ 1 / 4

ลักษณะเด่นสองข้อที่ใช้แยก Domain Service ออกจาก service ชนิดอื่นคืออะไร?