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

Tactical Design ด้วย Aggregate

จนถึง​ตอน​นี้​เรา​ได้​เดิน​ผ่าน strategic design กัน​มา​แล้ว​ทั้ง Bounded ContextBounded Contextขอบเขต​ที่​ชัดเจน (มัก​เป็น​ระบบ​ย่อย​หรือ​ทีม​หนึ่ง) ที่ model หนึ่ง​ใช้ได้​และ​มี​ความหมาย​แน่นอน ภายใน​ขอบเขต​นี้ Ubiquitous Language สอดคล้อง​กัน 100%Strategic Design, SubdomainSubdomainส่วน​ย่อย​ของ Domain ใหญ่ เช่น Domain e-commerce แตก​เป็น subdomain การ​สั่ง​ซื้อ การ​ชำระ​เงิน คลัง​สินค้า ฯลฯ อยู่​ใน “พื้นที่​ปัญหา” (problem space)Strategic Design และ Context MapContext Mapแผนภาพ​ที่​แสดง Bounded Context ทั้งหมด​และ​ความ​สัมพันธ์​ระหว่าง​กัน ช่วย​ให้​เห็น​ภาพ​รวม​และ​จุด​เชื่อม​ต่อ/แปล​ภาษา​ใน​ระบบ​ใหญ่Strategic Design เรา​เคย​เห็น​ภาพ​ของ2 Bounded Context ที่​ทำงาน​ร่วม​กัน — ฝั่ง​หนึ่ง​คือ Core DomainCore Domainsubdomain ที่​สำคัญ​และ​สร้าง​ความ​ได้​เปรียบ​ใน​การ​แข่งขัน​มาก​ที่สุด เป็น​เหตุผล​ที่​เรา​ต้อง​สร้าง​ซอฟต์แวร์​เอง​แทน​การ​ซื้อ ควร​ทุ่ม​ทีม​เก่ง​ที่สุด​ลง​ตรง​นี้Strategic Design ชื่อ Agile Project Management Context และ​อีก​ฝั่ง​คือ Supporting SubdomainSupporting Subdomainส่วน​ที่​จำเป็น​และ​เฉพาะ​กับ​ธุรกิจ​เรา แต่​ไม่ใช่​จุด​ที่​สร้าง​ความ​ได้​เปรียบ ไม่มี​ของ​สำเร็จรูป​ที่​พอดี จึง​ต้อง​ทำ​เอง แต่​ลงทุน​น้อย​กว่า coreStrategic Design ที่​ให้​เครื่องมือ​สำหรับ​การ​พูด​คุย​ร่วม​กัน (collaboration) โดย​เชื่อม​ต่อ​กัน​ผ่าน Context MappingContext Mappingกิจกรรมการ​ระบุ Bounded Context และ​นิยาม​ความ​สัมพันธ์​ระหว่าง​กัน (เช่น Partnership, Shared Kernel, ACL) เพื่อ​จัดการ​การ​เชื่อม​ต่อ​ระหว่าง​ทีม/ระบบStrategic Design

แต่​ถ้า​เจาะ​ลึก​เข้าไป “ภายใน” ของ Bounded Context หนึ่งๆ จะ​เจอ​อะไร? คำ​ตอบ​คือ​แนวคิด​ต่างๆ ที่​ส่วน​ใหญ่​แล้ว​ก็​คือ AggregateAggregateกลุ่ม​ของ Entity และ Value Object ที่​ถูก​มอง​เป็น​หนึ่ง​หน่วย​เดียว​เพื่อ​รักษา​ความ​ถูกต้อง​ของ​ข้อมูล มี​ขอบเขต​ชัดเจน และ​เป็น​หน่วย​ของ transactionTactical Design ใน model ของ​เรา บท​นี้​จะ​ลง​รายละเอียด​ของ tactical design pattern ที่​ทรง​พลัง​ที่สุด​ตัว​หนึ่ง​ของ DDD พร้อม​ใช้​ตัวอย่าง Scrum (Product, BacklogItem, Release, Sprint) จาก Agile Project Management Context เป็น​กรณี​ศึกษา​ตลอด​ทั้ง​บท


ลอง​นึกถึง2 Bounded Context ที่​เคย​เห็น — ฝั่ง Agile Project Management Core มี​แนวคิด​อย่าง Product, BacklogItem, Release, Sprint ส่วน​ฝั่ง​เครื่องมือ collaboration ซึ่ง​เป็น Supporting SubdomainSupporting Subdomainส่วน​ที่​จำเป็น​และ​เฉพาะ​กับ​ธุรกิจ​เรา แต่​ไม่ใช่​จุด​ที่​สร้าง​ความ​ได้​เปรียบ ไม่มี​ของ​สำเร็จรูป​ที่​พอดี จึง​ต้อง​ทำ​เอง แต่​ลงทุน​น้อย​กว่า coreStrategic Design มี Forum, Calendar, Discussion, Post และ CalendarEntry — ทุก​แนวคิด​เหล่า​นี้​ถูก​วงล้อม​ไว้ จึง​เป็น Aggregate ทั้งหมด ส่วน​แนวคิด​เดียว​ที่​ไม่​ได้​ถูก​วง​ไว้​คือ Discussion ที่​ปรากฏ​อยู่​ใน Agile Project Management Context (ไม่ใช่​ตัว​ที่​อยู่​ใน​ฝั่ง collaboration) ซึ่ง​ถูก​จำลอง​เป็น Value ObjectValue Objectอ็อบเจ็กต์ที่​สำคัญ​ที่ “ค่า” ของ​มัน ไม่มี​ตัวตน​เฉพาะ สอง​ตัว​ที่​ค่า​เท่า​กัน​ถือว่า​เท่า​กัน ควร​เป็น immutable (เปลี่ยน​ค่า​ไม่​ได้) เช่น เงิน, วัน​ที่, พิกัดTactical Design แทน บท​นี้​จะ​โฟกัส​ที่ Aggregate และ​ดู​วิธี​จำลอง Product, BacklogItem, Release และ Sprint อย่าง​ใกล้​ชิด

ก่อน​จะ​ไป​ต่อ ขอ​ทบทวน​องค์​ประกอบ​พื้นฐาน​สาม​อย่าง​ที่ Aggregate สร้าง​ขึ้น​มา​จาก:

  • EntityEntityอ็อบเจ็กต์ที่​มี “ตัวตน” (identity) ต่อ​เนื่อง​ตาม​เวลา แม้​ค่า​ภายใน​เปลี่ยน ก็​ยัง​เป็น​สิ่ง​เดิม เช่น ลูกค้า, คำ​สั่ง​ซื้อ — แยกแยะ​ด้วย id ไม่ใช่​ด้วย​ค่าTactical Design คือ​อะไร — Entity จำลอง “สิ่งหนึ่งๆ” ที่​มี IdentityIdentity“ตัวตน” ที่​ทำให้ Entity เป็น​สิ่ง​เดิม​ตลอด​อายุ แม้​ค่า​เปลี่ยน เช่น เลข​ที่​คำ​สั่ง​ซื้อ หรือ id ที่​ระบบ​สร้าง​ให้Tactical Design เฉพาะ​ตัว ใช้​แยก​มัน​ออก​จาก Entity อื่น​ทุก​ตัว แม้​จะ​เป็น​ชนิด​เดียวกัน​ก็ตาม โดย​มาก (อาจ​จะ​ส่วน​ใหญ่) Entity จะ​เปลี่ยน​สถานะ​ได้ (mutable) คือ​ค่า​ภายใน​เปลี่ยน​ไป​ตาม​เวลา แต่ Entity ก็​ไม่​จำเป็น​ต้อง mutable เสมอ​ไป สิ่ง​ที่​แยก Entity ออก​จาก​เครื่องมือ​จำลอง​อื่น​คือ “ความ​ไม่​ซ้ำ​ใคร” หรือ​ความ​เป็น​ปัจเจก​ของ​มัน

  • Aggregate คือ​อะไร — Aggregate ประกอบ​ขึ้น​จาก Entity ตั้งแต่​หนึ่ง​ตัว​ขึ้น​ไป โดย​มี Entity หนึ่ง​ตัว​ทำ​หน้าที่​เป็น Aggregate RootAggregate RootEntity หนึ่ง​ตัว​ที่​เป็น “ประตู​เดียว” เข้า​สู่ Aggregate การ​อ้างอิง​จาก​ภายนอก​ทำได้​กับ root เท่านั้น root จึง​ควบคุม​กฎ​ความ​ถูกต้อง​ของ​ทั้ง​กลุ่มTactical Design นอกจาก​นี้ Aggregate ยัง​มี Value Object ประกอบ​อยู่​ภายใน​ได้​ด้วย

  • Value ObjectValue Objectอ็อบเจ็กต์ที่​สำคัญ​ที่ “ค่า” ของ​มัน ไม่มี​ตัวตน​เฉพาะ สอง​ตัว​ที่​ค่า​เท่า​กัน​ถือว่า​เท่า​กัน ควร​เป็น immutable (เปลี่ยน​ค่า​ไม่​ได้) เช่น เงิน, วัน​ที่, พิกัดTactical Design คือ​อะไร — Value Object (หรือ​เรียกสั้นๆ ว่า Value) จำลอง “องค์​รวม​เชิง​แนวคิด” ที่​เปลี่ยน​ค่า​ไม่​ได้ (ImmutableImmutableเปลี่ยนแปลง​ไม่​ได้​หลัง​สร้าง ถ้า​จะ “เปลี่ยน​ค่า” ต้อง​สร้าง​อ็อบเจ็กต์ใหม่​แทน หลักการ​สำคัญ​ของ Value Object เพื่อ​เลี่ยง bug จาก​การ​ใช้​อ้างอิง​ร่วม​กันTactical Design) ภายใน model Value ก็​คือ​ค่าๆ หนึ่ง ต่าง​จาก Entity ตรง​ที่​มัน​ไม่มี identity เฉพาะ และ​การ​เทียบ​ว่า​เท่า​กัน​หรือ​ไม่ ตัดสิน​จาก​การ​เปรียบเทียบ attribute ที่ Value ห่อ​หุ้ม​ไว้​ทั้งหมด ยิ่ง​กว่า​นั้น Value Object มัก​ไม่ใช่ “สิ่งของ” แต่​ถูก​ใช้​เพื่อ​อธิบาย วัด หรือ​บ่ง​บอก​ปริมาณ​ของ Entity

