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

Application Architecture — สถาปัตยกรรม​ที่​ปกป้อง domain model

จนถึง​ตอน​นี้​คอร์ส​เรา​พูด​ถึง​แต่ “วิธี​สร้าง model” ที่​เข้าใจ problem domain — แต่ model ที่​ดี​จะ​ไร้​ค่า​ทันที หาก​ถูก​กลืน​หาย​ไป​ใน code ของ​ฐาน​ข้อมูล framework และ UI บท​นี้​จึง​เปลี่ยน​คำถาม​จาก “จะ​สร้าง model อย่างไร” เป็น “จะ วาง model ไว้​ตรง​ไหน ใน app จริง เพื่อ​ให้​มัน​อยู่​รอด​และ​ไม่​ถูก​ปน​เปื้อน”

หัวใจ​ของ​บท​นี้​สั้น​มาก: Domain-Driven DesignDomain-Driven Design (DDD)แนวทาง​พัฒนา​ซอฟต์แวร์​ที่​เน้น​สร้าง domain model ซึ่ง​เข้าใจ​กฎ​และ​กระบวนการ​ของ​ธุรกิจ​อย่าง​ลึกซึ้ง แล้ว​เขียน​ลง​ใน code จริง บัญญัติ​โดย Eric Evans (2003) แบ่ง​เป็น Strategic และ Tactical DesignStrategic Design ไม่​บังคับ ให้​คุณ​ใช้​สถาปัตยกรรม​แบบ​ใด​แบบ​หนึ่ง แต่​สถาปัตยกรรม​ที่​คุณ​เลือก ต้อง​ทำ​สิ่ง​เดียว​ให้​ได้ — แยก domain logic ออก​จาก technical concern อย่าง​เด็ดขาด ถ้า​ทำ​ไม่​ได้ codebase จะ​ค่อยๆ เสื่อม​กลับ​ไป​เป็น Big Ball of MudBig Ball of Mudระบบ​ที่​ปนเป​กัน​ยุ่งเหยิง​ไม่มี​ขอบเขต​ชัด ให้​ลาก​เส้น​ล้อม​ไว้​แล้ว​ยอมรับ​สภาพ ไม่​พยายาม​ทำ model ละเอียด​ใน​นั้น และ​ระวัง​ไม่​ให้​ลาม​ไป context อื่นStrategic Design เหมือน​เดิม


code ใน app หนึ่งๆ มี concern หลาย​ชนิด​ที่ เปลี่ยน​ด้วย​อัตรา​ต่าง​กัน​และ​ด้วย​เหตุผล​ต่าง​กัน — presentation (UI) เปลี่ยน​บ่อย​ตาม design, persistence เปลี่ยน​ตาม​เทคโนโลยี​ฐาน​ข้อมูล, ส่วน domain logic เปลี่ยน​ตาม​กฎ​ธุรกิจ ถ้า​ทั้ง​สาม​ปน​กัน​อยู่​ใน file เดียว การ​แก้ design ปุ่ม​อาจ​ไป​ทำ​กฎ​ธุรกิจ​พัง​โดย​ไม่​ตั้งใจ

DDD ต้องการ​สถาปัตยกรรม​ที่​ทำ​สอง​อย่าง:

  1. Separation of concerns — แยก​ความ​ซับซ้อน​ทาง​เทคนิค​ออก​จาก​ความ​ซับซ้อน​ของ domain ให้​แต่ละ​ส่วน​เปลี่ยน​ได้​โดย​ไม่​กระทบ​กัน
  2. Abstraction จาก​รายละเอียด​ของ domain — เปิด​ให้​โลก​ภายนอก​เรียก​ใช้​ผ่าน​ชุด use case แบบ coarse-grained (หยาบ​ใน​ระดับ​ธุรกิจ) แทนที่​จะ​ให้ client เข้าไป​ยุ่ง​กับ domain object โดยตรง
แก่น​ของ​ทั้ง​บท

สถาปัตยกรรม​ใน​มุมมอง DDD มีหน้าที่​เดียว: กัน domain model ไว้​ตรง​ใจกลาง ไม่​ให้​รายละเอียด​ทาง​เทคนิค​เข้า​มา​แตะ​ต้อง ไม่​ว่า​จะ​เป็น Layered, Hexagonal, Onion หรือ Clean Architecture — ทุก​แบบ​ล้วน​รับใช้​เป้าหมาย​เดียวกัน​นี้


