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

รู้จัก Building Blocks สำหรับ​สร้าง Domain Model

หลัง​จาก​ใช้​เวลา​ยาวนาน​กับ strategic design — การกลั่น DomainDomainขอบเขต​ของ​ปัญหา​หรือ​ธุรกิจ​ที่​ซอฟต์แวร์​ของ​เรา​เข้าไป​แก้ไข เช่น การ​ขนส่ง​สินค้า การ​ธนาคาร e-commerce — คือ “โลก​ของ​ผู้​ใช้” ที่​โปรแกรม​ต้อง​เข้าใจStrategic Design กำหนด Bounded ContextBounded Contextขอบเขต​ที่​ชัดเจน (มัก​เป็น​ระบบ​ย่อย​หรือ​ทีม​หนึ่ง) ที่ model หนึ่ง​ใช้ได้​และ​มี​ความหมาย​แน่นอน ภายใน​ขอบเขต​นี้ Ubiquitous Language สอดคล้อง​กัน 100%Strategic Design และ​วาด Context MapContext Mapแผนภาพ​ที่​แสดง Bounded Context ทั้งหมด​และ​ความ​สัมพันธ์​ระหว่าง​กัน ช่วย​ให้​เห็น​ภาพ​รวม​และ​จุด​เชื่อม​ต่อ/แปล​ภาษา​ใน​ระบบ​ใหญ่Strategic Design — ตอน​นี้​เรา​หัน​มา​ที่ “วิธี​ลงมือ​สร้าง model ใน code จริง” ภายใน context หนึ่งๆ Eric Evans ต่อยอด​จาก domain model pattern ที่ Martin Fowler บันทึก​ไว้​ใน Patterns of Enterprise Application Architecture แล้ว​จัด​กลุ่ม​ชุด​เครื่องมือ​ที่​เรียก​กัน​ว่า tactical patterns หรือ “building blocks” ของ DDD

บท​นี้​เป็น​บท​เกริ่น​นำ​ภาพ​รวม — เรา​จะ​แนะนำ building block แต่ละ​ตัว​แบบ high-level ว่า​มัน​คือ​อะไร​และ​มีหน้าที่​อะไร ส่วน​รายละเอียด​เชิง​ลึก​ของ​แต่ละ​ตัว​จะ​กระจายอยู่ในบทถัดๆ ไป​ของ​พาร์ตนี้ สิ่ง​สำคัญ​ที่​ควร​จำ​ตั้งแต่​ต้น​คือ pattern เหล่า​นี้​ส่วน​ใหญ่ ไม่ใช่​ของ​ใหม่ Evans เป็น​เพียง​คน​แรก​ที่​จัด​มัน​เข้า​หมวด​หมู่​เพื่อ​ช่วย​ให้​นัก​พัฒนา​สร้าง domain model ที่​ทรง​พลัง​ได้

building blocks เป็น​เพียง ‘ไกด์’ ไม่ใช่​กฎ​เหล็ก

Evans เขียน​ตำรา​ดั้งเดิม​โดย​เอนเอียง​ไป​ทาง object-oriented แต่​อย่า​มอง​ข้ามกระบวนทัศน์อื่นๆ ใน​การ​สร้าง model (เช่น functional programming) เขา​เอง​ยัง​เคย​กล่าว​ว่า​รู้สึก​เสียดาย​ที่​ไม่​ได้​รวม Domain EventDomain Eventสิ่ง​ที่ “เกิด​ขึ้น​แล้ว” ใน domain ซึ่ง​ส่วน​อื่น​สนใจ แทน​ด้วย​อ็อบเจ็กต์ที่​เปลี่ยนแปลง​ไม่​ได้ ตั้ง​ชื่อ​เป็น​อดีต เช่น CargoWasRouted ใช้​สื่อสาร​ข้าม AggregateTactical Design ไว้​ใน​ชุด building blocks ดั้งเดิม วิธี implement model เปลี่ยนแปลง​ได้​เสมอ — สิ่ง​ที่​สำคัญ​อย่าง​แท้จริง​คือ​การ​แทน​แนวคิด​ใน code ด้วย Ubiquitous LanguageUbiquitous Languageภาษา​กลาง​ที่​ทุก​คนใน​ทีม (ทั้ง​นัก​พัฒนา​และ​ผู้เชี่ยวชาญ​ธุรกิจ) ใช้​ร่วม​กัน อิง​กับ domain model โดยตรง คำ​เดียวกัน​ต้อง​หมาย​ถึง​สิ่ง​เดียวกัน และ​สะท้อน​ลง​ใน codeStrategic Design ของ domain


หน้าที่​ของ tactical patterns คือ จัดการ​ความ​ซับซ้อน​และ​ทำให้​กระบวนการ​ธุรกิจ (business process) ใน model ชัดเจน เรา​ใช้​มัน​เพื่อ​เก็บ​และ​สื่อ​ความหมาย ความ​สัมพันธ์ และ​ตรรกะ​ภายใน domain โดยที่ pattern แต่ละ​ตัว​ยึด​หลัก object-oriented ที่​ดี (หลาย​ตัว​มา​จาก Design Patterns ของ GoF)