ที่มา​เชิง​ลึก

Entity และ Value Object เป็น building block พื้นฐาน​ของ tactical DDD ที่​หนังสือ​เล่ม​นี้​กลั่น​มา​อย่าง​กระชับ หาก​ต้องการ​รายละเอียด​แบบ​ครบถ้วน Vernon แนะนำ​ให้​ตาม​อ่าน​ใน Implementing Domain-Driven Design ของ​เขา​เอง


Aggregate RootAggregate RootEntity หนึ่ง​ตัว​ที่​เป็น “ประตู​เดียว” เข้า​สู่ Aggregate การ​อ้างอิง​จาก​ภายนอก​ทำได้​กับ root เท่านั้น root จึง​ควบคุม​กฎ​ความ​ถูกต้อง​ของ​ทั้ง​กลุ่มTactical Design ของ​แต่ละ Aggregate “เป็น​เจ้าของ” องค์​ประกอบอื่นๆ ทั้งหมด​ที่​รวม​กลุ่ม​อยู่​ภายใน ชื่อ​ของ Root Entity ก็​คือ​ชื่อ​เชิง​แนวคิด​ของ​ทั้ง Aggregate จึง​ควร​เลือก​ชื่อ​ที่​อธิบาย “องค์​รวม” ที่ Aggregate นั้น​จำลอง​ได้​อย่าง​เหมาะสม

หัวใจ​สำคัญ​คือ Aggregate แต่ละ​ตัว​คือ​ขอบเขต​ความ​สอดคล้อง​เชิง transaction (transactional consistency boundary) หมายความ​ว่า ภายใน Aggregate หนึ่ง​ตัว ทุก​ส่วน​ที่​ประกอบ​กัน​อยู่​ต้อง​สอดคล้อง​กัน​ตาม​กฎ​ธุรกิจ ณ เวลา​ที่ transaction ถูก commit ลง​ฐาน​ข้อมูล นี่​ไม่​ได้​แปล​ว่า​เรา​ห้าม​ประกอบ​องค์​ประกอบ​ที่ “ไม่​จำเป็น​ต้อง​สอดคล้อง​ทันที” ไว้​ใน Aggregate เพราะ Aggregate ก็​จำลอง​องค์​รวม​เชิง​แนวคิด​เช่น​กัน แต่​สิ่ง​ที่​ต้อง​ให้​ความ​สำคัญ เป็น​อันดับ​แรก​สุด คือ​ความ​สอดคล้อง​เชิง transaction

graph LR
  subgraph T1["Single transaction"]
    R1["Aggregate Root 1"] --> E1["Entity"]
    R1 --> V1["Value Object"]
  end
  subgraph T2["Separate transaction"]
    R2["Aggregate Root 2"] --> E2["Entity"]
    R2 --> E3["Entity"]
    E2 --> V2["Value Object"]
  end

เหตุผล​ที่​ต้อง​มี​ขอบเขต transaction แบบ​นี้​ล้วน​ถูก​ขับ​เคลื่อน​ด้วย​ธุรกิจ เพราะ​ธุรกิจ​ที่​เป็น​ผู้​ตัดสิน​ว่า​สถานะ​ที่​ถูกต้อง​ของ​กลุ่ม​ข้อมูล ณ เวลา​ใด​เวลา​หนึ่ง​ควร​เป็น​อย่างไร พูด​อีก​แบบ​คือ ถ้า Aggregate ไม่​ได้​ถูก​บันทึก​ใน​สถานะ​ที่​สมบูรณ์​และ​ถูกต้อง การ​ทำงาน​ทาง​ธุรกิจ​ที่​เพิ่ง​เกิด​ขึ้น​ก็​จะ​ถือว่า “ผิด” ตาม​กฎ​ธุรกิจ

ความหมาย​ที่​กว้าง​ขึ้น​ของ​คำ​ว่า Transaction

การ​ใช้ transaction ใน application เป็น​รายละเอียด​ของ​การ implement ใน​ระดับ​หนึ่ง กรณี​ทั่วไป​คือ​มี Application ServiceApplication Serviceบริการ​ที่ “ประสาน​งาน” use case — จัดการ transaction, ความ​ปลอดภัย, การ​บันทึก, แปลง DTO — แต่​ไม่มี​กฎ​ธุรกิจ​อยู่​ใน​ตัว​มัน​เองTactical Design คอย​ควบคุม atomic database transaction แทน domain model แต่​ภาย​ใต้​สถาปัตยกรรม​อื่น เช่น Actor model ที่ Aggregate แต่ละ​ตัว​เป็น actor การ​จัดการ transaction อาจ​ทำ​ด้วย Event SourcingEvent Sourcingเก็บ​ทุก​การ​เปลี่ยน​สถานะ​เป็น event ที่​เปลี่ยน​ไม่​ได้ แล้ว​สร้าง​สถานะ​ปัจจุบัน​ด้วย​การ​เล่น event ซ้ำ มัก “ต้อง​ใช้​คู่​กับ CQRS” (Greg Young) มี event stream ต่อ1 AggregateArchitecture บน​ฐาน​ข้อมูล​ที่​ไม่​รองรับ atomic transaction ด้วย​ซ้ำ ไม่​ว่า​ทาง​ใด สิ่ง​ที่หมาย​ถึง​คือ การ​แก้ไข Aggregate ถูก​แยก​ส่วน (isolate) อย่างไร และ InvariantInvariantกฎ​ทาง​ธุรกิจ​ที่ “ต้อง​เป็น​จริง​เสมอ” ภายใน Aggregate เช่น ยอด​รวม​คำ​สั่ง​ซื้อ​ต้อง​เท่ากับ​ผล​รวม​ของ​รายการ​สินค้า — ต้อง​ถูกต้อง​ภายใน transaction เดียวTactical Design ทาง​ธุรกิจ — กฎ​ที่​ซอฟต์แวร์​ต้อง​ยึดถือ​เสมอ — ได้​รับ​การ​รับประกัน​ว่า​สอดคล้อง​กัน​หลัง​การ​ทำงาน​ทาง​ธุรกิจ​ทุก​ครั้ง

กฎ​ทั่วไป​ของ​การ​ออกแบบ Aggregate คือ แก้ไข​และ commit ได้​เพียง1 instance ของ Aggregate ต่อ1 transaction Aggregate ตัวอื่นๆ จะ​ถูก​แก้ไข​และ commit ใน transaction แยก​ต่างหาก นั่น​จึง​เป็น​เหตุผล​ที่​เรียก​ว่า Aggregate เป็น “ขอบเขต​ความ​สอดคล้อง​เชิง transaction” แล้ว​ถ้า Aggregate ตัว​ที่​สอง​ต้อง​อัปเดต​ตาม​การ​เปลี่ยนแปลง​ของ​ตัว​แรก​ล่ะ จะ​ทำ​อย่างไร? คำ​ตอบ​รอ​อยู่​ใน​กฎ​ข้อ​ที่​สี่​ท้าย​หัวข้อ​ถัด​ไป สิ่ง​สำคัญ​ที่​ต้อง​จำ​จาก​หัวข้อ​นี้​คือ กฎ​ธุรกิจ​คือ​ตัว​ขับ​เคลื่อน ที่​กำหนด​ว่า​อะไร​ต้อง​สมบูรณ์ ครบถ้วน และ​สอดคล้อง​กัน ณ ตอน​จบ​ของ1 transaction


ทีนี้​มา​ดู​กฎ​พื้นฐาน​สี่​ข้อ​ของ​การ​ออกแบบ Aggregate กัน:

  1. ปกป้อง business invariant ไว้​ภายใน​ขอบเขต​ของ Aggregate (Protect Business Invariants inside Aggregate Boundaries)
  2. ออกแบบ Aggregate ให้​เล็ก (Design Small Aggregates)
  3. อ้างอิง Aggregate อื่น​ด้วย identity เท่านั้น (Reference Other Aggregates by Identity Only)
  4. อัปเดต Aggregate อื่น​ด้วย Eventual Consistency (Update Other Aggregates Using Eventual Consistency)

แน่นอน​ว่า​กฎ​เหล่า​นี้​ไม่​ได้​มี “ตำรวจ DDD” มา​คอย​บังคับ​ใช้​อย่าง​เคร่งครัด มัน​เป็น​เพียง​แนวทาง​ที่​ดี ซึ่ง​เมื่อ​นำ​ไป​ใช้​อย่าง​ใคร่ครวญ​แล้ว จะ​ช่วย​ให้​ออกแบบ Aggregate ที่​ทำงาน​ได้​อย่าง​มี​ประสิทธิภาพ ต่อ​ไป​จะ​เจาะ​แต่ละ​ข้อ​ว่า​ควร​นำ​ไป​ใช้​อย่างไร​ใน​ทุก​ที่​ที่​ทำได้

กฎ​ข้อ 1 หมายความ​ว่า ธุรกิจ​คือ​ผู้​ตัดสิน​สุดท้าย ว่า Aggregate ควร​ประกอบ​ขึ้น​จาก​อะไร โดย​พิจารณา​จาก​สิ่ง​ที่ “ต้อง​สอดคล้อง​กัน” เมื่อ transaction ถูก commit ใน​ตัวอย่าง Scrum เรา​ออกแบบ​ให้ Product มี​ลักษณะ​ที่​ว่า เมื่อ​จบ transaction ทุก instance ของ ProductBacklogItem ที่​ประกอบ​อยู่​ต้อง​ถูก​นับ​ครบ​และ​สอดคล้อง​กับ root Product ใน​ทำนอง​เดียวกัน Sprint ก็​ถูก​ออกแบบ​ให้​ทุก instance ของ CommittedBacklogItem ที่​ประกอบ​อยู่ ต้อง​สอดคล้อง​กับ root Sprint เมื่อ​จบ transaction