วิธี​ที่​คลาสสิก​ที่สุด​ใน​การ​แยก concern คือ​การ​แบ่ง app เป็น ชั้น (layer) ตาม​ความ​รับผิดชอบ Martin Fowler รวบรวม​รูปแบบ Layered ArchitectureLayered Architectureการ​แยก code เป็น​ชั้น (UI, Application, Domain, Infrastructure) โดย​รวม code ที่​เกี่ยว​กับ domain ไว้​ชั้น​เดียว​และ​แยก​ออก​จาก​ชั้น​อื่น เป้าหมาย​คือ​การ​แยก​ส่วน (isolation)Architecture ไว้​ใน​หนังสือ Patterns of Enterprise Application Architecture และ​ยัง​มี​สถาปัตยกรรม​อื่น​ที่​เป้าหมาย​เดียวกัน เช่น Clean ArchitectureClean Architectureแนวคิด​ของ Robert C. Martin ที่​หลักการ​ตรง​กับ Hexagonal/Onion — dependency ทั้งหมด​ชี้​เข้าหา​แกน​กลาง​ที่​เป็น​กฎ​ธุรกิจ ทำให้ domain ไม่​ผูก​กับ frameworkArchitecture ของ Uncle Bob (Robert C. Martin), Hexagonal ArchitectureHexagonal Architectureสถาปัตยกรรม “Ports and Adapters” โดย Alistair Cockburn — วาง domain ไว้​ตรง​กลาง นิยาม port (interface) ให้ adapter ภายนอก​มา​เสียบ ทุก dependency ชี้​เข้าหา domainArchitecture (Ports and Adapters) และ Onion ArchitectureOnion Architectureสถาปัตยกรรม​โดย Jeffrey Palermo — code พึ่งพา​ชั้น​ที่​อยู่ “ด้าน​ใน​กว่า” ได้ แต่​พึ่งพา​ชั้น​นอก​ไม่​ได้ มี domain เป็น​แกน​กลาง​สุด แนวคิด​เดียว​กับ HexagonalArchitecture

แต่​ภาพ​ของ DDD ต่าง​จาก layered architecture แบบ​เรียง​ชั้น​ทั่วไป — รูป​ที่ 8-1 วาด​มัน​เป็น วงกลม​ซ้อน​กัน โดย​มี domain layer อยู่​ใจกลาง

รูปที่ 8-1: Layered Architecture แบบ DDD — domain layer อยู่แกนกลาง ห่อด้วย application layer แล้วจึงเป็น infrastructure (presentation, persistence, web services, messaging, SMTP) ที่อยู่วงนอกสุด

  • Domain Layer (แกน​กลาง) — บรรจุ​ตรรกะ​ธุรกิจ​ทั้งหมด ไม่รู้จัก​และ​ไม่​ขึ้น​กับ​เทคโนโลยีใดๆ เลย
  • Application Layer (ห่อ​รอบ domain) — เปิด API ระดับ​หยาบApplication Serviceบริการ​ที่ “ประสาน​งาน” use case — จัดการ transaction, ความ​ปลอดภัย, การ​บันทึก, แปลง DTO — แต่​ไม่มี​กฎ​ธุรกิจ​อยู่​ใน​ตัว​มัน​เองTactical Design ที่​แทน business use case โดย​ซ่อน​รายละเอียด​ระดับ​ล่าง​ของ domain เอา​ไว้
  • Infrastructure Layers (วงนอก) — รายละเอียด​ทาง​เทคนิค​ล้วนๆ: UI, ฐาน​ข้อมูล, web service, การ​ส่ง​อีเมล

จุด​สำคัญ​คือ domain layer และ application layer ถูก isolate และ​ปกป้อง จาก accidental complexity ของ client framework และ infrastructure ทั้งหมด


ทำไม​วงใน​ถึง​ไม่​ถูก​ปน​เปื้อน? เพราะ​กฎ​เหล็ก​ข้อ​เดียว — dependency ทุก​เส้น​ต้อง​ชี้​เข้าหา​แกน​กลาง domain layer ที่​อยู่​ใจกลาง​ไม่​พึ่งพา​อะไร​เลย จึง​โฟกัส​ที่​เรื่อง domain ได้​แบบ​ไร้​สิ่ง​รบกวน ส่วน application layer พึ่งพา​เฉพาะ domain layer เท่านั้น

รูปที่ 8-2: Dependency inversion ใน Layered Architecture — application layer เป็นฝ่ายนิยาม interface ที่ตัวเองต้องการ แล้ว infrastructure วงนอกเป็นฝ่ายมา implement ตาม ทำให้ลูกศร dependency ชี้กลับเข้าด้านใน

แล้ว state ของ domain object ที่​ต้อง​บันทึก​ลง​ฐาน​ข้อมูล​ล่ะ? นี่​คือ​จุด​ที่ Dependency Inversion เปล่ง​ประกาย แทนที่ domain จะ​ไป​เรียก​ฐาน​ข้อมูล (ซึ่ง​จะ​ทำให้ domain ผูก​ติด​กับ​เทคนิค) เรา​กลับ​ทิศ​ความ​สัมพันธ์​เสีย​ใหม่:

  1. Application layer เป็น​ฝ่าย​นิยาม interface — เขียน​ด้วย​ภาษา​และ​มุมมอง​ของ application เอง ปลอด​จาก​ศัพท์​เทคนิค​ของ framework ใดๆ เช่น interface ที่​บอกว่า “ขอ hydrate และ persist domain object”
  2. Infrastructure layer เป็น​ฝ่าย implement interface นั้น — มา​เสียบ​เข้า​กับ PortPortinterface ที่ domain นิยาม​ไว้​เพื่อ​บอกว่า “ต้องการ​อะไร” เช่น CargoRepository โดย​ไม่​สน​ว่า​ใคร​จะ​มา​ทำให้ — โลก​ภายนอก​ต้อง​ปรับ​ตัว​เข้าหา ไม่ใช่​กลับ​กันArchitecture ที่​วงใน​เปิด​ไว้​ใน​ฐานะ AdapterAdapterตัว​ที่ “ทำให้” port เป็น​จริง เช่น PostgresCargoRepository หรือ REST controller อยู่​ชั้น​นอก​และ​พึ่งพา domain ทำให้​สลับ​เทคโนโลยี​ได้​โดย​ไม่​แตะ domainArchitecture

