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
หัวข้อที่มีชื่อว่า “ทำความเข้าใจ Domain Service”ในเชิงแนวคิด 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 ใช้เพื่อ ประสานงาน entity และห่อหุ้มนโยบายธุรกิจ (business policy) ไม่ใช่เพื่อทำงาน “เดินสายท่อ” ทางเทคนิค (infrastructural plumbing) — งานเดินสายท่อให้ปล่อยเป็นหน้าที่ของ Application Service
เมื่อไรจึงควรใช้ Domain Service
หัวข้อที่มีชื่อว่า “เมื่อไรจึงควรใช้ Domain Service”คุณคุยกับ domain expert เรื่องแนวคิดหนึ่งที่ “เกี่ยวข้องกับหลาย Entity” แต่ไม่แน่ใจว่า Entity ตัวไหน “เป็นเจ้าของ” พฤติกรรมนั้น มันไม่เข้ากับใครเลย และพอพยายามฝืนใส่ลงไปก็รู้สึกไม่ลงตัว — รูปแบบความคิดเช่นนี้คือตัวบ่งชี้ที่ชัดเจนว่าถึงเวลาของ Domain Service
ห่อหุ้มนโยบายและกระบวนการธุรกิจ (business process)
หัวข้อที่มีชื่อว่า “ห่อหุ้มนโยบายและกระบวนการธุรกิจ (business process)”ลองนึกถึงระบบเกมออนไลน์ที่มี Entity ชื่อ Competitor (ผู้แข่งขัน) สองคน และ OnlineDeathmatch (การแข่งหนึ่งแมตช์) เมื่อจบแมตช์ คะแนนของผู้เล่นแต่ละคนต้องถูกอัปเดตตามผลการแข่ง
ทางเลือกแรกคือเอา “ตรรกะคำนวณคะแนนและรางวัล” ไปวางไว้ใน OnlineDeathmatch เลย แต่นั่นมีปัญหาใหญ่: กฎเหล่านี้ยืดหยุ่นมาก — ธุรกิจอาจอยากแจกคะแนนสองเท่าในบางช่วง หรือแจกรางวัลพิเศษตามแคมเปญ และโปรโมชันชุดเดียวกันมักถูกนำไปใช้กับเกมหลายประเภท เช่น TeamBattle, CaptureTheFlag, GangCrusade ฯลฯ
นี่คือสัญญาณว่า “การคิดคะแนน” และ “การให้รางวัล” เป็นแนวคิดสำคัญในตัวมันเอง ควรแยกออกมาเป็น Domain Service ที่ถือ single responsibility ของนโยบายนั้นๆ — แล้วฉีด (inject) เข้าไปใช้ใน OnlineDeathmatch หรือ object อื่นที่ต้องการได้
// interface ของ domain service — เป็นส่วนหนึ่งของ Ubiquitous Languagepublic interface IGamingScorePolicy{ void Apply(IGame game);}
public interface IGamingRewardPolicy{ void Apply(IGame game);}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
ในร้านค้าออนไลน์ “วิธีคิดราคาหลังหักส่วนลด” ก็เป็นแนวคิดที่ยืดหยุ่นแบบเดียวกัน — บางสัปดาห์เป็น “ลด 10%” บางช่วงเป็น “ซื้อ 1 แถม 1” การฝังตรรกะนี้ลงใน Order ตรงๆ จะทำให้ Aggregate รกและแบกความรับผิดชอบเกินตัว เราจึงแยกเป็น Domain Service เช่น IDiscountPolicy แล้วฉีดเข้าไป ทำให้สลับโปรโมชันได้โดยไม่ต้องแก้ Order และทำให้ “นโยบายส่วนลด” กลายเป็นแนวคิดที่มองเห็นได้ใน domain
Domain Service ในฐานะ “สัญญา” (Contract)
หัวข้อที่มีชื่อว่า “Domain Service ในฐานะ “สัญญา” (Contract)”อีกกรณีใช้งานกว้างๆ ของ Domain Service คือการเป็น contract — แนวคิดนั้นสำคัญต่อ domain แต่ตัว implementation ต้องพึ่ง infrastructure ที่ ใช้ใน domain model ไม่ได้
ตัวอย่างคลาสสิกคือ ShippingRouteFinder ที่ต้องยิง HTTP request ไปยัง routing API เพื่อหาเส้นทางที่เป็นไปได้ของการเดินทาง
// interface ของ domain service — อยู่ใน domain model (นี่คือ "สัญญา")public interface IShippingRouteFinder{ Route FindFastestRoute(Location departing, Location destination, DateTime departureDate);}
// implementation — อยู่ใน service layer เพราะยิง HTTP เป็นเรื่อง infrastructurepublic 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” พอดี
กายวิภาคของ Domain Service
หัวข้อที่มีชื่อว่า “กายวิภาคของ Domain Service”ลองดูตัวอย่าง RomanceOMeter จากระบบเว็บหาคู่ ที่ใช้ประเมินว่าคนสองคนเข้ากันได้แค่ไหน:
// domain service — เป็นส่วนหนึ่งของ Ubiquitous Languagepublic 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 หลายตัว
ระวังอย่าให้กลายเป็น Anemic Domain Model
หัวข้อที่มีชื่อว่า “ระวังอย่าให้กลายเป็น Anemic Domain Model”เมื่อยอมรับแล้วว่าไม่ใช่ทุกตรรกะที่ต้องอยู่บน 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 Service ต่างจาก Application Service อย่างไร
หัวข้อที่มีชื่อว่า “Domain Service ต่างจาก Application Service อย่างไร”นี่คือจุดที่นักพัฒนาสับสนกันมากที่สุด แต่พอเข้าใจบทบาทเชิงแนวคิดของทั้งคู่แล้วจะไม่สับสนอีกเลย:
| Domain Service | Application 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 เดียวจบ
การเรียกใช้ Domain Service
หัวข้อที่มีชื่อว่า “การเรียกใช้ Domain Service”จาก service layer (กรณีง่าย)
หัวข้อที่มีชื่อว่า “จาก service layer (กรณีง่าย)”กรณีง่ายที่สุด: Application Service ดึง Entity ที่ต้องการออกจาก RepositoryRepositoryตัวที่ทำให้เราเข้าถึง Aggregate ราวกับเป็น collection ในหน่วยความจำ ซ่อนรายละเอียดฐานข้อมูล มี1 repository ต่อ1 Aggregate RootTactical Design แล้วส่งเข้า Domain Service
// application servicepublic 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
จากภายใน domain model (กรณีถกเถียง)
หัวข้อที่มีชื่อว่า “จากภายใน domain model (กรณีถกเถียง)”บางครั้ง Entity ต้องใช้ Domain Service เพื่อทำพฤติกรรมของมันเอง จนไม่อาจประกอบกันใน Application Service ได้ ตัวอย่างคลาสสิกคือ RestaurantBooking ที่เมื่อลูกค้ายืนยันการจอง ต้องเรียก IRestaurantNotifier เพื่อแจ้งทางร้าน
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
public void ConfirmBooking(IRestaurantNotifier restaurantNotifier){ Confirmed = restaurantNotifier.NotifyBookingConfirmation(this);}วิธีนี้ไม่เป็นที่นิยมนัก เพราะบางคนมองว่าการบังคับให้ผู้เรียกต้องจัดหา dependency เป็นความรับผิดชอบที่ไม่ควรตกเป็นของเขา และ dependency ควรส่งผ่าน constructor ไม่ใช่กลายมาเป็นส่วนหนึ่งของ method signature ที่ควรคงที่
เทคนิคข้อ 1–4 ล้วนพยายาม “ทำให้ Entity พึ่ง Domain Service ได้” ซึ่งเป็นแนวทางที่ถกเถียงกันมากในชุมชน DDD — หลายคนมองว่ามันเป็นไอเดียที่ไม่ดีตั้งแต่ต้น สุดท้ายคุณต้องใช้ context, ความชอบส่วนตัว และประสบการณ์ในการเลือก
แยกขาดด้วย Domain Event
หัวข้อที่มีชื่อว่า “แยกขาดด้วย Domain Event”อีก pattern หนึ่งที่ เลี่ยงการฉีด Domain Service เข้า Entity ได้ทั้งหมด คือ Domain EventDomain Eventสิ่งที่ “เกิดขึ้นแล้ว” ใน domain ซึ่งส่วนอื่นสนใจ แทนด้วยอ็อบเจ็กต์ที่เปลี่ยนแปลงไม่ได้ ตั้งชื่อเป็นอดีต เช่น CargoWasRouted ใช้สื่อสารข้าม AggregateTactical Design — เมื่อเกิดเหตุการณ์สำคัญ Entity แค่ “ปล่อย event” ออกมา แล้ว subscriber (event handler) ที่ลงทะเบียนไว้จะเป็นผู้เรียก Domain Service เอง
// entity แค่ปล่อย event — ไม่รู้จัก domain service เลยpublic class RestaurantBooking{ public void ConfirmBooking() { DomainEvents.Raise(new BookingConfirmedByCustomer(this)); }}
// event handler (subscriber) เป็นที่อยู่ของ domain servicepublic 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:
- 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 ชนิดอื่นคืออะไร?