graph TD
  subgraph PA["Product Aggregate"]
    P["«root» Product"] --> PID["ProductId"]
    P -->|"1..*"| PBI["ProductBacklogItem"]
  end
  subgraph SA["Sprint Aggregate"]
    S["«root» Sprint"] --> SID["SprintId"]
    S -->|"1..*"| CBI["CommittedBacklogItem"]
  end

กฎ​ข้อ 1 จะ​ชัดเจน​ขึ้น​เมื่อ​ดู​อีก​ตัวอย่าง​หนึ่ง คือ BacklogItem Aggregate ซึ่ง​มี​กฎ​ธุรกิจ​ระบุ​ว่า “เมื่อ​ทุก instance ของ Task มี hoursRemaining เป็น​ศูนย์ สถานะ​ของ BacklogItem ต้อง​ถูก​ตั้ง​เป็น DONE ดังนั้น​เมื่อ​จบ transaction InvariantInvariantกฎ​ทาง​ธุรกิจ​ที่ “ต้อง​เป็น​จริง​เสมอ” ภายใน Aggregate เช่น ยอด​รวม​คำ​สั่ง​ซื้อ​ต้อง​เท่ากับ​ผล​รวม​ของ​รายการ​สินค้า — ต้อง​ถูกต้อง​ภายใน transaction เดียวTactical Design ทาง​ธุรกิจ​ที่​เฉพาะ​เจาะจง​ข้อ​นี้​ต้อง​เป็น​จริง เพราะ​ธุรกิจ​กำหนด​ให้​ต้อง​เป็น​เช่น​นั้น

graph TD
  subgraph BIA["BacklogItem Aggregate"]
    BI["«root» BacklogItem (status == DONE)"] --> BID["BacklogItemId"]
    BI -->|"1..*"| TASK["Task (hoursRemaining == 0)"]
  end
หัวใจ​ของ​กฎ​ข้อ 1

ขอบเขต​ของ Aggregate ถูก​กำหนด​โดย กฎ​ธุรกิจ​ที่​ต้อง​เป็น​จริง​เสมอ ณ ตอน​จบ transaction ไม่ใช่​ความ​สะดวก​ทาง​เทคนิค​หรือ​ความ​เคยชิน​กับ data model สิ่ง​ที่​ต้อง “สอดคล้อง​ทันที” เท่านั้น​จึง​ควร​อยู่​ใน Aggregate เดียวกัน

กฎ​ข้อ​นี้​เน้น​ว่า footprint ใน​หน่วย​ความ​จำ​และ​ขอบเขต transaction ของ Aggregate แต่ละ​ตัว​ควร​เล็ก​พอสมควร ลอง​ดู​ตัวอย่าง​ของ Aggregate ที่ “ไม่​เล็ก” — Product ที่​บรรจุ collection ขนาด​ใหญ่​ของ BacklogItem, Release และ Sprint ไว้​ใน​ตัว​เองตรงๆ เมื่อ​เวลา​ผ่าน​ไป collection เหล่า​นี้​อาจ​โต​ขึ้น​มหาศาล มี BacklogItem เป็น​พันๆ และ Release กับ Sprint อีก​หลาย​ร้อย แนวทาง​ออกแบบ​แบบ​นี้​โดย​ทั่วไป​ถือ​เป็น​ทาง​เลือก​ที่ “แย่​มาก”

graph TD
  subgraph LA["Large Aggregate (แย่)"]
    P["«root» Product"] -->|"1..*"| BI["BacklogItem (พัน ๆ)"]
    P -->|"1..*"| REL["Release (หลายร้อย)"]
    P -->|"1..*"| SP["Sprint (หลายร้อย)"]
  end

แต่​ถ้า​เรา​แยก Product Aggregate ออก​เป็น4 Aggregate ต่างหาก จะ​ได้ Product Aggregate เล็กๆ, BacklogItem Aggregate เล็กๆ, Release Aggregate เล็กๆ และ Sprint Aggregate เล็กๆ ทั้ง​สี่​ตัว​นี้​โหลด​เร็ว ใช้​หน่วย​ความ​จำ​น้อย และ garbage collect ได้​เร็ว​กว่า ที่​สำคัญ​ที่สุด​คือ Aggregate เหล่า​นี้​จะ commit สำเร็จ​ใน​ระดับ transaction บ่อย​กว่า Product แบบ​กลุ่ม​ใหญ่​เดิม​มาก

graph TD
  PA["Product Aggregate (root: Product)"]
  BIA["BacklogItem Aggregate (root: BacklogItem)"]
  RA["Release Aggregate (root: Release)"]
  SA["Sprint Aggregate (root: Sprint)"]

การ​ทำ​ตาม​กฎ​ข้อ​นี้​ยัง​มี​ผลพลอยได้​คือ Aggregate แต่ละ​ตัว​จะ​ดูแล​ง่าย​ขึ้น เพราะ​งาน​แต่ละ​ชิ้น​บริหาร​ได้​ด้วย​นัก​พัฒนา​คน​เดียว และ​ยัง​ทำให้ Aggregate test ได้​ง่าย​ขึ้น ด้วย

อีก​สิ่ง​ที่​ควร​นึกถึง​ตอน​ออกแบบ Aggregate คือ Single Responsibility Principle (SRP) ถ้า Aggregate พยายาม​ทำ​หลาย​อย่าง​เกิน​ไป แสดง​ว่า​มัน​ไม่​ได้​ทำ​ตาม SRP และ​มัก​จะ​สะท้อน​ออก​มา​ใน​ขนาด​ของ​มัน ลอง​ถาม​ตัวเอง​ว่า Product เป็นการ implement Scrum product ที่​โฟกัส​ชัดเจน หรือ​กำลัง​พยายาม​เป็น​อย่าง​อื่น​ไป​ด้วย เหตุผล​ที่​จะ​แก้ Product ควร​เป็น “เพื่อ​ทำให้​มัน​เป็น Scrum product ที่​ดี​ขึ้น” เท่านั้น ไม่ใช่​เพื่อ​บริหาร backlog item, release หรือ sprint

ใน​เมื่อ​เรา​แยก Product กลุ่ม​ใหญ่​ออก​เป็น4 Aggregate เล็กๆ แล้ว แต่ละ​ตัว​ควร​อ้างอิง​ตัว​อื่น​ใน​จุด​ที่​จำเป็น​อย่างไร? คำ​ตอบ​คือ​ทำ​ตาม​กฎ​ข้อ 3 “อ้างอิง Aggregate อื่น​ด้วย identity เท่านั้น” ใน​ตัวอย่าง​นี้ BacklogItem, Release และ Sprint ต่าง​อ้างอิง Product โดย ถือ ProductId เอา​ไว้ ไม่ใช่​ถือ object reference ตรงๆ วิธี​นี้​ช่วย​ให้ Aggregate เล็ก และ​ป้องกัน​การ “เอื้อม​มือ” ไป​แก้ไข​หลาย Aggregate ใน transaction เดียวกัน

graph TD
  subgraph PA["Product Aggregate"]
    P["«root» Product"] --> PID["ProductId"]
  end
  subgraph BIA["BacklogItem Aggregate"]
    BI["«root» BacklogItem"] --> PID2["ProductId"]
  end
  subgraph RA["Release Aggregate"]
    REL["«root» Release"] --> PID3["ProductId"]
  end
  subgraph SA["Sprint Aggregate"]
    SP["«root» Sprint"] --> PID4["ProductId"]
  end

วิธี​นี้​ยัง​ช่วย​ให้ design ของ Aggregate เล็ก​และ​มี​ประสิทธิภาพ ลด​ความ​ต้องการ​หน่วย​ความ​จำ​และ​โหลด​จาก persistence store ได้​เร็ว​ขึ้น อีก​ทั้ง​ยัง​ช่วย​บังคับ​ใช้​กฎ​ที่​ว่า “อย่า​แก้​หลาย instance ของ Aggregate ใน transaction เดียวกัน” เพราะ​เมื่อ​ถือ​ไว้​เพียง identity ของ Aggregate อื่น ก็​ไม่มี​ทาง​ที่​จะ​ได้ object reference ตรง​ไป​ยัง​มัน

ประโยชน์​อีก​ข้อ​ของ​การ​อ้างอิง​ด้วย identity เท่านั้น​คือ Aggregate ของ​เรา​จะ​เก็บ​ลง​กลไก persistence แทบ​ทุก​ชนิด​ได้​ง่าย ไม่​ว่า​จะ​เป็น relational database, document database, key-value store หรือ data grid/fabric เช่น ตาราง MySQL, store แบบ JSON อย่าง PostgreSQL หรือ MongoDB, GemFire/Geode, Coherence และ GigaSpaces

สมมติ BacklogItem ถูก commit เข้าไป​ใน Sprint ทั้ง BacklogItem และ Sprint ต้อง​ตอบ​สนอง​ต่อ​เหตุการณ์​นี้ ตัว​แรก​ที่ “รู้ตัว” ก่อน​คือ BacklogItem ที่​รู้​ว่า​ตัวเอง​ถูก commit เข้า Sprint แล้ว ส่วน​นี้​จัดการ​ได้​ใน transaction เดียว ตอน​ที่​สถานะ​ของ BacklogItem ถูก​แก้​ให้​ถือ SprintId ของ Sprint ที่​มัน commit เข้าไป แล้ว​เรา​จะ​มั่นใจ​ได้​อย่างไร​ว่า Sprint เอง​ก็​ถูก​อัปเดต​ด้วย BacklogItemId ของ BacklogItem ที่​เพิ่ง​ถูก commit?