เรื่อง transaction management, security, logging (cross-cutting concern) ก็​จัดการ​ด้วย​วิธี​เดียวกัน

🚢 Cargo Shipping — interface เป็น​ของ domain

ใน Cargo Shipping ฝั่ง domain เรา​นิยาม CargoRepositoryRepositoryตัว​ที่​ทำให้​เรา​เข้าถึง Aggregate ราวกับ​เป็น collection ใน​หน่วย​ความ​จำ ซ่อน​รายละเอียด​ฐาน​ข้อมูล มี1 repository ต่อ1 Aggregate RootTactical Design ใน​ฐานะ interface ที่ อยู่​ใน​วงใน — มัน​พูด​ภาษา​ธุรกิจ​ล้วนๆ: find(trackingId), store(cargo) ไม่มี​คำ​ว่า SQL หรือ table อยู่​เลย

ส่วน SqlCargoRepository ที่​รู้จัก connection string และ​ตาราง cargo ถูกวาง​ไว้​ใน​วง infrastructure แล้ว​มา implement interface นั้น ผล​คือ​เรา​สลับ​จาก SQL ไป NoSQL ได้​โดย ไม่​ต้อง​แตะ domain layer แม้แต่​บรรทัด​เดียว

domain/CargoRepository.ts
// interface นี้อยู่ "วงใน" — domain เป็นฝ่ายกำหนดว่าตัวเองต้องการอะไร
// โลกภายนอกต้องปรับตัวมาทำตาม ไม่ใช่กลับกัน
interface CargoRepository {
find(trackingId: TrackingId): Cargo | null;
store(cargo: Cargo): void;
}

ตาม​ที่​เรา​เรียน​ใน Model-Driven Design domain modelDomain ModelModel ของ Domain ที่​ฝัง​กฎ​และ​กระบวนการ​ทาง​ธุรกิจ​ไว้​ใน code จริง หัวใจ​ของ DDD คือ​การ​พัฒนา domain model ที่ “เข้าใจ​ธุรกิจ​อย่าง​ลึกซึ้ง”Strategic Design คือ​ภาพ​นามธรรม​ของ problem domain ที่​สร้าง​ขึ้น​เพื่อ​ตอบ business use case domain layer ที่​บรรจุ model นี้ ไม่​พึ่งพา​อะไร​เลย และ​ไม่​สนใจ​ว่า client เป็น​ใคร​หรือ​ข้อมูล​ถูก​เก็บ​ที่ไหน

application service layerApplication Serviceบริการ​ที่ “ประสาน​งาน” use case — จัดการ transaction, ความ​ปลอดภัย, การ​บันทึก, แปลง DTO — แต่​ไม่มี​กฎ​ธุรกิจ​อยู่​ใน​ตัว​มัน​เองTactical Design แทน use case และ​กระบวนการ​ธุรกิจ (business process) ของ app มัน​ทำงาน​ที่​ระดับ abstraction สูง​กว่า domain object — เปิด​บริการ​ระดับ​หยาบ​ใน​เชิง​ธุรกิจ และ​ซ่อน​รายละเอียด​ของ domain ไว้​หลัง façade บอกว่า “ระบบ​ทำ​อะไร​ได้” แต่​ไม่​บอก “ทำ​อย่างไร” ผล​คือ​เรา​พัฒนา model ด้าน​ใน​ต่อ​ไป​ได้​โดย​ไม่​กระทบ client

มุมมอง​ที่​ทรง​พลัง​มาก​คือ: application layer ทำตัว​เป็น Anticorruption LayerAnticorruption Layer (ACL)ชั้น​กั้น​ที่​ทีม​ปลาย​น้ำ​สร้าง​ขึ้น​เพื่อ​แปลง model ของ​ระบบ​อื่น​ให้​เป็น​ภาษา​ของ model ตัวเอง ป้องกัน​ไม่​ให้ model ภายนอก​มา “ปน​เปื้อน” model เราStrategic Design — client ใหม่​ทุก​ตัว​ต้อง​ปรับ​ตัว​เข้าหา contract (API) ของ app และ​ต้อง​แปลง output เอง ไม่ใช่​ให้ app ไป​ปรับ​ตามใจ client วิธี​นี้​ทำให้ domain layer “บริสุทธิ์” ไม่​ถูก​รายละเอียด​ทาง​เทคนิค​ภายนอก​มา​แปดเปื้อน