หลัก​คิด​สำคัญ​คือ building block แต่ละ​ตัว​มี ความ​รับผิดชอบ​เดียว (single responsibility) — บาง​ตัว​มี​ไว้​แทน​แนวคิด​ใน domain (เช่น EntityEntityอ็อบเจ็กต์ที่​มี “ตัวตน” (identity) ต่อ​เนื่อง​ตาม​เวลา แม้​ค่า​ภายใน​เปลี่ยน ก็​ยัง​เป็น​สิ่ง​เดิม เช่น ลูกค้า, คำ​สั่ง​ซื้อ — แยกแยะ​ด้วย id ไม่ใช่​ด้วย​ค่าTactical Design, Value ObjectValue Objectอ็อบเจ็กต์ที่​สำคัญ​ที่ “ค่า” ของ​มัน ไม่มี​ตัวตน​เฉพาะ สอง​ตัว​ที่​ค่า​เท่า​กัน​ถือว่า​เท่า​กัน ควร​เป็น immutable (เปลี่ยน​ค่า​ไม่​ได้) เช่น เงิน, วัน​ที่, พิกัดTactical Design) ส่วน​บาง​ตัว​มี​ไว้​เพื่อ​กัน​เรื่อง​วงจร​ชีวิต​ของ​อ็อบเจ็กต์ (lifecycle) ไม่​ให้​มารบกวน model (เช่น FactoryFactoryตัว​ที่​ห่อ​หุ้ม​ตรรกะ​การ “สร้าง” Aggregate ที่​ซับซ้อน และ​รับประกัน​ว่าอ็อบเจ็กต์ใหม่​ถูกต้อง​ตาม invariant ตั้งแต่​แรก​เกิด — “สร้าง​ของ​ใหม่” (repository หา “ของ​เก่า”)Tactical Design, RepositoryRepositoryตัว​ที่​ทำให้​เรา​เข้าถึง Aggregate ราวกับ​เป็น collection ใน​หน่วย​ความ​จำ ซ่อน​รายละเอียด​ฐาน​ข้อมูล มี1 repository ต่อ1 Aggregate RootTactical Design) พูด​อีก​แบบ​คือ building blocks เปรียบ​เหมือน ภาษา​กลาง​ของ​นัก​พัฒนา สำหรับ​ประกอบ model ที่​อุดม​ไป​ด้วย​ตรรกะ (rich domain model)

รูปที่ 14-1: ภาพรวม tactical patterns — ภายใน Bounded Context 1 Client เรียก Application Service ซึ่งใช้ Factory สร้าง อัปเดต model ผ่าน aggregate root และใช้ Repository บันทึก/ดึง aggregate

Application Service ไม่ใช่​ที่​อยู่​ของ​กฎ​ธุรกิจ

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


กลุ่ม​แรก​คือ pattern ที่​ใช้​แทน​นโยบาย​และ​ตรรกะ​ใน problem domain โดยตรง

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

รูปที่ 14-2: Entity — มี Identity, equality นิยามด้วย id, มีแอตทริบิวต์และ method ที่เปลี่ยนสถานะได้ ตัวอย่างคือ Product ที่มี id คงที่แต่ราคาเปลี่ยนได้ผ่าน change_price_to

ลอง​ดู​ตัวอย่าง Product ที่​จำลอง​เป็น entity — id ถูก​ตั้ง​ตอน​สร้าง​และ​ไม่มี method ให้​เปลี่ยน ส่วน​พฤติกรรม​อื่น​ถูก​ห่อ​หุ้ม (encapsulate) ไว้​ใน​ตัว​มัน​เอง

Product.cs
public class Product : Entity<Guid>
{
private Price _sellingPrice;
private Price _retailPrice;
// id ตั้งครั้งเดียวตอนสร้าง — ไม่มี method ให้เปลี่ยน identity
public Product(Guid id, Price sellingPrice, Price retailPrice) : base(id)
{
// invariant: ราคาขายกับราคาตั้งต้องเป็นสกุลเงินเดียวกัน
if (!SellingPriceMatches(retailPrice))
throw new PricesNotInTheSameCurrencyException("…");
_sellingPrice = sellingPrice;
_retailPrice = retailPrice;
}
// พฤติกรรมที่เปลี่ยนสถานะ แต่ยังคุมกฎไว้
public void ChangePriceTo(Price newPrice) { /* ตรวจสกุลเงินก่อนเปลี่ยน */ }
}

เทคนิค​ที่​นิยม​คือ​ให้ entity สืบทอด​จาก base class Entity<TId> ที่ override equality ไว้​ให้​แล้ว — เทียบ​กัน​ด้วย “ชนิด + id” base class นี้​ช่วย​กวาด​เรื่อง​จุกจิก​ซ้ำซาก​อย่าง identity และ equality ออก​ไป เพื่อ​ให้ class entity จริง​โฟกัส​ที่​กฎ​ธุรกิจ​ล้วนๆ