คำ​ตอบ​คือ​ใช้ Eventual ConsistencyEventual Consistencyความ​ถูกต้อง​ที่ “ตาม​มา​ทีหลัง” ไม่ใช่​ทันที ใช้​กับ​การ​อัปเดต​ข้าม Aggregate ผ่าน Domain Event แทน​การ​บังคับ​ให้​ตรง​กัน​ใน​ทันที​ภายใน transaction เดียวTactical Design โดย​ใน​ฐานะ​ส่วน​หนึ่ง​ของ transaction ของ BacklogItem Aggregate มัน​จะ publish Domain EventDomain Eventสิ่ง​ที่ “เกิด​ขึ้น​แล้ว” ใน domain ซึ่ง​ส่วน​อื่น​สนใจ แทน​ด้วย​อ็อบเจ็กต์ที่​เปลี่ยนแปลง​ไม่​ได้ ตั้ง​ชื่อ​เป็น​อดีต เช่น CargoWasRouted ใช้​สื่อสาร​ข้าม AggregateTactical Design ชื่อ BacklogItemCommitted ออก​มา transaction ของ BacklogItem จบ​ลง​และ​สถานะ​ถูก​บันทึก​พร้อม​กับ Domain Event เมื่อ BacklogItemCommitted เดินทาง​ไป​ถึง subscriber ใน​เครื่อง​เดียวกัน transaction ใหม่​จะ​เริ่ม​ขึ้น และ​สถานะ​ของ Sprint จะ​ถูก​แก้​ให้​ถือ BacklogItemId ของ BacklogItem ที่​ถูก commit โดย Sprint เก็บ BacklogItemId นั้น​ไว้​ภายใน CommittedBacklogItem Entity ตัว​ใหม่

graph LR
  subgraph BIA["BacklogItem Aggregate (transaction 1)"]
    BI["«root» BacklogItem"] --> BID["BacklogItemId"]
    BI --> SIDREF["SprintId"]
  end
  EVT["Domain Event: BacklogItemCommitted"]
  subgraph SA["Sprint Aggregate (transaction 2)"]
    SP["«root» Sprint"] --> SID["SprintId"]
    SP --> CBI["CommittedBacklogItem (ถือ BacklogItemId ใหม่)"]
  end
  BIA -->|publish| EVT
  EVT -->|"eventual"| SA

ทบทวน​จาก​บท Strategic Design ด้วย Context MappingContext Mappingกิจกรรมการ​ระบุ Bounded Context และ​นิยาม​ความ​สัมพันธ์​ระหว่าง​กัน (เช่น Partnership, Shared Kernel, ACL) เพื่อ​จัดการ​การ​เชื่อม​ต่อ​ระหว่าง​ทีม/ระบบStrategic Design — Domain Event ถูก publish โดย Aggregate และ​ถูก subscribe โดย Bounded Context ที่​สนใจ กลไก​ส่ง​ข้อความ (messaging mechanism) จะ​นำ​ส่ง Domain Event ไป​ยัง​ผู้​สนใจ​ผ่าน​การ subscribe โดย Bounded Context ที่​สนใจ​อาจ​เป็น​ตัว​เดียว​กับ​ที่ publish event หรือ​เป็น Bounded Context อื่น​ก็ได้ ใน​กรณี BacklogItem และ Sprint นี้ ทั้ง​ผู้ publish และ​ผู้ subscribe อยู่​ใน Bounded Context เดียวกัน — ไม่​จำเป็น​ต้อง​ใช้ messaging middleware เต็ม​รูปแบบ แต่​ก็​ทำได้​ง่าย​ถ้า​อยู่​แล้ว เพราะ​ใช้​ตัว​เดียว​กับ​ที่ publish ไป​ยัง Bounded Context อื่น

ถ้า Eventual Consistency ดู​น่า​กลัว

จริงๆ แล้ว​การ​ใช้ eventual consistency ไม่​ได้​ยากเย็น​อะไร แต่​ถ้า​ยัง​ไม่มี​ประสบการณ์​และ​ยัง​กังวล ก็​ยัง​ควร​แบ่ง model เป็น Aggregate ตาม​ขอบเขต transaction ที่​ธุรกิจ​กำหนด​อยู่ดี อย่างไร​ก็ตาม ไม่มี​อะไร​ห้าม​ไม่​ให้ commit การ​แก้ไข2 Aggregate ขึ้น​ไป​ใน atomic database transaction เดียว เรา​อาจ​เลือก​ทำ​แบบ​นี้​ใน​กรณี​ที่ “รู้​ว่า​จะ​สำเร็จ​แน่” แล้ว​ใช้ eventual consistency กับ​กรณี​อื่น​ทั้งหมด เพื่อ​ค่อยๆ คุ้น​เคย​กับ​เทคนิค​โดย​ไม่​ต้อง​ก้าว​กระโดด​เกิน​ไป เพียง​เข้าใจ​ว่า​นี่​ไม่ใช่​วิธี​หลัก​ที่ Aggregate ตั้งใจ​ให้​ใช้ และ​อาจ​เจอ transaction ล้มเหลว​เป็น​ผล​ตาม​มา

เมื่อ​โฟกัส​การ​สร้าง Aggregate A1 แล้ว​พบ​ว่า A2 ต้อง​สอดคล้อง​กัน “ทันที” (immediate) ส่วน C14 สอดคล้อง​แบบ “ตาม​มา​ทีหลัง” (eventual ภายใน 30 วินาที) ผล​ก็​คือ A1 กับ A2 จะ​ถูกรวม​เป็น Aggregate เดียวกัน A[1,2] ส่วน​ตอน runtime นั้น A[1,2] จะ publish Domain Event ที่​ทำให้ C14 ถูก​อัปเดต​แบบ eventual

graph TD
  A1["A1"] -->|become| A12["A[1,2]"]
  A2["A2"] -->|become| A12
  A12 -->|"Domain Event (eventual ~30s)"| C14["C14"]

พึง​ระวัง​ว่า​ธุรกิจ​ไม่​ได้​ยืนกราน​ให้ Aggregate ทุก​ตัว​ต้อง​อยู่​ใน​ข้อ​กำหนด “สอดคล้อง​ทันที” เสมอ แนวโน้ม​ที่​จะ​คิด​เช่น​นั้น​แรง​เป็น​พิเศษ​เมื่อ​หลาย​คนใน​วง​ออกแบบ​ถูก​ครอบงำ​ด้วย​มุมมอง​แบบ database design และ data modeling ซึ่ง​มี​มุมมอง​ที่​ยึด transaction เป็น​ศูนย์กลาง​สูง​มาก แต่​ใน​ความ​เป็น​จริง ธุรกิจ​แทบ​ไม่​ค่อย​ต้องการ​ความ​สอดคล้อง​ทันที​ใน​ทุก​กรณี การ​จะ​เปลี่ยน​ความคิด​นี้ มัก​ต้อง​ใช้​เวลา​พิสูจน์​ให้​เห็น​ว่า transaction จะ​ล้มเหลว​อย่างไร​เมื่อ​ผู้​ใช้​หลาย​คน​อัปเดต​ส่วนประกอบ​ต่างๆ ของ Aggregate กลุ่ม​ใหญ่​พร้อม​กัน อีก​ทั้ง​ยัง​ชี้​ให้​เห็น​ภาระ​หน่วย​ความ​จำ​มหาศาล​ของ design กลุ่ม​ใหญ่​แบบ​นั้น ซึ่ง​ล้วน​เป็น​ปัญหา​ที่​เรา​พยายาม​หลีก​เลี่ยง​ตั้งแต่​แรก

Eventual Consistency ถูก​ขับ​เคลื่อน​ด้วย​ธุรกิจ

แบบฝึกหัด​นี้​ชี้​ว่า eventual consistency ถูก​ขับ​เคลื่อน​ด้วย​ธุรกิจ ไม่ใช่​ด้วย​เทคนิค มี​เพียง​ธุรกิจ​เท่านั้น​ที่​ตัดสิน​ได้​ว่า​กรอบ​เวลา​ที่​ยอมรับ​ได้​สำหรับ​การ​อัปเดต​ระหว่าง Entity ต่างๆ ควร​เป็น​เท่าไร บาง​อย่าง​ต้อง​ทันที (transactional) คือ​ต้อง​บริหาร​ด้วย Aggregate เดียวกัน บาง​อย่าง​เป็น eventual คือ​บริหาร​ผ่าน Domain Event และ messaging ได้ ลอง​นึก​ว่า​ถ้า​ธุรกิจ​ต้อง​ดำเนิน​งาน​ด้วย​ระบบ​กระดาษ​ล้วนๆ จะ​ทำ​อย่างไร — มัก​ให้ insight ที่​มี​ค่า​ว่าการ​ทำงาน​ต่างๆ ควร​ทำงาน​อย่างไร​ใน model ซอฟต์แวร์


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

นอกจาก​นี้​ยัง​ต้อง​ระวัง​การ “รั่ว” ของ ตรรกะ​ทาง​ธุรกิจ (Business Logic)Domain Serviceบริการ​ที่​ถือ “ตรรกะ​ธุรกิจ” ซึ่ง​ไม่​เข้า​กับ Entity หรือ Value Object ตัว​ใด​ตัว​หนึ่ง​โดย​ธรรมชาติ เช่น การ​โอน​เงิน​ที่​เกี่ยวข้อง​กับ​สอง​บัญชี — ไร้​สถานะ (stateless)Tactical Design ไป​อยู่​ใน Application ServiceApplication Serviceบริการ​ที่ “ประสาน​งาน” use case — จัดการ transaction, ความ​ปลอดภัย, การ​บันทึก, แปลง DTO — แต่​ไม่มี​กฎ​ธุรกิจ​อยู่​ใน​ตัว​มัน​เองTactical Design ที่​อยู่​เหนือ domain model ภาวะ​นี้​เกิด​ขึ้น​ได้​โดย​ไม่ทัน​สังเกต เหมือน​ภาวะ​โลหิต​จางจริงๆ การ​โยน business logic จาก service ไป​ไว้​ใน helper/utility class ก็​ไม่​ได้​ผล​เช่น​กัน เพราะ service utility เหล่า​นี้​มัก “หา​ตัวตน​ไม่​เจอ” และ​เล่า​เรื่อง​ของ​ตัวเอง​ไม่​ตรง​สัก​ที จง​วาง model ที่​มี​พฤติกรรม​สมบูรณ์ (Rich Domain Model) ไว้​ใน domain model ของ​เรา ไม่​เช่น​นั้น​ก็​ต้อง​ทน​กับ bug ที่ Anemic Domain Model เป็น​สปอนเซอร์​ให้