infrastructure คือ​สิ่ง​ที่​ทำให้ app “ทำงาน​ได้​จริง”: การ​เปิด​ให้​คน​เข้าถึง​ผ่าน UI หรือ​ให้​ระบบ​อื่น​เข้าถึง​ผ่าน web service/message endpoint, การ implement การ​จัด​เก็บ state, logging, security, notification และ​การ integrate กับ bounded context อื่น ทั้งหมด​นี้​เป็น “รายละเอียด​ภายนอก” ที่​ไม่​ควร​กระทบ use case และ domain logic โดยตรง


เมื่อ​ข้อมูล​วิ่ง​ข้าม​ขอบเขต​ของ​ชั้น มี​กฎ​สอง​ข้อ:

  • อย่า​ส่ง domain object ออก​ไป​ข้าม​ขอบเขต — เพราะ​นั่น​คือ​การ​เปิดเผย​โครงสร้าง​ภายใน​ของ model ให้​โลก​ภายนอก
  • อย่า​ปล่อย​ข้อมูล​ดิบ​หรือ user input ตรง​เข้า domain layer — ต้อง​ผ่าน​การ​แปลง​ก่อน​เสมอ

แทนที่​จะ​ส่ง domain object เรา​ใช้ DTO (Data Transfer Object), presentation model และ application event object เป็น​ตัวกลาง​สื่อสาร​การ​เปลี่ยนแปลง ชั้น​ที่​อยู่​สูง​กว่า​ต้อง​ปรับ​ตัว​เข้าหา message type ของ​ชั้น​ที่​ต่ำ​กว่า ทำให้​วงใน​ยัง​คง isolate และ loosely coupled

application/BookCargoService.ts
class BookCargoService {
bookCargo(request: BookCargoRequest): BookingResult {
// 1) แปลง input ดิบ -> Value Object ที่ domain เข้าใจ
const spec = new RouteSpecification(request.origin, request.destination, request.deadline);
const cargo = Cargo.book(this.trackingIdFactory.next(), spec);
this.cargoRepository.store(cargo);
// 2) แปลง domain object -> DTO ก่อนส่งกลับ ไม่ปล่อย Cargo รั่วออกไป
return new BookingResult(cargo.trackingId.value, cargo.routingStatus);
}
}

ผลพลอยได้​ที่​ยอดเยี่ยม​ของ​การ​แยก concern คือ ทดสอบ​ได้​ง่าย​ขึ้น​มาก เมื่อ domain logic ไม่​ผูก​กับ code เชิง​เทคนิค (UI, persistence framework) เรา​ทดสอบ domain และ application logic แบบ isolate ได้

  • Domain layer — ใช้ unit test ยืนยัน​ตรรกะ​ล้วนๆ ได้​เลย ไม่​ต้อง​มี​ฐาน​ข้อมูล
  • Application layer — ใช้ mock/stub แทน external resource เพื่อ​ยืนยัน​ว่า​มัน​ประสาน​งาน (coordinate) ระหว่าง domain กับ infrastructure ถูกต้อง
  • Infrastructure layer — เน้น integration test และ end-to-end test แทน เพราะ​มัน​คือ​จุด​ที่​แตะ​ของ​จริง

การ​แยก concern ต้อง​ลาม​ไป​ถึง persistence ด้วย วิธี integrate ที่​ดู​ง่าย​แต่​อันตราย​คือ การ​ให้​หลาย app ใช้​ฐาน​ข้อมูล​และ schema ชุด​เดียวกัน

กับดัก: shared database = ทาง​ลัด​สู่ Big Ball of Mud

การ share schema เปิด​ช่อง​ให้ client หลบ​เลี่ยง​กลไก​ป้องกัน​ของ Bounded ContextBounded Contextขอบเขต​ที่​ชัดเจน (มัก​เป็น​ระบบ​ย่อย​หรือ​ทีม​หนึ่ง) ที่ model หนึ่ง​ใช้ได้​และ​มี​ความหมาย​แน่นอน ภายใน​ขอบเขต​นี้ Ubiquitous Language สอดคล้อง​กัน 100%Strategic Design ได้​โดย​ง่าย — มัน​เขียน​ทับ state ของ domain object ได้​โดยตรง ข้าม domain logic ที่​ควร​เป็น​ผู้​บังคับ InvariantInvariantกฎ​ทาง​ธุรกิจ​ที่ “ต้อง​เป็น​จริง​เสมอ” ภายใน Aggregate เช่น ยอด​รวม​คำ​สั่ง​ซื้อ​ต้อง​เท่ากับ​ผล​รวม​ของ​รายการ​สินค้า — ต้อง​ถูกต้อง​ภายใน transaction เดียวTactical Design ผล​ที่​ตาม​มา​คือ​ข้อมูล​ที่​ละเมิด​กฎ​ธุรกิจ และ​เส้น​แบ่ง​ของ model ที่​พร่า​เลือน​จน​กลาย​เป็น BBoM