🚢 Cargo คือ Entity ตัวเอก​ของ​ระบบ​ขนส่ง

ใน​ระบบ​ขนส่ง​สินค้า Cargo คือ entity ชัดเจน — มัน​มี Tracking IDTracking IDรหัส​ติดตาม​ที่​ไม่​ซ้ำ​ของ Cargo ใน​ตัวอย่าง​การ​ขนส่ง ใช้​เป็น identity ของ Cargo ตั้งแต่​ตอน​จอง​จน​ส่ง​มอบTactical Design เป็น identity ที่​คงที่​ตั้งแต่​ตอน​จอง​จน​ส่ง​มอบ ระหว่าง​ทาง ItineraryItineraryValue Object ที่​เป็น “แผนการ​เดินทาง​จริง” ของ Cargo ประกอบ​ด้วย​ลำดับ​ของ Leg (ช่วง​การ​ขนส่ง​แต่ละ​ช่วง)Tactical Design (แผน​เดินทาง) และ​สถานะ​ของ​มัน​เปลี่ยน​ได้​หลาย​ครั้ง แต่​มัน​ก็​ยัง “เป็น Cargo ตู้​เดิม” อยู่ดี — นี่​คือ​หัวใจ​ของ​การ​แยกแยะ​ด้วย identity ไม่ใช่​ด้วย​ค่า

Value ObjectValue Objectอ็อบเจ็กต์ที่​สำคัญ​ที่ “ค่า” ของ​มัน ไม่มี​ตัวตน​เฉพาะ สอง​ตัว​ที่​ค่า​เท่า​กัน​ถือว่า​เท่า​กัน ควร​เป็น immutable (เปลี่ยน​ค่า​ไม่​ได้) เช่น เงิน, วัน​ที่, พิกัดTactical Design แทน​สิ่ง​ที่​เรา​รู้จัก​จาก คุณลักษณะ ของ​มัน​เท่านั้น ไม่​ต้องการ identity เพราะ​มัน​มัก​ผูก​อยู่​กับ​อ็อบเจ็กต์อื่น​เสมอ เช่น order entity ใช้ value object แทนที่​อยู่​จัด​ส่ง, รายการ​สินค้า, ข้อมูล​ผู้​ส่ง — “ที่​อยู่​จัด​ส่ง​ที่​ไม่​ผูก​กับ order ใดๆ” ไม่มี​ความหมาย​ใน​ตัว​มัน​เอง

เพราะ​นิยาม​ด้วย​ค่า value object จึง​ควร​เป็น ImmutableImmutableเปลี่ยนแปลง​ไม่​ได้​หลัง​สร้าง ถ้า​จะ “เปลี่ยน​ค่า” ต้อง​สร้าง​อ็อบเจ็กต์ใหม่​แทน หลักการ​สำคัญ​ของ Value Object เพื่อ​เลี่ยง bug จาก​การ​ใช้​อ้างอิง​ร่วม​กันTactical Design — สร้าง​แล้ว​เปลี่ยน​สถานะ​ไม่​ได้ ตัวอย่าง​คลาสสิก​คือ​เงิน: หาก​มี​ใคร​นำ​ธนบัตร​ห้า​ร้อย​มา​แลก​กับ​ธนบัตร​ห้า​ร้อย​ใน​กระเป๋า​ของ​คุณ คุณ​ก็​ยัง​มี “ห้า​ร้อย​บาท” เท่า​เดิม — เรา​สนใจ​แค่​ค่า​ของ​มัน ไม่​สนใจ​ตัวตน (เลข​ซีเรียล​ของ​ธนบัตร)

รูปที่ 14-3: Value Object — equality นิยามด้วยแอตทริบิวต์ทั้งหมด ไม่มี state-changing method ตัวอย่าง Money: method add คืน Money instance ใหม่แทนการแก้ของเดิม

Money.cs
public class Money // immutable value object
{
private readonly decimal _value;
private readonly Currency _currency;
// "บวกเงิน" ไม่แก้ของเดิม แต่คืน instance ใหม่ — closure of operations
public Money Add(Money toAdd)
{
if (!_currency.Equals(toAdd._currency))
throw new NonMatchingCurrencyException("คนละสกุลเงิน บวกกันไม่ได้");
return new Money(_value + toAdd._value, _currency);
}
// override equality ให้เทียบจาก value + currency
}

การ​ที่ method Add คืน instance ใหม่​แทน​การ​แก้​ของ​เดิม เรียก​ว่า closure of operations — รับ​เข้า​และ​คืน​ออก​เป็น​ชนิด​เดียวกัน โดย​ไม่​แตะ​สถานะ​เดิม