แล้ว Functional Programming ล่ะ?

เมื่อ​ใช้ functional programming กฎ​จะ​เปลี่ยน​ไป​พอสมควร แม้ Anemic Domain ModelAnemic Domain Modelanti-pattern ที่อ็อบเจ็กต์มี​แต่ getter/setter ไร้​พฤติกรรม ตรรกะ​ถูก​ดึง​ไป​ไว้​ใน service ทั้งหมด — จ่าย​ค่า domain model แต่​ไม่​ได้​ประโยชน์​ของ​มัน​เลยTactical Design จะ​เป็น​ความคิด​ที่​แย่​ใน​แบบ object-oriented แต่​ใน​แบบ functional มัน​ค่อน​ข้าง​เป็น​เรื่อง​ปกติ เพราะ functional programming ส่งเสริม​การ​แยก data ออก​จาก behavior ข้อมูล​ถูก​ออกแบบ​เป็น immutable data structure หรือ record type ส่วน​พฤติกรรม implement เป็น pure function ที่​ทำงาน​บน record เหล่า​นั้น แทนที่​จะ​แก้ไข​ข้อมูล​ที่​รับ​เข้า​มา​เป็น argument function จะ​คืน​ค่า​ใหม่​ออก​มา ค่า​ใหม่​เหล่า​นี้​อาจ​เป็น​สถานะ​ใหม่​ของ Aggregate หรือ​เป็น Domain EventDomain Eventสิ่ง​ที่ “เกิด​ขึ้น​แล้ว” ใน domain ซึ่ง​ส่วน​อื่น​สนใจ แทน​ด้วย​อ็อบเจ็กต์ที่​เปลี่ยนแปลง​ไม่​ได้ ตั้ง​ชื่อ​เป็น​อดีต เช่น CargoWasRouted ใช้​สื่อสาร​ข้าม AggregateTactical Design ที่​แทน​การ​เปลี่ยน​ผ่าน​สถานะ บท​นี้​พูด​ถึง​แนวทาง object-oriented เป็น​หลัก​เพราะ​ยัง​ใช้​กัน​แพร่หลาย​และ​เข้าใจ​กัน​ดี​ที่สุด แต่​หาก​ใช้​ภาษา​และ​แนวทาง functional พึง​รู้​ว่า​คำ​แนะนำ​บาง​ส่วน​อาจ​ใช้​ไม่​ได้​หรือ​อย่าง​น้อย​ก็​มี​กฎ​ที่ override ทับ

ต่อ​ไป​จะ​ดู​องค์​ประกอบ​ทาง​เทคนิค​บาง​ส่วน​ที่​ต้อง​ใช้​ใน​การ implement Aggregate พื้นฐาน ตัวอย่าง​ต่อ​ไป​นี้​เป็น​ภาษา C# แต่​ผู้​ที่​ใช้ Scala, F#, Java, Ruby, Python ฯลฯ ก็​เข้าใจ​ได้

สิ่ง​แรก​ที่​ต้อง​ทำ​คือ​สร้าง class สำหรับ Aggregate RootAggregate RootEntity หนึ่ง​ตัว​ที่​เป็น “ประตู​เดียว” เข้า​สู่ Aggregate การ​อ้างอิง​จาก​ภายนอก​ทำได้​กับ root เท่านั้น root จึง​ควบคุม​กฎ​ความ​ถูกต้อง​ของ​ทั้ง​กลุ่มTactical Design Entity class Product extend มา​จาก class ฐาน​ชื่อ Entity ซึ่ง​ดูแล​เรื่อง​พื้นฐาน​ทั่วไป​ของ Entity:

Product.cs
public class Product : Entity
{
// ...
}

Aggregate RootAggregate RootEntity หนึ่ง​ตัว​ที่​เป็น “ประตู​เดียว” เข้า​สู่ Aggregate การ​อ้างอิง​จาก​ภายนอก​ทำได้​กับ root เท่านั้น root จึง​ควบคุม​กฎ​ความ​ถูกต้อง​ของ​ทั้ง​กลุ่มTactical Design Entity ทุก​ตัว​ต้อง​มี IdentityIdentity“ตัวตน” ที่​ทำให้ Entity เป็น​สิ่ง​เดิม​ตลอด​อายุ แม้​ค่า​เปลี่ยน เช่น เลข​ที่​คำ​สั่ง​ซื้อ หรือ id ที่​ระบบ​สร้าง​ให้Tactical Design ที่ unique ทั่ว​ทั้ง​ระบบ (globally unique) Product ใน Agile Project Management Context จริงๆ แล้ว​มี identity ที่ unique ทั่ว​ระบบ​ถึง​สอง​รูปแบบ รูปแบบ​แรก​คือ TenantId ซึ่ง​จำกัด​ขอบเขต Root Entity ให้​อยู่​ภายใน​องค์กร​ผู้​สมัคร​ใช้​บริการ​ราย​หนึ่ง (ทุก​องค์กร​ที่​สมัคร​ใช้​บริการ​เรียก​ว่า tenant และ​มี identity เฉพาะ​ของ​ตัวเอง) อีก​รูปแบบ​หนึ่ง​ซึ่ง​ก็ unique ทั่ว​ระบบ​เช่น​กัน​คือ ProductId ที่​แยก Product ตัว​นี้​ออก​จาก​ตัวอื่นๆ ทั้งหมด​ภายใน tenant เดียวกัน:

Product.cs
public class Product : Entity
{
private ProductId productId;
private TenantId tenantId;
}

โดย TenantId และ ProductId ต่าง​ถูก​จำลอง​เป็น Value ObjectValue Objectอ็อบเจ็กต์ที่​สำคัญ​ที่ “ค่า” ของ​มัน ไม่มี​ตัวตน​เฉพาะ สอง​ตัว​ที่​ค่า​เท่า​กัน​ถือว่า​เท่า​กัน ควร​เป็น immutable (เปลี่ยน​ค่า​ไม่​ได้) เช่น เงิน, วัน​ที่, พิกัดTactical Design ที่ ImmutableImmutableเปลี่ยนแปลง​ไม่​ได้​หลัง​สร้าง ถ้า​จะ “เปลี่ยน​ค่า” ต้อง​สร้าง​อ็อบเจ็กต์ใหม่​แทน หลักการ​สำคัญ​ของ Value Object เพื่อ​เลี่ยง bug จาก​การ​ใช้​อ้างอิง​ร่วม​กันTactical Design จาก​นั้น​จึง​เก็บ intrinsic attribute หรือ field ที่​จำเป็นต่อ​การ “ค้นหา” Aggregate ใน​กรณี​ของ Product คือ description และ name ซึ่ง​ผู้​ใช้​สามารถ​ค้นหา​ด้วย​อย่าง​ใด​อย่าง​หนึ่ง​หรือ​ทั้ง​คู่​เพื่อ​หา Product แต่ละ​ตัว:

Product.cs
public class Product : Entity
{
private string description;
private string name;
private ProductId productId;
private TenantId tenantId;
}

แน่นอน​ว่า​เรา​เพิ่ม​พฤติกรรม​พื้นฐาน​อย่าง read accessor (getter) ให้ intrinsic attribute ได้ ใน C# มัก​ทำ​ผ่าน public property getter อย่างไร​ก็ตาม อาจ​ไม่​ต้องการ​เปิด setter เป็น public ถ้า​ไม่มี public setter แล้ว​ค่า​จะ​เปลี่ยน​ได้​อย่างไร? เมื่อ​ใช้​แนวทาง object-oriented (C#, Scala, Java) เรา​เปลี่ยน internal state ผ่าน behavioral method ส่วน​ถ้า​ใช้​แนวทาง functional (F#, Scala, Clojure) function จะ​คืน​ค่า​ใหม่​ที่​ต่าง​จาก​ค่าที่​รับ​เข้า​มา​เป็น argument:

Product.cs
public class Product : Entity
{
// ...
public string Description { get; private set; }
public string Name { get; private set; }
}

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

สุดท้าย​จึง​เพิ่ม​พฤติกรรม​ที่​ซับซ้อน​เข้าไป ใน​ตัวอย่าง​นี้​มี method ใหม่​สี่​ตัว ได้แก่ PlanBacklogItem(), PlannedProductBacklogItem(), ScheduleRelease() และ ScheduleSprint():

Product.cs
public class Product : Entity
{
// ...
public void PlannedProductBacklogItem(/* ... */)
{
// ...
}
}