ทาง​ที่​ถูก​คือ favor application/bounded-context database มากกว่า integration database — bounded context สอง​ตัว​อาจ​ใช้​เครื่อง​ฐาน​ข้อมูล​ร่วม​กัน​ได้ แต่ schema ต้อง​แยก​กัน (ใน​อุดมคติ​คือ​ต่าง​ฐาน​ข้อมูล​ไป​เลย) เหมือน​ที่​เรา​ลาก​เส้น context boundary ใน model เรา​ต้อง​ลาก​เส้น​เดียวกัน​กับ persistence model ด้วย เพื่อ​บังคับ​ให้​ทุก​คน integrate ผ่าน application service layer ที่​นิยาม​ไว้​ชัดเจน


app หนึ่ง​ตัว​อาจ​ประกอบ​ด้วย bounded context มากกว่า​หนึ่ง ตัว และ​สถาปัตยกรรม​ก็​ใช้​กับ “app” และ “bounded context” คนละ​แบบ​กัน

graph TD
  UI["Presentation Layer<br/>(มี application layer ของตัวเองเพื่อประสาน context)"]
  UI --> BCA["Bounded Context A<br/>Domain Model Pattern"]
  UI --> BCB["Bounded Context B<br/>Transaction Script"]
  UI --> BCC["Bounded Context C<br/>Table Module"]

สังเกต​ว่า​แต่ละ bounded context เลือก​วิธี​แทน domain logic ของ​ตัวเอง​ได้​อิสระ — context ที่​ตรรกะ​รวย​อาจ​ใช้ Domain Model patternDomain ModelModel ของ Domain ที่​ฝัง​กฎ​และ​กระบวนการ​ทาง​ธุรกิจ​ไว้​ใน code จริง หัวใจ​ของ DDD คือ​การ​พัฒนา domain model ที่ “เข้าใจ​ธุรกิจ​อย่าง​ลึกซึ้ง”Strategic Design ส่วน context ที่​งาน​ง่ายๆ อาจ​ใช้ Transaction ScriptTransaction Scriptรูปแบบ​ที่​จัด​ตรรกะ​ธุรกิจ​เป็น​ชุด​คำ​สั่ง​ตาม​ขั้นตอน (procedural) แทน​การกระจายลง​ใน​อ็อบเจ็กต์ เหมาะ​กับ​งาน​ง่าย ๆ แต่​จะ​รก​เมื่อ domain ซับซ้อน​ขึ้นArchitecture หรือ Table Module ก็ได้

มี​อีก​สำนัก​คิด (Udi Dahan’s business component) ที่​เชื่อ​ว่า​ขอบเขต​ของ bounded context ควร​ขยาย​ไป​ถึง presentation layer ด้วย — คือ​ให้​แต่ละ context เป็น​เจ้าของ​ส่วน UI เฉพาะ​ของ​มัน​เอง โดย​มี infrastructure คอย​จัดการ​การ​สื่อสาร​และ correlation ID

หลักการ​ที่​จำ​ให้​แม่น

ไม่​จำเป็น ต้อง​ใช้​สถาปัตยกรรม​หรือ data store แบบ​เดียวกัน​ทั่ว​ทุก bounded context — แต่ ภายใน bounded context เดียว ควร​ยึด​วิธี​แทน domain logic แบบ​เดียว​ให้​สม่ำเสมอ


นี่​คือ​เส้น​แบ่ง​ที่​คน​สับสน​บ่อย​ที่สุด รูป​ที่ 8-9 แยก​ให้​เห็น​ชัด

รูปที่ 8-9: Application logic เทียบกับ Domain logic — application service ทำหน้าที่ validate input, ดึง domain object จาก data store, มอบหมายการตัดสินใจให้ domain object และแจ้ง event ไปยังระบบอื่น ส่วนการตัดสินใจเชิงธุรกิจอยู่ที่แกน Domain Layer

Application logic = ขั้นตอน workflow เพื่อ​ทำ use case ให้​สำเร็จ เช่น hydrate domain object จาก​ฐาน​ข้อมูล, map user input เป็น object ที่ domain เข้าใจ, มอบหมาย (delegate) ให้ domain ตัดสิน​ใจ, แล้ว​แจ้ง​ระบบ​อื่น​ผ่าน messaging มัน​เป็น​เรื่อง​ของ coordination และ orchestration ผ่าน​การ delegate — application service เอง “ไม่​ลงมือ​ทำงาน​จริง” แต่​รู้​ว่า “ต้อง​คุย​กับ​ใคร” เพื่อ​ให้​งาน​สำเร็จ จึง​ควร​เป็น code แบบ procedural ที่ บาง (thin)

Domain logic = กฎ แนวคิด ข้อมูล และ flow ของ domain ล้วนๆ ปลอด​จาก​รายละเอียด​เทคนิค​รวม​ถึง persistence

🛒 e-commerce — ใส่​คูปอง​ส่วนลด​ลง​ตะกร้า