🛒 ที่​อยู่​จัด​ส่ง = Value Object ใน Order

ใน Order ของ e-commerce ShippingAddress, Money (ยอด​รวม) และ Quantity ล้วน​เป็น value object — เรา​ไม่​สนใจ​ว่า “ที่​อยู่​นี้​เป็น​อัน​เดียว​กับ​ที่​อยู่​ใน​ออเดอร์​ก่อน​หรือ​ไม่” เรา​สนใจ​เพียง​ว่า​ค่า​ของ​มัน​ถูกต้อง​ครบถ้วน หาก​ลูกค้า​จะ​เปลี่ยน​ที่​อยู่ ก็ แทนที่​ทั้ง​ก้อน ด้วย value object ใหม่ ไม่ใช่​ไป​แก้​ที​ละ field — วิธี​นี้​รักษา InvariantInvariantกฎ​ทาง​ธุรกิจ​ที่ “ต้อง​เป็น​จริง​เสมอ” ภายใน Aggregate เช่น ยอด​รวม​คำ​สั่ง​ซื้อ​ต้อง​เท่ากับ​ผล​รวม​ของ​รายการ​สินค้า — ต้อง​ถูกต้อง​ภายใน transaction เดียวTactical Design ว่าที่​อยู่​ต้อง​สมบูรณ์​เสมอ

Domain ServiceDomain Serviceบริการ​ที่​ถือ “ตรรกะ​ธุรกิจ” ซึ่ง​ไม่​เข้า​กับ Entity หรือ Value Object ตัว​ใด​ตัว​หนึ่ง​โดย​ธรรมชาติ เช่น การ​โอน​เงิน​ที่​เกี่ยวข้อง​กับ​สอง​บัญชี — ไร้​สถานะ (stateless)Tactical Design ห่อ​หุ้ม​ตรรกะ​ของ domain ที่ ไม่​เข้า​กับ entity หรือ value object ตัว​ใด​ตัว​หนึ่ง​โดย​ธรรมชาติ มัน​ไม่มี identity และ​ไม่มี state หน้าที่​คือ​ประสาน​งาน​ตรรกะ​ธุรกิจ​โดย​ใช้ entity และ value object ตัวอย่าง​ที่​ดี​คือ “ตัว​คำนวณ​ค่า​ขนส่ง” (ShippingCostCalculator) ที่​รับ​ชุด​ของ consignment และ​ตาราง​น้ำหนัก แล้ว​คืน​ค่า​ขนส่ง​ออก​มา — ตรรกะ​นี้​ไม่​เข้า​กับ​อ็อบเจ็กต์ตัว​ใด​ตัว​หนึ่ง​โดยตรง จึง​แทน​ด้วย domain service จะ​เหมาะ​กว่า

ตั้ง​ชื่อ = ทำให้​แนวคิด​เป็น ‘ของ​จริง’

การ​จัด​ตรรกะ​ใส่ domain service แล้ว​ตั้ง​ชื่อ​ให้​มัน ทำให้​เรา คุย​กับ domain expert ได้​อย่าง​เจาะจง ว่า​กำลัง​พูด​ถึง​นโยบาย​หรือ​กระบวนการ​ชิ้น​ใด แนวคิด​ที่​เคย​อยู่​ใน​ใจ​ของ​ธุรกิจ​แบบ​ไม่​ชัด​แจ้ง (implicit) จะ​กลาย​เป็น​ชัด​แจ้ง (explicit) และ​ควร​ถูก​เพิ่ม​เข้า Ubiquitous Language ทันที จุด​ต่าง​สำคัญ​คือ Domain Service เป็น​ที่​อยู่​ของ​กฎ​ธุรกิจ ส่วน Application Service ทำ​หน้าที่​ประสาน​งาน​เท่านั้น