อย่า​ลืม​ว่า​เมื่อ​ใช้ DDD เรา​จำลอง Ubiquitous LanguageUbiquitous Languageภาษา​กลาง​ที่​ทุก​คนใน​ทีม (ทั้ง​นัก​พัฒนา​และ​ผู้เชี่ยวชาญ​ธุรกิจ) ใช้​ร่วม​กัน อิง​กับ domain model โดยตรง คำ​เดียวกัน​ต้อง​หมาย​ถึง​สิ่ง​เดียวกัน และ​สะท้อน​ลง​ใน codeStrategic Design ภายใน Bounded ContextBounded Contextขอบเขต​ที่​ชัดเจน (มัก​เป็น​ระบบ​ย่อย​หรือ​ทีม​หนึ่ง) ที่ model หนึ่ง​ใช้ได้​และ​มี​ความหมาย​แน่นอน ภายใน​ขอบเขต​นี้ Ubiquitous Language สอดคล้อง​กัน 100%Strategic Design เสมอ ดังนั้น​ทุก​ส่วน​ของ Product Aggregate จึง​ถูก​จำลอง​ตาม Ubiquitous Language เรา​ไม่​ได้​กุ​ชิ้น​ส่วนประกอบ​เหล่า​นี้​ขึ้น​มา​เอง ทุก​อย่าง​ต้อง​สะท้อน​ความ​กลมกลืน​ระหว่าง Domain ExpertDomain Expertผู้เชี่ยวชาญ​ธุรกิจ​ที่​รู้​กฎ กระบวนการ และ​ความ​ต้องการ​ของ domain อย่าง​ลึกซึ้ง เป็น​แหล่ง​ความ​รู้​สำคัญ​ที่​ทีม​พัฒนา​ต้อง​ร่วม​งาน​ด้วย​อย่าง​ใกล้​ชิด​ใน DDDStrategic Design กับ​นัก​พัฒนา​ใน​ทีม​ที่​ทำงาน​ใกล้​ชิด​กัน

model ซอฟต์แวร์​ที่​ได้​ผล​จะ​อิง​อยู่​บน​ชุด abstraction ที่​ตอบ​สนอง​วิธี​ทำงาน​ของ​ธุรกิจ​เสมอ แต่​ก็​ต้อง​เลือก “ระดับ​ของ abstraction” ที่​เหมาะสม​สำหรับ​แต่ละ​แนวคิด​ที่​จะ​จำลอง ถ้า​เดิน​ตาม​ทิศทาง​ของ Ubiquitous LanguageUbiquitous Languageภาษา​กลาง​ที่​ทุก​คนใน​ทีม (ทั้ง​นัก​พัฒนา​และ​ผู้เชี่ยวชาญ​ธุรกิจ) ใช้​ร่วม​กัน อิง​กับ domain model โดยตรง คำ​เดียวกัน​ต้อง​หมาย​ถึง​สิ่ง​เดียวกัน และ​สะท้อน​ลง​ใน codeStrategic Designก็​จะ​สร้าง abstraction ที่​ถูกต้อง เพราะ​การ​จำลอง​ให้​ถูก​ง่าย​ขึ้น​มาก​เมื่อ Domain ExpertDomain Expertผู้เชี่ยวชาญ​ธุรกิจ​ที่​รู้​กฎ กระบวนการ และ​ความ​ต้องการ​ของ domain อย่าง​ลึกซึ้ง เป็น​แหล่ง​ความ​รู้​สำคัญ​ที่​ทีม​พัฒนา​ต้อง​ร่วม​งาน​ด้วย​อย่าง​ใกล้​ชิด​ใน DDDStrategic Design เป็น​ผู้​ถ่ายทอด​ที่มา​ของ​ภาษา​ที่​ใช้​จำลอง ถึง​กระนั้น​บาง​ครั้ง​นัก​พัฒนา​ที่​กระตือรือร้น​เกิน​ไป​กับ​การ​แก้​ปัญหา​ผิดๆ ก็​พยายาม​ยัด abstraction ที่ “นามธรรม​เกิน​ไป”

ยก​ตัวอย่าง ใน Agile Project Management Context เรา​กำลัง​จัดการ​กับ Scrum การ​จำลอง​แนวคิด Product, BacklogItem, Release และ Sprint ตาม​ที่​คุย​กัน​มา​ย่อม​สม​เหตุ​สม​ผล แต่​ถ้า​นัก​พัฒนา​สนใจ​การ​จำลอง Ubiquitous Language ของ Scrum น้อย​ลง แล้ว​หัน​ไป​สนใจ​การ​จำลอง “ทางออก​สำหรับ​แนวคิด Scrum ทั้งหมด​ทั้ง​ใน​ปัจจุบัน​และ​อนาคต” ล่ะ?

ถ้า​ไล่​ตาม​มุม​นี้ นัก​พัฒนา​คง​คิด abstraction อย่าง ScrumElement และ ScrumElementContainer ขึ้น​มา โดย ScrumElement อาจ​รองรับ​ทั้ง Product และ BacklogItem ส่วน ScrumElementContainer แทน​แนวคิด​ที่​ชัดเจน​กว่า​อย่าง Release และ Sprint แล้ว​ให้ ScrumElement มี property ชื่อ typeName ตั้ง​ค่า​เป็น "Product" หรือ "BacklogItem" ตาม​กรณี และ​ออกแบบ typeName ทำนอง​เดียวกัน​ให้ ScrumElementContainer รับ​ค่า "Release" หรือ "Sprint" ได้

เห็น​ปัญหา​ของ​แนวทาง​นี้​ไหม? มี​หลาย​ข้อ​ที​เดียว:

  • ภาษา​ของ model ซอฟต์แวร์​ไม่​ตรง​กับ mental model ของ Domain ExpertDomain Expertผู้เชี่ยวชาญ​ธุรกิจ​ที่​รู้​กฎ กระบวนการ และ​ความ​ต้องการ​ของ domain อย่าง​ลึกซึ้ง เป็น​แหล่ง​ความ​รู้​สำคัญ​ที่​ทีม​พัฒนา​ต้อง​ร่วม​งาน​ด้วย​อย่าง​ใกล้​ชิด​ใน DDDStrategic Design
  • ระดับ abstraction สูง​เกิน​ไป จะ​เจอ​ปัญหา​หนัก​เมื่อ​เริ่ม​จำลอง​รายละเอียด​ของ​แต่ละ type
  • นำ​ไป​สู่​การ​สร้าง special case ใน​แต่ละ class และ​มี​แนวโน้ม​กลาย​เป็น class hierarchy ที่​ซับซ้อน​ซึ่ง​ใช้​วิธีทั่วๆ ไป​แก้​ปัญหา​เฉพาะ​เจาะจง
  • จะมี code มาก​เกิน​จำเป็น เพราะ​พยายาม​แก้​ปัญหา​ที่​แก้​ไม่​ได้ ซึ่ง​ไม่​ควร​มี​ความ​สำคัญ​ตั้งแต่​แรก
  • ภาษา​ของ abstraction ผิดๆ มัก​เล็ดลอด​ไป​ถึง user interface จน​ทำให้​ผู้​ใช้​สับสน
  • เสีย​ทั้ง​เวลา​และ​เงิน​ไป​มาก
  • ไม่มี​วัน​รองรับ​ความ​ต้องการ​อนาคต​ได้​ทั้งหมด​ตั้งแต่​ต้น แปล​ว่า​หาก​มี​แนวคิด Scrum ใหม่​เพิ่ม​ใน​อนาคต model เดิม​ก็​จะ​พิสูจน์​ว่า​ล้มเหลว​ใน​การ​คาด​การณ์​ความ​ต้องการ​เหล่า​นั้น

แนวทาง​นี้​อาจ​ดู​แปลก​สำหรับ​บาง​คน แต่​ระดับ abstraction ที่​ผิด​นี้​กลับ​ถูก​ใช้​บ่อย​ใน​การ implement ที่​ขับ​เคลื่อน​ด้วย​มุมมอง​เชิง​เทคนิค อย่า​ไป​ติด​กับดัก​ของ​การ implement ที่​ดูหรูหรา​นามธรรม​เกิน​จริง​นี้ จง​จำลอง Ubiquitous Language อย่าง​ชัดเจน​ตาม mental model ของ Domain Expert ที่​ทีม​กลั่น​มา การ​จำลอง​สิ่ง​ที่​ธุรกิจ​ต้องการ “วัน​นี้” จะ​ช่วย​ประหยัด​เวลา งบประมาณ code และ​ความ​น่า​อับอาย​ไป​ได้​มาก ยิ่ง​กว่า​นั้น​ยัง​เป็นการ​ช่วย​ธุรกิจ​ด้วย​การ​จำลอง Bounded ContextBounded Contextขอบเขต​ที่​ชัดเจน (มัก​เป็น​ระบบ​ย่อย​หรือ​ทีม​หนึ่ง) ที่ model หนึ่ง​ใช้ได้​และ​มี​ความหมาย​แน่นอน ภายใน​ขอบเขต​นี้ Ubiquitous Language สอดคล้อง​กัน 100%Strategic Design ที่​แม่นยำ​และ​ใช้งาน​ได้​จริง ซึ่ง​สะท้อน effective design