ลอง​ดู use case “ใช้​คูปอง​กับ​ตะกร้า” ตาม​รูป 8-9:

  1. presentation layer (เช่น ASP.NET MVC) แปลง HTTP request เป็น​รูปแบบ​ที่ application service ต้องการ แล้ว​เรียก service method — นี่​คือ application logic
  2. application service delegate ไป persistence เพื่อ​ดึง object Coupon มา แล้ว​เช็กว่า​ยัง​ใช้ได้​ไหม — coordination = application logic
  3. ถ้า​ใช้ได้ มัน​ดึง Basket มา แล้ว ส่ง basket ให้ coupon เป็น​ฝ่าย​คำนวณ​ส่วนลด​เองการ​คำนวณ​ส่วนลด​คือ domain logic อยู่​ใน domain object
  4. บันทึก​การ​เปลี่ยนแปลง แล้ว publish eventDomain Eventสิ่ง​ที่ “เกิด​ขึ้น​แล้ว” ใน domain ซึ่ง​ส่วน​อื่น​สนใจ แทน​ด้วย​อ็อบเจ็กต์ที่​เปลี่ยนแปลง​ไม่​ได้ ตั้ง​ชื่อ​เป็น​อดีต เช่น CargoWasRouted ใช้​สื่อสาร​ข้าม AggregateTactical Design ว่า coupon ถูก redeem — กลับ​มา​เป็น application logic

เห็น​ไหม​ว่า service เป็น​เพียง “ผู้​ควบคุม​วง” ส่วน​การ​ตัดสิน​ใจ​เชิง​ธุรกิจ​ถูก​ผลัก​เข้าไป​ไว้​ใน domain object เสมอ

อย่า​ให้ domain logic หลุด​เข้าไป​ใน application service

ถ้า​คุณ​พบ​เงื่อนไข if ที่​เป็น​กฎ​ธุรกิจ (เช่น “ส่วนลด​เกิน 50% ต้อง​ให้​ผู้จัดการ​อนุมัติ”) อยู่​ใน application service — นั่น​คือ​สัญญาณ​ว่า​ตรรกะ​กำลัง​รั่วไหล​ออก​จาก domain ปลายทาง​คือ Anemic Domain ModelAnemic Domain Modelanti-pattern ที่อ็อบเจ็กต์มี​แต่ getter/setter ไร้​พฤติกรรม ตรรกะ​ถูก​ดึง​ไป​ไว้​ใน service ทั้งหมด — จ่าย​ค่า domain model แต่​ไม่​ได้​ประโยชน์​ของ​มัน​เลยTactical Design ที่ domain object เหลือ​แต่ getter/setter ส่วน​กฎ​ทั้งหมด​กระจัดกระจาย​อยู่​ใน service


เพราะ application service เปิดเผย ความ​สามารถ​ของ​ระบบ มัน​จึง​ไม่​ควร​เปลี่ยน​ตาม client — client ต่างหาก​ที่​ต้อง​ปรับ​ตัว​เข้าหา contract ของ service capability ของ​ระบบ​ควร​เปลี่ยน ก็​ต่อ​เมื่อ business use case เปลี่ยน เท่านั้น ซึ่ง​เกิด​ด้วย​อัตรา​และ​เหตุผล​ต่าง​จาก domain logic ที่​อยู่​เบื้องหลัง — นี่เอง​ที่​ปกป้อง client จาก​การ​เปลี่ยนแปลง​บ่อยๆ ของ domain

ตัวอย่าง: use case “ประเมิน​ความ​เสี่ยง​ทุจริต​ของออเดอร์” — domain logic ภายใน​อาจ​เปลี่ยน​วิธี​คำนวณ​คะแนน​ไป​เรื่อยๆ แต่ application service ที่​เป็น “หน้าตา” ของ use case นั้น​แทบ​ไม่​เปลี่ยน จะ​เปลี่ยน​ก็​เฉพาะ​ตอน​แก้ contract เพื่อ​ให้​ข้อมูล​เพิ่ม

application service จึง​ควร​ตั้ง​ชื่อ​ให้ เผย​เจตนา​ของ​ผู้​ใช้ ไม่ใช่​ตั้ง​ตาม CRUD — Behavior-Driven Design (BDD) ช่วย​ให้​เรา​จับ​กระบวนการ​ธุรกิจ (business process) ของ​ระบบ แล้ว​นำ​ภาษา​จาก BDD specification มา​ตั้ง​ชื่อ use case เหมือน​ที่​เรา​ใช้ Ubiquitous LanguageUbiquitous Languageภาษา​กลาง​ที่​ทุก​คนใน​ทีม (ทั้ง​นัก​พัฒนา​และ​ผู้เชี่ยวชาญ​ธุรกิจ) ใช้​ร่วม​กัน อิง​กับ domain model โดยตรง คำ​เดียวกัน​ต้อง​หมาย​ถึง​สิ่ง​เดียวกัน และ​สะท้อน​ลง​ใน codeStrategic Design ใน code ของ domain layer

Application Service เผย​ว่า​ต้อง​มี domain model ไหม