ModuleModuleการ​จัด​กลุ่ม​แนวคิด​ที่​เกี่ยวข้อง​กัน​ไว้​ด้วย​กัน (เช่น package) ตั้ง​ชื่อ​ให้​เป็น​ส่วน​หนึ่ง​ของ Ubiquitous Language จัด​ตาม​ธุรกิจ ไม่ใช่​ตาม​ชนิด​ทาง​เทคนิคTactical Design (ใน C# คือ namespace หรือ project) ใช้​จัด​กลุ่ม​แนวคิด​ที่​เกี่ยวข้อง​กัน เพื่อ​ให้​เข้าใจ model ขนาด​ใหญ่​ได้​ง่าย​ขึ้น ชื่อ module ควร​ยก​มา​จาก Ubiquitous Language โดยตรง ไม่ใช่​จัด​ตาม​ชนิด​ทาง​เทคนิค (อย่า​ทำ folder Entities/ หรือ Services/) Module ช่วย​ลด coupling และ​เพิ่ม cohesion ทำให้​ผู้​อ่าน​เข้าใจ design ได้​อย่าง​รวดเร็ว​ก่อน​จะ​มุด​ลง​ไป​อ่าน file class

อย่า​สับสน Module กับ Subdomain หรือ Bounded Context

ทั้ง​สาม​อย่าง​นี้ “แบ่ง” อะไร​บาง​อย่าง แต่​คนละ​ระดับ: SubdomainSubdomainส่วน​ย่อย​ของ Domain ใหญ่ เช่น Domain e-commerce แตก​เป็น subdomain การ​สั่ง​ซื้อ การ​ชำระ​เงิน คลัง​สินค้า ฯลฯ อยู่​ใน “พื้นที่​ปัญหา” (problem space)Strategic Design แบ่ง domain (problem space), Bounded ContextBounded Contextขอบเขต​ที่​ชัดเจน (มัก​เป็น​ระบบ​ย่อย​หรือ​ทีม​หนึ่ง) ที่ model หนึ่ง​ใช้ได้​และ​มี​ความหมาย​แน่นอน ภายใน​ขอบเขต​นี้ Ubiquitous Language สอดคล้อง​กัน 100%Strategic Design กำหนด ขอบเขต​ที่ model ใช้ได้ ส่วน Module แค่​จัด​ระเบียบ ภายใน model เดียว เท่านั้น มัน​ทำงาน​ใน​ระดับ abstraction ที่​สูง​กว่า aggregate และ entity


กลุ่ม​ที่​สอง​ดูแล “การ​สร้าง” และ “การ​คง​อยู่” (persistence) ของ​อ็อบเจ็กต์ใน model

เมื่อ entity และ value object โยง​กัน​จน​กลาย​เป็น object graph ขนาด​ใหญ่ การ​พยายาม​มอง​ทั้ง​กอง​เป็น “หนึ่ง​หน่วย” จะ​ทั้ง​ยาก​และ​กระทบ performance ลอง​นึกถึง graph ที่ Customer, Order, Product, Address พัน​กัน​ไป​หมด — เรา​คง​ไม่​อยาก​ให้การ​ที่​ลูกค้า​แก้​ที่​อยู่​ต้อง​ไป block การ​เปลี่ยน​สถานะ​ของ order เก่า ทั้ง​สอง​เรื่อง​นี้​ไม่​เกี่ยวข้อง​กัน​และ​ไม่​จำเป็น​ต้อง​ใช้​ขอบเขต consistency ร่วม​กัน

AggregateAggregateกลุ่ม​ของ Entity และ Value Object ที่​ถูก​มอง​เป็น​หนึ่ง​หน่วย​เดียว​เพื่อ​รักษา​ความ​ถูกต้อง​ของ​ข้อมูล มี​ขอบเขต​ชัดเจน และ​เป็น​หน่วย​ของ transactionTactical Design คือ​คำ​ตอบ — เรา​ตัด graph ใหญ่​ออก​เป็น​ก้อน​เล็กๆ โดย​ยึด InvariantInvariantกฎ​ทาง​ธุรกิจ​ที่ “ต้อง​เป็น​จริง​เสมอ” ภายใน Aggregate เช่น ยอด​รวม​คำ​สั่ง​ซื้อ​ต้อง​เท่ากับ​ผล​รวม​ของ​รายการ​สินค้า — ต้อง​ถูกต้อง​ภายใน transaction เดียวTactical Design เป็น​เส้น​แบ่ง แต่ละ​ก้อน​ถูก​มอง​เป็น​หนึ่ง​หน่วย​เชิง​แนวคิด​และ​เป็น​ขอบเขต​ของ transaction

รูปที่ 14-6: Object graph ใหญ่ที่ถูกตัดเป็น Customer Aggregate, Order Aggregate และ Product Aggregate ตามเส้นประ

อย่า​แบ่ง Aggregate ตาม ‘ความ​เกี่ยวข้อง’ — แบ่ง​ตาม Invariant

เมื่อ​แรก​เห็น การ​จับ Customer + Address + Credit Card รวม​เป็น​ก้อน​เดียว​ดู​สม​เหตุ​สม​ผล แต่​ถ้า​การ​แก้​ที่​อยู่​กับ​การ​แก้​ข้อมูล​ส่วนตัว​เกิด​ขึ้น​พร้อม​กัน เรา​จะ​เจอ​ปัญหา blocking ทั้ง​ที่​สอง​เรื่อง​นี้ ไม่​ได้​ใช้ invariant ร่วม​กัน ทาง​ที่​ถูก​คือ​แยก​เป็น Personal Details Aggregate, Wallet Aggregate และ Address Book Aggregate แล้ว​จัด​ทั้งหมด​ไว้​ใต้ Customer Module — ขอบเขต​ของ aggregate มา​จาก​กฎ ไม่ใช่​มา​จาก​ผัง​ความ​สัมพันธ์

ความ​สัมพันธ์​ระหว่าง aggregate ควร อ้างอิง​ด้วย id ของ​อีก aggregate root เท่านั้น ไม่ใช่​ถือ​อ็อบเจ็กต์ทั้ง​ก้อน หลัก​นี้​รักษา​เส้น​แบ่ง​และ​หลีก​เลี่ยง​การ​โหลด object graph มหึมา​ที่​ไม่​ได้​ใช้

Aggregate RootAggregate RootEntity หนึ่ง​ตัว​ที่​เป็น “ประตู​เดียว” เข้า​สู่ Aggregate การ​อ้างอิง​จาก​ภายนอก​ทำได้​กับ root เท่านั้น root จึง​ควบคุม​กฎ​ความ​ถูกต้อง​ของ​ทั้ง​กลุ่มTactical Design คือ entity หนึ่ง​ตัว​ที่​ทำ​หน้าที่​เป็น “ประตู​เดียว” เข้า​สู่ aggregate ไม่มี entity หรือ value object นอก aggregate ที่​ถือ reference ไป​ยัง​อ็อบเจ็กต์ภายใน​ได้ — ภายนอก​อ้าง​ถึง​ได้​แค่ root เท่านั้น และ​ทุก​การ​เปลี่ยนแปลง​ต้อง​ผ่าน root เพื่อ​ให้ aggregate อยู่​ใน​สถานะ​ถูกต้อง​เสมอ มอง​มัน​เป็น atomic unit

รูปที่ 14-9: Aggregate Root เป็นจุดเข้าสู่ aggregate — อ็อบเจ็กต์ภายนอกถือ reference เข้าไปข้างในไม่ได้ root encapsulate ข้อมูลและเปิดเฉพาะพฤติกรรมที่เปลี่ยนแปลงได้

🚢 Cargo เป็น Aggregate Root, Handling Event แยก​ก้อน

Cargo คือ aggregate root — เข้าถึง Itinerary หรือ delivery status ได้​ผ่าน Cargo เท่านั้น แต่ Handling EventHandling Eventการ​บันทึก​การ​จัดการ​สินค้า​จริง (LOAD, UNLOAD, RECEIVE, CLAIM) เป็น Aggregate “แยก​ต่างหาก” จาก Cargo เพราะ​มี​ปริมาณ​มาก​และ​ต้อง​ประมวล​ผล​แบบ asyncTactical Design (บันทึก LOAD/UNLOAD/RECEIVE/CLAIM) ถูก​แยก​เป็น aggregate คนละ​ก้อน เพราะ​มัน​เกิด​ถี่​และ​มี​ปริมาณ​มหาศาล หาก​ฝัง​ไว้​ใน Cargo จะ​กลาย​เป็น object graph ที่​ใหญ่​เกิน​กว่า​จะ​โหลด​ทั้ง​กอง​ได้​ไหว — 2 aggregate จึง​สื่อสาร​กัน​ผ่าน id และ Eventual ConsistencyEventual Consistencyความ​ถูกต้อง​ที่ “ตาม​มา​ทีหลัง” ไม่ใช่​ทันที ใช้​กับ​การ​อัปเดต​ข้าม Aggregate ผ่าน Domain Event แทน​การ​บังคับ​ให้​ตรง​กัน​ใน​ทันที​ภายใน transaction เดียวTactical Design

ถ้า​การ​สร้าง entity หรือ value object ซับซ้อน​มาก​พอ ควร​มอบ​งาน​ให้ FactoryFactoryตัว​ที่​ห่อ​หุ้ม​ตรรกะ​การ “สร้าง” Aggregate ที่​ซับซ้อน และ​รับประกัน​ว่าอ็อบเจ็กต์ใหม่​ถูกต้อง​ตาม invariant ตั้งแต่​แรก​เกิด — “สร้าง​ของ​ใหม่” (repository หา “ของ​เก่า”)Tactical Design — มัน​รับประกัน​ว่า ทุก invariant ถูกต้อง​ก่อน ที่อ็อบเจ็กต์จะ​ถือ​กำเนิด ถ้าอ็อบเจ็กต์เรียบ​ง่าย​และ​ไม่มี​กฎ​พิเศษ ใช้ constructor ธรรมดา​ก็​เพียงพอ ไม่​จำเป็น​ต้อง​ใช้ factory เรา​ใช้ factory ได้​ทั้ง​ตอน​สร้าง​ของ​ใหม่ และ​ตอน​ประกอบ​อ็อบเจ็กต์ขึ้น​ใหม่ (re-create) จาก persistent storage

RepositoryRepositoryตัว​ที่​ทำให้​เรา​เข้าถึง Aggregate ราวกับ​เป็น collection ใน​หน่วย​ความ​จำ ซ่อน​รายละเอียด​ฐาน​ข้อมูล มี1 repository ต่อ1 Aggregate RootTactical Design ทำให้​เรา​เข้าถึง aggregate ราวกับ​เป็น collection ใน​หน่วย​ความ​จำ โดย​ซ่อน​รายละเอียด​ของ persistence store ทำให้​สร้าง model ได้​โดย​ไม่​ต้อง​คิด​เรื่อง infrastructure เพราะ aggregate เป็น atomic unit เรา​จึง บันทึก​บาง​ส่วน​ของ aggregate ไม่​ได้ — ต้อง​บันทึก​ทั้ง​ก้อน

อย่า​หมกมุ่น​กับ​การ abstract repository

นัก​พัฒนา​หลาย​คน​ติด​กับดัก​ทำให้ repository เป็น​นามธรรม​เกิน​จำเป็น — Millett & Tune แนะนำ​ให้​มอง​มัน​อย่าง​เรียบ​ง่าย​ว่า​เป็น​เพียง​กลไก persist/rehydrate และ​เป็น​เรื่อง​ของ infrastructure ดังนั้น​การ​อาศัย ORM (เช่น NHibernate, Entity Framework, RavenDB) มา​ทำ​หน้าที่ repository จึง​คุ้ม​ค่า​กว่า และ อย่า​ใช้ repository ทำ​รายงาน — การ query ตรง​เข้า data store มี​ประสิทธิภาพ​กว่า​สำหรับ​งาน reporting (แนวคิด​นี้​นำ​ไป​สู่ CQRSCQRSCommand Query Responsibility Segregation — แยก model สำหรับ “เขียน/แก้ไข” (command) ออก​จาก model สำหรับ “อ่าน” (query) เหมาะ​กับ domain ซับซ้อน แต่​เพิ่ม​ความ​ซับซ้อน ใช้​อย่าง​ระมัดระวังArchitecture)


หลัง​ตำรา​ดั้งเดิม​ของ Evans มี2 pattern ที่​กลาย​เป็น​เครื่องมือ​สำคัญ​ใน​การ​สร้าง model

Domain EventDomain Eventสิ่ง​ที่ “เกิด​ขึ้น​แล้ว” ใน domain ซึ่ง​ส่วน​อื่น​สนใจ แทน​ด้วย​อ็อบเจ็กต์ที่​เปลี่ยนแปลง​ไม่​ได้ ตั้ง​ชื่อ​เป็น​อดีต เช่น CargoWasRouted ใช้​สื่อสาร​ข้าม AggregateTactical Design บ่ง​บอกว่า​มี​บาง​อย่าง “เกิด​ขึ้น​แล้ว” ใน problem domain ที่​ธุรกิจ​สนใจ ใช้​บันทึก​การ​เปลี่ยนแปลง​แบบ audit หรือ​ใช้​เป็น​ช่อง​ทาง​สื่อสาร ข้าม aggregate ได้ — บ่อย​ครั้ง​การกระทำ​บน aggregate root หนึ่ง​มี side effect นอก​ขอบเขต​ของ​มัน aggregate อื่น​จึง “ฟัง” event แล้ว​ทำงาน​ตาม​ได้

รูปที่ 14-12: เมื่อลูกค้าเพิ่มสินค้าลง Basket Aggregate มันจะ publish ProductAddedEvent หลังบันทึกเสร็จ จากนั้น Recommendations Aggregate subscribe event นี้แล้วอัปเดตคำแนะนำ

Basket.cs
public void Add(Product product)
{
_items.Add(BasketItemFactory.CreateItemFor(product, this));
// ประกาศว่า "มีสินค้าถูกเพิ่มแล้ว" — context อื่นจะมา subscribe เอง
DomainEvents.Raise(new ProductAddedToBasket(_id, _items.IdsOfItems()));
}

ข้อดี​คือ Basket context ไม่​ต้อง​ผูก​ตรง กับ Recommendation context — มัน​เพียง​ป่าว​ประกาศ event ออก​ไป แล้ว​ใคร​สนใจ​ก็​มา subscribe เอง รูปแบบ​นี้​คล้าย publish-subscribe และ​โฟกัส​ที่ “เมื่อไร” (the when) ของ​เหตุการณ์

🛒 ProductAddedToBasket ขับ​เคลื่อน​คำ​แนะนำ​สินค้า

ทุก​ครั้ง​ที่​ลูกค้า​หยิบ​ของ​ลง​ตะกร้า ระบบ​ต้อง​อัปเดต “สินค้า​แนะนำ” — แทนที่​จะ​ให้ Basket รู้จัก Recommendation โดยตรง Basket แค่ raise ProductAddedToBasket แล้ว recommendation context ก็ handle เอง ทำให้2 bounded context หลวมตัว​ต่อ​กัน (loosely coupled) และ​วิวัฒน์​แยก​กัน​ได้

Event SourcingEvent Sourcingเก็บ​ทุก​การ​เปลี่ยน​สถานะ​เป็น event ที่​เปลี่ยน​ไม่​ได้ แล้ว​สร้าง​สถานะ​ปัจจุบัน​ด้วย​การ​เล่น event ซ้ำ มัก “ต้อง​ใช้​คู่​กับ CQRS” (Greg Young) มี event stream ต่อ1 AggregateArchitecture เป็น​ทาง​เลือก​แทน​การ​เก็บ​แค่ “สถานะ​ปัจจุบัน” (snapshot) — แทนที่​จะ​เก็บ state ของ entity ลง​ฐาน​ข้อมูล เรา​เก็บ ลำดับ​ของ event ทั้งหมด ที่​นำ​มา​สู่​สถานะ​นั้น แล้ว​สร้าง​สถานะ​ปัจจุบัน​ด้วย​การ “เล่น event ซ้ำ”

ข้อดี​คือ​ธุรกิจ​สามารถ​ถาม​คำถาม​ที่ snapshot ตอบ​ไม่​ได้ เช่น “สถานะ​เมื่อ​เดือน​ที่​แล้ว​เป็น​อย่างไร” (temporal query) ตัวแทน​ท่องเที่ยว​ออนไลน์​อาจ​อยาก​รู้​ว่า​เหตุ​ใด​ยอด​จอง​จึง​ดิ่ง​ลง​ใน​เดือน​หนึ่ง — ด้วย event sourcing พวก​เขา​สามารถ rebuild สถานะ​ของ catalog ย้อนหลัง แล้ว re-run การ​ค้นหา​ที่​ผู้​ใช้​เคย​ทำได้

Event Sourcing เปลี่ยน​วิธี​คิด​เรื่อง​การ​เก็บ​ข้อมูล

ใน​ฐานะ DDD practitioner หน้าที่​ของ​คุณ​ไม่ใช่​แค่​เสนอ​ความ​เป็น​ไป​ได้​ของ event sourcing ให้​ธุรกิจ แต่​ต้อง​เรียนรู้​วิธี model ใหม่ ด้วย — เลิก​คิด​แบบ​ทุ่มเท​สถานะ​ลง ORM (dump state) แล้ว​หัน​มา capture และ persist domain event แทน Greg Young ถึง​กับ​กล่าว​ว่า event sourcing มัก “ต้อง​ใช้​คู่​กับ CQRS” รายละเอียด​ทั้งหมด​อยู่​ใน​บท​เรื่อง Event Sourcing ต่อ​ไป


graph TD
  subgraph จำลอง domain
    E[Entity]
    V[Value Object]
    DS[Domain Service]
    M[Module]
  end
  subgraph Lifecycle
    AG[Aggregate]
    AR[Aggregate Root]
    F[Factory]
    R[Repository]
  end
  subgraph Emerging
    DE[Domain Event]
    ES[Event Sourcing]
  end
  AG --> AR
  AR --> E
  AR --> V
  F -.สร้าง.-> AG
  R -.persist/hydrate.-> AG
  AR -.raise.-> DE
  DE -.ฐานเก็บ.-> ES
หัวใจ​ของ​บท​นี้

tactical patterns ของ DDD คือ building blocks ที่ Evans จัด​กลุ่ม​จาก​แนวทาง OO ของ Fowler แบ่ง​เป็น​สาม​กลุ่ม: (1) จำลอง domain — Entity (นิยาม​ด้วย identity), Value Object (นิยาม​ด้วย​ค่า และ​เป็น immutable), Domain Service (ตรรกะ​ที่​ไม่​เข้า​กับ​อ็อบเจ็กต์ตัว​ใด​ตัว​หนึ่ง), Module (จัด​ระเบียบ); (2) lifecycle — Aggregate (ขอบเขต consistency ที่​แบ่ง​ตาม invariant), Factory (แยก​การ​สร้าง), Repository (persist/hydrate); (3) emerging — Domain Event และ Event Sourcing แต่​จำ​ไว้​เสมอ​ว่า​ทั้งหมด​นี้​เป็น​เพียง ไกด์​ไลน์ ไม่ใช่​กฎ​ตายตัว — สิ่ง​ที่​สำคัญ​ที่สุด​คือ​การ​สะท้อน Ubiquitous Language ลงใน code


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

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

  • Entity — นิยาม​ด้วย identity ที่​คงที่​ตลอด​อายุ ตรง​กับ​ตัวอย่าง Product และ Cargo ใน​บท​นี้
  • Value Object — นิยาม​ด้วย​ค่า​และ​ควร immutable ตรง​กับ​ตัวอย่าง Money และ ShippingAddress
  • Aggregate — ขอบเขต consistency ที่​แบ่ง​ตาม invariant ไม่ใช่​ตาม​ความ​เกี่ยวข้อง ตาม​ที่​บท​นี้​อธิบาย​ไว้
  • Repository — กลไก​เข้าถึง​และ persist/hydrate aggregate ราวกับ​เป็น collection ใน​หน่วย​ความ​จำ
  • Domain Events — วิธี​สื่อสาร​ข้าม aggregate/context แบบ loosely coupled ตาม​ตัวอย่าง ProductAddedToBasket

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

ข้อ 1 / 4

อะไรคือความต่างหลักระหว่าง Entity กับ Value Object?