อาจ​สงสัย​ว่า​จะ​กำหนด​ขอบเขต​ของ Aggregate และ​ป้องกัน​การ​ออกแบบ​กลุ่ม​ใหญ่ ใน​ขณะ​ที่​ยัง​รักษา​ขอบเขต​ความ​สอดคล้อง​ที่​ปกป้อง business invariant จริงๆ ได้​อย่างไร ต่อ​ไป​นี้​คือ​ขั้นตอน​การ​ออกแบบ​ที่​ช่วย​ให้​บรรลุ​เป้าหมาย​ขอบเขต​ความ​สอดคล้อง​ได้ (ถ้า​มี Aggregate กลุ่ม​ใหญ่​อยู่​แล้ว ก็​ใช้​แนวทาง​นี้ refactor ให้​เล็ก​ลง​ได้):

  1. โฟกัส​ที่​กฎ​ข้อ 2 ก่อน “ออกแบบ Aggregate ให้​เล็ก” เริ่ม​ด้วย​การ​สร้าง​ทุก Aggregate ให้​มี EntityEntityอ็อบเจ็กต์ที่​มี “ตัวตน” (identity) ต่อ​เนื่อง​ตาม​เวลา แม้​ค่า​ภายใน​เปลี่ยน ก็​ยัง​เป็น​สิ่ง​เดิม เช่น ลูกค้า, คำ​สั่ง​ซื้อ — แยกแยะ​ด้วย id ไม่ใช่​ด้วย​ค่าTactical Design เพียง​ตัว​เดียว ซึ่ง​จะ​ทำ​หน้าที่​เป็น Aggregate Root อย่า​เพิ่ง​กล้า​วาง2 Entity ไว้​ใน​ขอบเขต​เดียว โอกาส​นั้น​จะ​มา​ถึง​เอง​ใน​ไม่​ช้า ใส่ field/attribute/property ที่​ผูก​กับ Root Entity ตัว​เดียว​นี้​ใกล้​ชิด​ที่สุด รวม​ถึง​นิยาม​ทุก field ที่​จำเป็นต่อ​การ “ระบุ​และ​ค้นหา” Aggregate และ field ที่​จำเป็นต่อ​การ​สร้าง Aggregate ให้​อยู่​ใน​สถานะ​เริ่มต้น​ที่​ถูกต้อง
  2. จาก​นั้น​โฟกัส​ที่​กฎ​ข้อ 1 “ปกป้อง business invariant ไว้​ภายใน​ขอบเขต​ของ Aggregate” เรา​ได้​ยืนยัน​จาก​ขั้น​ที่​แล้ว​ว่า​อย่าง​น้อย​ทุก intrinsic field/attribute ต้อง​เป็น​ปัจจุบัน​เมื่อ Aggregate แบบ single-Entity ถูก​บันทึก แต่​ตอน​นี้​ต้อง​พิจารณา Aggregate ที​ละ​ตัว ขณะ​พิจารณา Aggregate A1 ให้​ถาม Domain ExpertDomain Expertผู้เชี่ยวชาญ​ธุรกิจ​ที่​รู้​กฎ กระบวนการ และ​ความ​ต้องการ​ของ domain อย่าง​ลึกซึ้ง เป็น​แหล่ง​ความ​รู้​สำคัญ​ที่​ทีม​พัฒนา​ต้อง​ร่วม​งาน​ด้วย​อย่าง​ใกล้​ชิด​ใน DDDStrategic Design ว่า​มี Aggregate อื่น​ใด​ที่​ต้อง​อัปเดต “เพื่อ​ตอบ​สนอง” การ​เปลี่ยนแปลง​ของ A1 บ้าง ทำ​รายการ​ของ Aggregate แต่ละ​ตัว​พร้อม​กฎ​ความ​สอดคล้อง ซึ่ง​จะ​บ่ง​บอก​กรอบ​เวลา​ของ​การ​อัปเดต​ทั้งหมด​ที่​เกิด​จาก​การ​ตอบ​สนอง คือ “Aggregate A1” เป็น​หัวข้อ​ของ​รายการ​หนึ่ง แล้ว list Aggregate type อื่นๆ ที่​จะ​อัปเดต​เพื่อ​ตอบ​สนอง A1 ไว้​ใต้​หัวข้อ​นั้น
  3. ถาม Domain Expert ว่า​แต่ละ​การ​อัปเดต​ที่​เป็นการ​ตอบ​สนอง​นั้น รอ​ได้​นาน​แค่​ไหน จะ​นำ​ไป​สู่​ข้อ​กำหนด​สอง​แบบ คือ (a) ทันที (immediately) และ (b) ภายใน N วินาที/นาที/ชั่วโมง/วัน วิธี​หนึ่ง​ที่​ช่วย​หา threshold ที่​ถูกต้อง​คือ เสนอ​กรอบ​เวลา​เกิน​จริง (เช่น เป็น​สัปดาห์​หรือ​เป็น​เดือน) ที่​ยอมรับ​ไม่​ได้​อย่าง​ชัดเจน ซึ่ง​มัก​กระตุ้น​ให้​ผู้เชี่ยวชาญ​ธุรกิจ​ตอบ​กลับ​มา​ด้วย​กรอบ​เวลา​ที่​ยอมรับ​ได้
  4. สำหรับ​กรอบ​เวลา​แบบ​ทันที (3a) ควร​พิจารณา​อย่าง​หนักแน่น​ที่​จะ “ประกอบ” 2 Entity นั้น​ไว้​ใน​ขอบเขต Aggregate เดียวกัน เช่น Aggregate A1 และ A2 จะ​ถูกรวม​เป็น Aggregate ใหม่ A[1,2] ตอน​นี้ A1 และ A2 แบบ​เดิม​จะ​ไม่มี​อยู่​อีก​ต่อ​ไป เหลือ​เพียง A[1,2]
  5. สำหรับ Aggregate ที่​ตอบ​สนอง​ได้​หลัง​เวลา​ผ่าน​ไป​ระยะ​หนึ่ง (3b) ให้​อัปเดต​ด้วย​กฎ​ข้อ 4 “อัปเดต Aggregate อื่น​ด้วย eventual consistency”

ควร​ออกแบบ Aggregate ให้​เป็น encapsulation ที่​ดี​สำหรับ​การ​ทำ unit test ด้วย Aggregate ที่​ซับซ้อน​นั้น​ทดสอบ​ยาก การ​ทำ​ตาม​คำ​แนะนำ​การ​ออกแบบ​ข้าง​ต้น​จะ​ช่วย​ให้​จำลอง Aggregate ที่​ทดสอบ​ได้

Unit test ต่าง​จาก​การ validate business specification (acceptance test) สิ่ง​ที่​เรา​สนใจ​ตรง​นี้​คือ​การ​ทดสอบ​ว่า Aggregate ทำงาน​ถูกต้อง​ตาม​ที่​ควร​จะ​เป็น​หรือ​ไม่ เรา​อยาก​กด​ทุก operation ให้​ทำงาน เพื่อ​รับรอง​ความ​ถูกต้อง คุณภาพ และ​ความ​เสถียร​ของ Aggregate ใช้ unit testing framework ทำได้ และ​มี​เอกสาร​มากมาย​ว่า​จะ unit test อย่าง​มี​ประสิทธิภาพ​ได้​อย่างไร unit test เหล่า​นี้​จะ​ผูก​กับ Bounded ContextBounded Contextขอบเขต​ที่​ชัดเจน (มัก​เป็น​ระบบ​ย่อย​หรือ​ทีม​หนึ่ง) ที่ model หนึ่ง​ใช้ได้​และ​มี​ความหมาย​แน่นอน ภายใน​ขอบเขต​นี้ Ubiquitous Language สอดคล้อง​กัน 100%Strategic Design โดยตรง และ​เก็บ​ไว้​ด้วย​กัน​ใน source code repository ของ​มัน

🛒 เทียบ​กับ e-commerce

ใน​ระบบ e-commerce Order มัก​เป็น Aggregate RootAggregate RootEntity หนึ่ง​ตัว​ที่​เป็น “ประตู​เดียว” เข้า​สู่ Aggregate การ​อ้างอิง​จาก​ภายนอก​ทำได้​กับ root เท่านั้น root จึง​ควบคุม​กฎ​ความ​ถูกต้อง​ของ​ทั้ง​กลุ่มTactical Design ที่​ปกป้อง InvariantInvariantกฎ​ทาง​ธุรกิจ​ที่ “ต้อง​เป็น​จริง​เสมอ” ภายใน Aggregate เช่น ยอด​รวม​คำ​สั่ง​ซื้อ​ต้อง​เท่ากับ​ผล​รวม​ของ​รายการ​สินค้า — ต้อง​ถูกต้อง​ภายใน transaction เดียวTactical Design เช่น “ยอด​รวม​ต้อง​เท่ากับ​ผล​รวม​ของ OrderLine ทุก​บรรทัด” — กฎ​นี้​ต้อง​สอดคล้อง​ทันที​จึง​อยู่​ใน Aggregate เดียวกัน ส่วน Customer หรือ Product ที่ Order อ้าง​ถึง ให้​ถือ​ไว้​แค่ CustomerId/ProductId (กฎ​ข้อ 3) และ​เมื่อ Order ถูกวาง ก็​ปล่อย Domain EventDomain Eventสิ่ง​ที่ “เกิด​ขึ้น​แล้ว” ใน domain ซึ่ง​ส่วน​อื่น​สนใจ แทน​ด้วย​อ็อบเจ็กต์ที่​เปลี่ยนแปลง​ไม่​ได้ ตั้ง​ชื่อ​เป็น​อดีต เช่น CargoWasRouted ใช้​สื่อสาร​ข้าม AggregateTactical Design ชื่อ OrderPlaced ให้ Aggregate ฝั่ง​คลัง​สินค้า​อัปเดต​แบบ Eventual ConsistencyEventual Consistencyความ​ถูกต้อง​ที่ “ตาม​มา​ทีหลัง” ไม่ใช่​ทันที ใช้​กับ​การ​อัปเดต​ข้าม Aggregate ผ่าน Domain Event แทน​การ​บังคับ​ให้​ตรง​กัน​ใน​ทันที​ภายใน transaction เดียวTactical Design (กฎ​ข้อ 4)