method ของ application service เป็น​เครื่อง​ชี้​วัด​ที่​ดี: ถ้า​ทุก use case เป็น​แค่ add/update/delete ข้อมูล​เฉยๆ แสดง​ว่า domain แทบ​ไม่มี​ตรรกะ​จริง — ใช้ Transaction ScriptTransaction Scriptรูปแบบ​ที่​จัด​ตรรกะ​ธุรกิจ​เป็น​ชุด​คำ​สั่ง​ตาม​ขั้นตอน (procedural) แทน​การกระจายลง​ใน​อ็อบเจ็กต์ เหมาะ​กับ​งาน​ง่าย ๆ แต่​จะ​รก​เมื่อ domain ซับซ้อน​ขึ้นArchitecture ก็พอ ไม่​ต้อง​ลงทุน​ทำ rich domain model แต่​ถ้า service และ​กระบวนการ​ธุรกิจ (business process) เต็ม​ไป​ด้วย​ภาษา​เชิง​ธุรกิจ​ที่​รวย นั่น​คือ​สัญญาณ​ว่า​ควร​ใช้ Domain Model pattern


นอกจาก​ประสาน​งาน application service ยัง​ต้อง รายงาน state ของ domain ออก​มา​เป็น view แต่​เรา​ไม่​อยาก​เปิด​โครงสร้าง​ภายใน​ของ model จึง​แปลง domain object เป็น presentation/view model ที่​ให้​เฉพาะ​มุม​ที่​ต้องการ

รูปที่ 8-10: View model ดึงข้อมูลจากหลาย Aggregate — บาง view ต้องการข้อมูลข้าม Aggregate A, B, C การ hydrate domain object ทั้งหมดเพื่อแสดงข้อมูลแค่บางส่วนนั้นแพงและไม่คุ้ม

ปัญหา​คือ บาง view ต้องการ​ข้อมูล​ที่ ข้าม​หลาย AggregateAggregateกลุ่ม​ของ Entity และ Value Object ที่​ถูก​มอง​เป็น​หนึ่ง​หน่วย​เดียว​เพื่อ​รักษา​ความ​ถูกต้อง​ของ​ข้อมูล มี​ขอบเขต​ชัดเจน และ​เป็น​หน่วย​ของ transactionTactical Design การ hydrate domain object ที่​รวย​ทั้ง​ก้อน​เพียง​เพื่อ​แสดง​ข้อมูล​เพียง​บาง​ส่วน​บน​หน้า​จอ​นั้น​สิ้น​เปลือง​โดย​ใช่​เหตุ ทาง​ที่​ดี​กว่า​คือ​ให้ application service ดึง view ออก​มา ตรง​จาก data source โดย​ไม่​ต้อง​ประกอบ object graph ขนาด​ใหญ่

แต่​การ​ให้​ทั้ง read และ write ใช้ model เดียวกัน​มี​ข้อ​เสีย — ความ​ต้องการ​ของ view อาจ​บีบ​ให้​โครงสร้าง​ของ domain object ต้อง​เปลี่ยน​ตาม​ความ​ต้องการ​ของ presentation เพื่อ​กัน​เรื่อง​นี้ เรา​เก็บ​ข้อมูล​สำหรับ​อ่าน​ไว้​ใน schema แยก​ที่​เหมาะ​กับ​การ query โดย​อาจ​อาศัย​การ​บันทึก eventDomain Eventสิ่ง​ที่ “เกิด​ขึ้น​แล้ว” ใน domain ซึ่ง​ส่วน​อื่น​สนใจ แทน​ด้วย​อ็อบเจ็กต์ที่​เปลี่ยนแปลง​ไม่​ได้ ตั้ง​ชื่อ​เป็น​อดีต เช่น CargoWasRouted ใช้​สื่อสาร​ข้าม AggregateTactical Design ที่​เกิด​ใน model มา​เป็น​ฐาน​ของ​รายงาน

รูปแบบ​ที่​แยก “ฝั่ง​เขียน” (transactional model) ออก​จาก “ฝั่ง​อ่าน” (read model) อย่าง​ชัดเจน​นี้​เรียก​ว่า CQRSCQRSCommand Query Responsibility Segregation — แยก model สำหรับ “เขียน/แก้ไข” (command) ออก​จาก model สำหรับ “อ่าน” (query) เหมาะ​กับ domain ซับซ้อน แต่​เพิ่ม​ความ​ซับซ้อน ใช้​อย่าง​ระมัดระวังArchitecture (Command Query Responsibility Segregation) — เรา​จะ​ลง​ลึก​ใน​บท​หลัง


หน้าที่​ของ client คือ “เปิดเผย​ความ​สามารถ​ของ​ระบบ​สู่​ผู้​ใช้” — บาง app มี UI, บาง app เปิด​เป็น REST/web service ไม่​ว่า​แบบ​ไหน หลักการ​เดียว​คือ service ต้อง​ไม่รู้​และ​ไม่​สน​ว่า​ใคร​เรียก​ใช้​มัน application service ไม่​ควร​ดัด​ตัวเอง​ให้​เข้า​กับ client แต่​ควร​เปิด use case แล้ว​บังคับ​ให้ client ปรับ​ตัว​เข้าหา API

รูปที่ 8-13: client หลายแบบเรียกใช้พฤติกรรมของ app ผ่าน application service layer เดียวกัน — ทั้ง presentation, web service, messaging โดยที่ application service เองก็เรียกใช้ context ภายนอกและ third-party service ได้ด้วย

