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 เป็นกรณีศึกษาตลอดทั้งบท
ทำไมต้องใช้ Aggregate (Why Used)
หัวข้อที่มีชื่อว่า “ทำไมต้องใช้ Aggregate (Why Used)”ลองนึกถึง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 ของเขาเอง
ขอบเขตความสอดคล้องเชิง Transaction (Transactional Consistency Boundary)
หัวข้อที่มีชื่อว่า “ขอบเขตความสอดคล้องเชิง Transaction (Transactional Consistency Boundary)”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 ใน 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 (Aggregate Rules of Thumb)
หัวข้อที่มีชื่อว่า “กฎสี่ข้อในการออกแบบ Aggregate (Aggregate Rules of Thumb)”ทีนี้มาดูกฎพื้นฐานสี่ข้อของการออกแบบ Aggregate กัน:
- ปกป้อง business invariant ไว้ภายในขอบเขตของ Aggregate (Protect Business Invariants inside Aggregate Boundaries)
- ออกแบบ Aggregate ให้เล็ก (Design Small Aggregates)
- อ้างอิง Aggregate อื่นด้วย identity เท่านั้น (Reference Other Aggregates by Identity Only)
- อัปเดต Aggregate อื่นด้วย Eventual Consistency (Update Other Aggregates Using Eventual Consistency)
แน่นอนว่ากฎเหล่านี้ไม่ได้มี “ตำรวจ DDD” มาคอยบังคับใช้อย่างเคร่งครัด มันเป็นเพียงแนวทางที่ดี ซึ่งเมื่อนำไปใช้อย่างใคร่ครวญแล้ว จะช่วยให้ออกแบบ Aggregate ที่ทำงานได้อย่างมีประสิทธิภาพ ต่อไปจะเจาะแต่ละข้อว่าควรนำไปใช้อย่างไรในทุกที่ที่ทำได้
กฎข้อ 1: ปกป้อง Business Invariant ไว้ภายในขอบเขตของ Aggregate
หัวข้อที่มีชื่อว่า “กฎข้อ 1: ปกป้อง Business Invariant ไว้ภายในขอบเขตของ 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
ขอบเขตของ Aggregate ถูกกำหนดโดย กฎธุรกิจที่ต้องเป็นจริงเสมอ ณ ตอนจบ transaction ไม่ใช่ความสะดวกทางเทคนิคหรือความเคยชินกับ data model สิ่งที่ต้อง “สอดคล้องทันที” เท่านั้นจึงควรอยู่ใน Aggregate เดียวกัน
กฎข้อ 2: ออกแบบ Aggregate ให้เล็ก
หัวข้อที่มีชื่อว่า “กฎข้อ 2: ออกแบบ 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
กฎข้อ 3: อ้างอิง Aggregate อื่นด้วย Identity เท่านั้น
หัวข้อที่มีชื่อว่า “กฎข้อ 3: อ้างอิง Aggregate อื่นด้วย Identity เท่านั้น”ในเมื่อเราแยก 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
กฎข้อ 4: อัปเดต Aggregate อื่นด้วย Eventual Consistency
หัวข้อที่มีชื่อว่า “กฎข้อ 4: อัปเดต Aggregate อื่นด้วย Eventual Consistency”สมมติ 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 ไม่ได้ยากเย็นอะไร แต่ถ้ายังไม่มีประสบการณ์และยังกังวล ก็ยังควรแบ่ง 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 ถูกขับเคลื่อนด้วยธุรกิจ ไม่ใช่ด้วยเทคนิค มีเพียงธุรกิจเท่านั้นที่ตัดสินได้ว่ากรอบเวลาที่ยอมรับได้สำหรับการอัปเดตระหว่าง Entity ต่างๆ ควรเป็นเท่าไร บางอย่างต้องทันที (transactional) คือต้องบริหารด้วย Aggregate เดียวกัน บางอย่างเป็น eventual คือบริหารผ่าน Domain Event และ messaging ได้ ลองนึกว่าถ้าธุรกิจต้องดำเนินงานด้วยระบบกระดาษล้วนๆ จะทำอย่างไร — มักให้ insight ที่มีค่าว่าการทำงานต่างๆ ควรทำงานอย่างไรใน model ซอฟต์แวร์
การสร้าง model Aggregate (Modeling Aggregates)
หัวข้อที่มีชื่อว่า “การสร้าง model Aggregate (Modeling Aggregates)”ระหว่างที่ลงมือ 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 กฎจะเปลี่ยนไปพอสมควร แม้ 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:
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 เดียวกัน:
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 แต่ละตัว:
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:
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():
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 กับนักพัฒนาในทีมที่ทำงานใกล้ชิดกัน
เลือก Abstraction อย่างรอบคอบ (Choose Your Abstractions Carefully)
หัวข้อที่มีชื่อว่า “เลือก Abstraction อย่างรอบคอบ (Choose Your Abstractions Carefully)”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
Right-Sizing — กำหนดขนาด Aggregate ให้พอดี
หัวข้อที่มีชื่อว่า “Right-Sizing — กำหนดขนาด Aggregate ให้พอดี”อาจสงสัยว่าจะกำหนดขอบเขตของ Aggregate และป้องกันการออกแบบกลุ่มใหญ่ ในขณะที่ยังรักษาขอบเขตความสอดคล้องที่ปกป้อง business invariant จริงๆ ได้อย่างไร ต่อไปนี้คือขั้นตอนการออกแบบที่ช่วยให้บรรลุเป้าหมายขอบเขตความสอดคล้องได้ (ถ้ามี Aggregate กลุ่มใหญ่อยู่แล้ว ก็ใช้แนวทางนี้ refactor ให้เล็กลงได้):
- โฟกัสที่กฎข้อ 2 ก่อน “ออกแบบ Aggregate ให้เล็ก” เริ่มด้วยการสร้างทุก Aggregate ให้มี EntityEntityอ็อบเจ็กต์ที่มี “ตัวตน” (identity) ต่อเนื่องตามเวลา แม้ค่าภายในเปลี่ยน ก็ยังเป็นสิ่งเดิม เช่น ลูกค้า, คำสั่งซื้อ — แยกแยะด้วย id ไม่ใช่ด้วยค่าTactical Design เพียงตัวเดียว ซึ่งจะทำหน้าที่เป็น Aggregate Root อย่าเพิ่งกล้าวาง2 Entity ไว้ในขอบเขตเดียว โอกาสนั้นจะมาถึงเองในไม่ช้า ใส่ field/attribute/property ที่ผูกกับ Root Entity ตัวเดียวนี้ใกล้ชิดที่สุด รวมถึงนิยามทุก field ที่จำเป็นต่อการ “ระบุและค้นหา” Aggregate และ field ที่จำเป็นต่อการสร้าง Aggregate ให้อยู่ในสถานะเริ่มต้นที่ถูกต้อง
- จากนั้นโฟกัสที่กฎข้อ 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 ไว้ใต้หัวข้อนั้น
- ถาม Domain Expert ว่าแต่ละการอัปเดตที่เป็นการตอบสนองนั้น รอได้นานแค่ไหน จะนำไปสู่ข้อกำหนดสองแบบ คือ (a) ทันที (immediately) และ (b) ภายใน N วินาที/นาที/ชั่วโมง/วัน วิธีหนึ่งที่ช่วยหา threshold ที่ถูกต้องคือ เสนอกรอบเวลาเกินจริง (เช่น เป็นสัปดาห์หรือเป็นเดือน) ที่ยอมรับไม่ได้อย่างชัดเจน ซึ่งมักกระตุ้นให้ผู้เชี่ยวชาญธุรกิจตอบกลับมาด้วยกรอบเวลาที่ยอมรับได้
- สำหรับกรอบเวลาแบบทันที (3a) ควรพิจารณาอย่างหนักแน่นที่จะ “ประกอบ” 2 Entity นั้นไว้ในขอบเขต Aggregate เดียวกัน เช่น Aggregate A1 และ A2 จะถูกรวมเป็น Aggregate ใหม่ A[1,2] ตอนนี้ A1 และ A2 แบบเดิมจะไม่มีอยู่อีกต่อไป เหลือเพียง A[1,2]
- สำหรับ Aggregate ที่ตอบสนองได้หลังเวลาผ่านไประยะหนึ่ง (3b) ให้อัปเดตด้วยกฎข้อ 4 “อัปเดต Aggregate อื่นด้วย eventual consistency”
Testable Units — หน่วยที่ทดสอบได้
หัวข้อที่มีชื่อว่า “Testable Units — หน่วยที่ทดสอบได้”ควรออกแบบ 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 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)
สรุป (Summary)
หัวข้อที่มีชื่อว่า “สรุป (Summary)”ในบทนี้เราได้เรียนรู้ว่า 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:
- 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 / 4Aggregate คือ “ขอบเขต” ของอะไรเป็นอันดับแรก?