ใน​บท​นี้​เรา​ได้​เรียนรู้​ว่า AggregateAggregateกลุ่ม​ของ Entity และ Value Object ที่​ถูก​มอง​เป็น​หนึ่ง​หน่วย​เดียว​เพื่อ​รักษา​ความ​ถูกต้อง​ของ​ข้อมูล มี​ขอบเขต​ชัดเจน และ​เป็น​หน่วย​ของ transactionTactical Design pattern คือ​อะไร​และ​ทำไม​จึง​ควร​ใช้ ได้​เห็น​ความ​สำคัญ​ของ​การ​ออกแบบ​โดย​คำนึง​ถึง ขอบเขต​ความ​สอดคล้อง (consistency boundary) ตั้งแต่​ต้น รวม​ถึง​องค์​ประกอบ​ต่างๆ ของ Aggregate ทั้ง Aggregate RootAggregate RootEntity หนึ่ง​ตัว​ที่​เป็น “ประตู​เดียว” เข้า​สู่ Aggregate การ​อ้างอิง​จาก​ภายนอก​ทำได้​กับ root เท่านั้น root จึง​ควบคุม​กฎ​ความ​ถูกต้อง​ของ​ทั้ง​กลุ่มTactical Design, EntityEntityอ็อบเจ็กต์ที่​มี “ตัวตน” (identity) ต่อ​เนื่อง​ตาม​เวลา แม้​ค่า​ภายใน​เปลี่ยน ก็​ยัง​เป็น​สิ่ง​เดิม เช่น ลูกค้า, คำ​สั่ง​ซื้อ — แยกแยะ​ด้วย id ไม่ใช่​ด้วย​ค่าTactical Design และ Value ObjectValue Objectอ็อบเจ็กต์ที่​สำคัญ​ที่ “ค่า” ของ​มัน ไม่มี​ตัวตน​เฉพาะ สอง​ตัว​ที่​ค่า​เท่า​กัน​ถือว่า​เท่า​กัน ควร​เป็น immutable (เปลี่ยน​ค่า​ไม่​ได้) เช่น เงิน, วัน​ที่, พิกัดTactical Design

เรา​ได้​เจาะ กฎ​สี่​ข้อ ของ​การ​ออกแบบ Aggregate ที่​ได้​ผล — ปกป้อง InvariantInvariantกฎ​ทาง​ธุรกิจ​ที่ “ต้อง​เป็น​จริง​เสมอ” ภายใน Aggregate เช่น ยอด​รวม​คำ​สั่ง​ซื้อ​ต้อง​เท่ากับ​ผล​รวม​ของ​รายการ​สินค้า — ต้อง​ถูกต้อง​ภายใน transaction เดียวTactical Design ทาง​ธุรกิจ, ออกแบบ​ให้​เล็ก, อ้างอิง Aggregate อื่น​ด้วย identity เท่านั้น และ​อัปเดต Aggregate อื่น​ด้วย Eventual ConsistencyEventual Consistencyความ​ถูกต้อง​ที่ “ตาม​มา​ทีหลัง” ไม่ใช่​ทันที ใช้​กับ​การ​อัปเดต​ข้าม Aggregate ผ่าน Domain Event แทน​การ​บังคับ​ให้​ตรง​กัน​ใน​ทันที​ภายใน transaction เดียวTactical Design ได้​เห็น​วิธี​จำลอง IdentityIdentity“ตัวตน” ที่​ทำให้ Entity เป็น​สิ่ง​เดิม​ตลอด​อายุ แม้​ค่า​เปลี่ยน เช่น เลข​ที่​คำ​สั่ง​ซื้อ หรือ id ที่​ระบบ​สร้าง​ให้Tactical Design ที่ unique ทั่ว​ระบบ​ให้ Aggregate ความ​สำคัญ​ของ attribute และ​วิธี​ป้องกัน​การ​สร้าง Anemic Domain ModelAnemic Domain Modelanti-pattern ที่อ็อบเจ็กต์มี​แต่ getter/setter ไร้​พฤติกรรม ตรรกะ​ถูก​ดึง​ไป​ไว้​ใน service ทั้งหมด — จ่าย​ค่า domain model แต่​ไม่​ได้​ประโยชน์​ของ​มัน​เลยTactical Design ด้วย​การ​รักษา model ที่​มี​พฤติกรรม​สมบูรณ์ (Rich Domain Model) ตลอด​จน​วิธี​จำลอง​พฤติกรรม​บน Aggregate ผ่าน behavioral method แทน​การ​เปิด public setter

นอกจาก​นี้​ยัง​ย้ำ​ว่า​ต้อง​ยึด Ubiquitous LanguageUbiquitous Languageภาษา​กลาง​ที่​ทุก​คนใน​ทีม (ทั้ง​นัก​พัฒนา​และ​ผู้เชี่ยวชาญ​ธุรกิจ) ใช้​ร่วม​กัน อิง​กับ domain model โดยตรง คำ​เดียวกัน​ต้อง​หมาย​ถึง​สิ่ง​เดียวกัน และ​สะท้อน​ลง​ใน codeStrategic Design ภายใน Bounded ContextBounded Contextขอบเขต​ที่​ชัดเจน (มัก​เป็น​ระบบ​ย่อย​หรือ​ทีม​หนึ่ง) ที่ model หนึ่ง​ใช้ได้​และ​มี​ความหมาย​แน่นอน ภายใน​ขอบเขต​นี้ Ubiquitous Language สอดคล้อง​กัน 100%Strategic Design เสมอ เลือก​ระดับ abstraction ที่​เหมาะสม และ​ใช้​เทคนิค right-sizing ใน​การ​ประกอบ Aggregate ซึ่ง​รวม​ถึง​การ​ออกแบบ​เพื่อ​ให้​ทดสอบ​ได้​ด้วย สำหรับ​การ​ศึกษา Entity, Value Object และ Aggregate อย่าง​ลึกซึ้ง Vernon แนะนำ​ให้​ตาม​อ่าน​ใน Implementing Domain-Driven Design ของ​เขา​เอง

หัวใจ​ของ​บท​นี้
  • Aggregate คือ​ขอบเขต​ความ​สอดคล้อง​เชิง transaction — ภายใน​ต้อง​สอดคล้อง​กัน​ตาม​กฎ​ธุรกิจ​เมื่อ commit และ​แก้​ได้​เพียง1 instance ต่อ1 transaction
  • ออกแบบ​ให้ เล็ก: เริ่ม​จาก Root Entity ตัว​เดียว แล้ว​รวม​เข้า​ด้วย​กัน​เฉพาะ​เมื่อ InvariantInvariantกฎ​ทาง​ธุรกิจ​ที่ “ต้อง​เป็น​จริง​เสมอ” ภายใน Aggregate เช่น ยอด​รวม​คำ​สั่ง​ซื้อ​ต้อง​เท่ากับ​ผล​รวม​ของ​รายการ​สินค้า — ต้อง​ถูกต้อง​ภายใน transaction เดียวTactical Design ทาง​ธุรกิจ​บังคับ​ให้​ต้อง​สอดคล้อง​ทันที
  • อ้างอิง Aggregate อื่น ด้วย identity เท่านั้น (เช่น ProductId) ไม่ใช่ object reference ตรงๆ
  • อัปเดต​ข้าม Aggregate ด้วย Eventual ConsistencyEventual Consistencyความ​ถูกต้อง​ที่ “ตาม​มา​ทีหลัง” ไม่ใช่​ทันที ใช้​กับ​การ​อัปเดต​ข้าม Aggregate ผ่าน Domain Event แทน​การ​บังคับ​ให้​ตรง​กัน​ใน​ทันที​ภายใน transaction เดียวTactical Design ผ่าน Domain EventDomain Eventสิ่ง​ที่ “เกิด​ขึ้น​แล้ว” ใน domain ซึ่ง​ส่วน​อื่น​สนใจ แทน​ด้วย​อ็อบเจ็กต์ที่​เปลี่ยนแปลง​ไม่​ได้ ตั้ง​ชื่อ​เป็น​อดีต เช่น CargoWasRouted ใช้​สื่อสาร​ข้าม AggregateTactical Design — และ​นี่​คือ​การ​ตัดสิน​ใจ​ที่​ขับ​เคลื่อน​ด้วย​ธุรกิจ ไม่ใช่​เทคนิค
  • หลีก​เลี่ยง Anemic Domain ModelAnemic Domain Modelanti-pattern ที่อ็อบเจ็กต์มี​แต่ getter/setter ไร้​พฤติกรรม ตรรกะ​ถูก​ดึง​ไป​ไว้​ใน service ทั้งหมด — จ่าย​ค่า domain model แต่​ไม่​ได้​ประโยชน์​ของ​มัน​เลยTactical Design และ abstraction ที่​นามธรรม​เกิน​จริง — จำลอง​ตาม Ubiquitous LanguageUbiquitous Languageภาษา​กลาง​ที่​ทุก​คนใน​ทีม (ทั้ง​นัก​พัฒนา​และ​ผู้เชี่ยวชาญ​ธุรกิจ) ใช้​ร่วม​กัน อิง​กับ domain model โดยตรง คำ​เดียวกัน​ต้อง​หมาย​ถึง​สิ่ง​เดียวกัน และ​สะท้อน​ลง​ใน codeStrategic Design ของ Domain ExpertDomain Expertผู้เชี่ยวชาญ​ธุรกิจ​ที่​รู้​กฎ กระบวนการ และ​ความ​ต้องการ​ของ domain อย่าง​ลึกซึ้ง เป็น​แหล่ง​ความ​รู้​สำคัญ​ที่​ทีม​พัฒนา​ต้อง​ร่วม​งาน​ด้วย​อย่าง​ใกล้​ชิด​ใน DDDStrategic Design
🔗 อ้างอิง​เพิ่มเติม​ใน DevIQ

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

  • Aggregate — นิยาม​และ​กฎ​การ​ออกแบบ Aggregate แบบ​สรุป ตรง​กับ​แกน​หลัก​ของ​บท​นี้
  • Entity — องค์​ประกอบ​พื้นฐาน​ที่​ประกอบ​กัน​เป็น Aggregate โดย​มี Aggregate Root เป็น Entity หนึ่ง​ตัว
  • Value Object — องค์​ประกอบ immutable อีก​ชนิด​ที่​อยู่​ภายใน Aggregate ได้ เช่น ProductId
  • Anemic Domain Model — ตะขอ​สำคัญ​ที่​ต้อง​หลีก​เลี่ยง​เมื่อ​สร้าง model Aggregate ให้​มี​พฤติกรรม​สมบูรณ์
  • Domain Events — กลไก​ที่​ใช้​ทำ Eventual Consistency ตาม​กฎ​ข้อ 4 ระหว่าง Aggregate

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

ข้อ 1 / 4

Aggregate คือ “ขอบเขต” ของอะไรเป็นอันดับแรก?