จริงๆ แล้ว​คุณ สร้าง​ระบบ​โดย​ไม่มี application service layer ก็ได้ — ปล่อย​ให้ client ทำงาน​ทั้งหมด​แทน แต่​การ​สร้าง​ชุด service ที่​ชัดเจน​ทำให้​คุณ model use case ออก​มา​เป็น​รูปธรรม และ​แยก​มัน​ออก​จาก​ความ​ต้องการ​ของ presentation ทำให้​โฟกัส​ที่​พฤติกรรม​ของ​ระบบ​และ​แยก domain logic ออก​จาก concern อื่น​ได้​สะอาด

bounded context หลาย​ตัวประกอบ​กัน​เป็น​ระบบ​ใหญ่​ผ่าน technical infrastructure (เช่น messaging) และ​เมื่อ​กระบวนการ​ธุรกิจ (business process) พาด​ผ่าน หลาย bounded context เรา​ใช้ Process Manager มา​ประสาน​งาน — มัน​คล้าย application service คือ stateless (ยกเว้น state ที่​ใช้​ติดตาม​ความ​คืบ​หน้า​ของ task) และ delegate งาน​กลับ​ไป​ให้​แต่ละ app ทำ

🚢 Cargo Shipping — หลาย client 1 service layer

ระบบ​ขนส่ง​ของ​เรา​มี client หลาย​แบบ​ที่​เรียก​ใช้​ความ​สามารถ​เดียวกัน: เจ้าหน้าที่​หน้า​เคาน์เตอร์​จอง​ผ่าน​เว็บ UI, ระบบ​ของ​ท่าเรือ​ยิง message เข้า​มา​แจ้ง Handling EventHandling Eventการ​บันทึก​การ​จัดการ​สินค้า​จริง (LOAD, UNLOAD, RECEIVE, CLAIM) เป็น Aggregate “แยก​ต่างหาก” จาก Cargo เพราะ​มี​ปริมาณ​มาก​และ​ต้อง​ประมวล​ผล​แบบ asyncTactical Design เมื่อ​โหลด/ขน​ถ่าย​ตู้, และ​พาร์ทเนอร์​เรียก​ผ่าน web service ทั้ง​สาม​เข้า​ทาง BookCargoService / HandlingEventService ชุด​เดียวกัน — ถ้า​วัน​หน้า​เพิ่ม mobile app เข้า​มา เรา​แค่​ให้​มัน​ปรับ​ตัว​เข้าหา API เดิม ไม่​ต้อง​แก้ domain เลย


  • DDD ไม่​บังคับ สถาปัตยกรรม​แบบ​ใด — ขอ​เพียง​แยก technical concern ออก​จาก business concern ให้​ขาด​จาก​กัน
  • แยก concern ด้วย​การ layer เพื่อ isolate ความ​ซับซ้อน​ทาง​ธุรกิจ​ออก​จาก​ความ​ซับซ้อน​ทาง​เทคนิค
  • ชั้น​นอก​พึ่ง​ชั้น​ใน ส่วน​ชั้น​ใน​เปิด interface ให้​ชั้น​นอก​มา implement — dependency inversion นี้​คือ​กลไก​ที่​ปกป้อง domain
  • domain layer อยู่​ใจกลาง ถูก isolate จาก​เทคนิค​โดย​มี application layer ห่อ​หุ้ม​ไว้
  • application service เปิด​ความ​สามารถ​ของ​ระบบ​โดย​ยก domain logic ขึ้น​ไป​สู่​ระดับ abstraction ที่​สูง​กว่า, อิง​กับ use case, เป็น client ของ domain และ delegate งาน​ให้ domain
  • application service ต้อง ไม่รู้จัก client — client เป็น​ฝ่าย​ปรับ​ตัว​เข้าหา API ทำให้​รองรับ client ได้​หลาย​แบบ
  • application service layer คือ​การ implement ขอบเขต​ของ bounded context อย่าง​เป็น​รูปธรรม

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

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

  • Anti-Corruption Layer — ขยาย​ความ​มุมมอง​ที่​ว่า application layer ทำตัว​เป็น​ชั้น​ป้องกัน​ไม่​ให้​รายละเอียด​ของ client ภายนอก​ไหล​ย้อน​เข้า​มา​ปน​เปื้อน domain
  • Repository — รูปแบบ​ที่​ใช้​อธิบาย​ตัวอย่าง CargoRepository ใน​บท​นี้ ซึ่ง​เป็น​ตัวอย่าง​คลาสสิก​ของ dependency inversion ระหว่าง domain กับ infrastructure
  • CQRS — ขยาย​ความ​ส่วน “Domain Reporting” ที่​แยก read model ออก​จาก transactional model
  • Anemic Domain Model — อธิบาย​ผลลัพธ์​ที่​เกิด​ขึ้น​เมื่อ​กฎ​ธุรกิจ​รั่วไหล​จาก domain layer เข้าไป​อยู่​ใน application service

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

ข้อ 1 / 4

ตามหนังสือ DDD เรียกร้องอะไรจากสถาปัตยกรรมของ app?