รู้จัก 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 ที่ทรงพลังได้
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
หัวข้อที่มีชื่อว่า “บทบาทของ tactical patterns”หน้าที่ของ 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)

สังเกตในรูปว่า 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
กลุ่มที่ 1 — patterns สำหรับ “จำลอง” domain
หัวข้อที่มีชื่อว่า “กลุ่มที่ 1 — patterns สำหรับ “จำลอง” domain”กลุ่มแรกคือ pattern ที่ใช้แทนนโยบายและตรรกะใน problem domain โดยตรง
Entity — สิ่งที่นิยามด้วย “ตัวตน”
หัวข้อที่มีชื่อว่า “Entity — สิ่งที่นิยามด้วย “ตัวตน””EntityEntityอ็อบเจ็กต์ที่มี “ตัวตน” (identity) ต่อเนื่องตามเวลา แม้ค่าภายในเปลี่ยน ก็ยังเป็นสิ่งเดิม เช่น ลูกค้า, คำสั่งซื้อ — แยกแยะด้วย id ไม่ใช่ด้วยค่าTactical Design แทนแนวคิดที่ถูกนิยามด้วย identity (ตัวตน) ไม่ใช่ด้วยค่าของแอตทริบิวต์ ตัวตนของมันคงที่ตลอดอายุ แต่แอตทริบิวต์เปลี่ยนได้ Entity จึงรับผิดชอบนิยามว่า “การเป็นสิ่งเดียวกันหมายความว่าอย่างไร” ใน code มักทำด้วยการ override การเปรียบเทียบความเท่ากัน (equality) ให้เทียบจาก id ไม่ใช่จากค่า

ลองดูตัวอย่าง Product ที่จำลองเป็น entity — id ถูกตั้งตอนสร้างและไม่มี method ให้เปลี่ยน ส่วนพฤติกรรมอื่นถูกห่อหุ้ม (encapsulate) ไว้ในตัวมันเอง
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 ชัดเจน — มันมี Tracking IDTracking IDรหัสติดตามที่ไม่ซ้ำของ Cargo ในตัวอย่างการขนส่ง ใช้เป็น identity ของ Cargo ตั้งแต่ตอนจองจนส่งมอบTactical Design เป็น identity ที่คงที่ตั้งแต่ตอนจองจนส่งมอบ ระหว่างทาง ItineraryItineraryValue Object ที่เป็น “แผนการเดินทางจริง” ของ Cargo ประกอบด้วยลำดับของ Leg (ช่วงการขนส่งแต่ละช่วง)Tactical Design (แผนเดินทาง) และสถานะของมันเปลี่ยนได้หลายครั้ง แต่มันก็ยัง “เป็น Cargo ตู้เดิม” อยู่ดี — นี่คือหัวใจของการแยกแยะด้วย identity ไม่ใช่ด้วยค่า
Value Object — สิ่งที่นิยามด้วย “ค่า”
หัวข้อที่มีชื่อว่า “Value Object — สิ่งที่นิยามด้วย “ค่า””Value ObjectValue Objectอ็อบเจ็กต์ที่สำคัญที่ “ค่า” ของมัน ไม่มีตัวตนเฉพาะ สองตัวที่ค่าเท่ากันถือว่าเท่ากัน ควรเป็น immutable (เปลี่ยนค่าไม่ได้) เช่น เงิน, วันที่, พิกัดTactical Design แทนสิ่งที่เรารู้จักจาก คุณลักษณะ ของมันเท่านั้น ไม่ต้องการ identity เพราะมันมักผูกอยู่กับอ็อบเจ็กต์อื่นเสมอ เช่น order entity ใช้ value object แทนที่อยู่จัดส่ง, รายการสินค้า, ข้อมูลผู้ส่ง — “ที่อยู่จัดส่งที่ไม่ผูกกับ order ใดๆ” ไม่มีความหมายในตัวมันเอง
เพราะนิยามด้วยค่า value object จึงควรเป็น ImmutableImmutableเปลี่ยนแปลงไม่ได้หลังสร้าง ถ้าจะ “เปลี่ยนค่า” ต้องสร้างอ็อบเจ็กต์ใหม่แทน หลักการสำคัญของ Value Object เพื่อเลี่ยง bug จากการใช้อ้างอิงร่วมกันTactical Design — สร้างแล้วเปลี่ยนสถานะไม่ได้ ตัวอย่างคลาสสิกคือเงิน: หากมีใครนำธนบัตรห้าร้อยมาแลกกับธนบัตรห้าร้อยในกระเป๋าของคุณ คุณก็ยังมี “ห้าร้อยบาท” เท่าเดิม — เราสนใจแค่ค่าของมัน ไม่สนใจตัวตน (เลขซีเรียลของธนบัตร)

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 — รับเข้าและคืนออกเป็นชนิดเดียวกัน โดยไม่แตะสถานะเดิม
ใน Order ของ e-commerce ShippingAddress, Money (ยอดรวม) และ Quantity ล้วนเป็น value object — เราไม่สนใจว่า “ที่อยู่นี้เป็นอันเดียวกับที่อยู่ในออเดอร์ก่อนหรือไม่” เราสนใจเพียงว่าค่าของมันถูกต้องครบถ้วน หากลูกค้าจะเปลี่ยนที่อยู่ ก็ แทนที่ทั้งก้อน ด้วย value object ใหม่ ไม่ใช่ไปแก้ทีละ field — วิธีนี้รักษา InvariantInvariantกฎทางธุรกิจที่ “ต้องเป็นจริงเสมอ” ภายใน Aggregate เช่น ยอดรวมคำสั่งซื้อต้องเท่ากับผลรวมของรายการสินค้า — ต้องถูกต้องภายใน transaction เดียวTactical Design ว่าที่อยู่ต้องสมบูรณ์เสมอ
Domain Service — ตรรกะที่ไม่เข้ากับใคร
หัวข้อที่มีชื่อว่า “Domain Service — ตรรกะที่ไม่เข้ากับใคร”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 ทำหน้าที่ประสานงานเท่านั้น
Module — จัดระเบียบ model
หัวข้อที่มีชื่อว่า “Module — จัดระเบียบ model”ModuleModuleการจัดกลุ่มแนวคิดที่เกี่ยวข้องกันไว้ด้วยกัน (เช่น package) ตั้งชื่อให้เป็นส่วนหนึ่งของ Ubiquitous Language จัดตามธุรกิจ ไม่ใช่ตามชนิดทางเทคนิคTactical Design (ใน C# คือ namespace หรือ project) ใช้จัดกลุ่มแนวคิดที่เกี่ยวข้องกัน เพื่อให้เข้าใจ model ขนาดใหญ่ได้ง่ายขึ้น ชื่อ module ควรยกมาจาก Ubiquitous Language โดยตรง ไม่ใช่จัดตามชนิดทางเทคนิค (อย่าทำ folder Entities/ หรือ Services/) Module ช่วยลด coupling และเพิ่ม cohesion ทำให้ผู้อ่านเข้าใจ design ได้อย่างรวดเร็วก่อนจะมุดลงไปอ่าน file class
ทั้งสามอย่างนี้ “แบ่ง” อะไรบางอย่าง แต่คนละระดับ: 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
กลุ่มที่ 2 — Lifecycle patterns
หัวข้อที่มีชื่อว่า “กลุ่มที่ 2 — Lifecycle patterns”กลุ่มที่สองดูแล “การสร้าง” และ “การคงอยู่” (persistence) ของอ็อบเจ็กต์ใน model
Aggregate — ก้อนที่รักษาความถูกต้อง
หัวข้อที่มีชื่อว่า “Aggregate — ก้อนที่รักษาความถูกต้อง”เมื่อ 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

เมื่อแรกเห็น การจับ Customer + Address + Credit Card รวมเป็นก้อนเดียวดูสมเหตุสมผล แต่ถ้าการแก้ที่อยู่กับการแก้ข้อมูลส่วนตัวเกิดขึ้นพร้อมกัน เราจะเจอปัญหา blocking ทั้งที่สองเรื่องนี้ ไม่ได้ใช้ invariant ร่วมกัน ทางที่ถูกคือแยกเป็น Personal Details Aggregate, Wallet Aggregate และ Address Book Aggregate แล้วจัดทั้งหมดไว้ใต้ Customer Module — ขอบเขตของ aggregate มาจากกฎ ไม่ใช่มาจากผังความสัมพันธ์
ความสัมพันธ์ระหว่าง aggregate ควร อ้างอิงด้วย id ของอีก aggregate root เท่านั้น ไม่ใช่ถืออ็อบเจ็กต์ทั้งก้อน หลักนี้รักษาเส้นแบ่งและหลีกเลี่ยงการโหลด object graph มหึมาที่ไม่ได้ใช้
Aggregate Root — ประตูเดียวเข้าก้อน
หัวข้อที่มีชื่อว่า “Aggregate Root — ประตูเดียวเข้าก้อน”Aggregate RootAggregate RootEntity หนึ่งตัวที่เป็น “ประตูเดียว” เข้าสู่ Aggregate การอ้างอิงจากภายนอกทำได้กับ root เท่านั้น root จึงควบคุมกฎความถูกต้องของทั้งกลุ่มTactical Design คือ entity หนึ่งตัวที่ทำหน้าที่เป็น “ประตูเดียว” เข้าสู่ aggregate ไม่มี entity หรือ value object นอก aggregate ที่ถือ reference ไปยังอ็อบเจ็กต์ภายในได้ — ภายนอกอ้างถึงได้แค่ root เท่านั้น และทุกการเปลี่ยนแปลงต้องผ่าน root เพื่อให้ aggregate อยู่ในสถานะถูกต้องเสมอ มองมันเป็น atomic unit

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
Factory — แยก “การสร้าง” ออกจาก “การใช้”
หัวข้อที่มีชื่อว่า “Factory — แยก “การสร้าง” ออกจาก “การใช้””ถ้าการสร้าง entity หรือ value object ซับซ้อนมากพอ ควรมอบงานให้ FactoryFactoryตัวที่ห่อหุ้มตรรกะการ “สร้าง” Aggregate ที่ซับซ้อน และรับประกันว่าอ็อบเจ็กต์ใหม่ถูกต้องตาม invariant ตั้งแต่แรกเกิด — “สร้างของใหม่” (repository หา “ของเก่า”)Tactical Design — มันรับประกันว่า ทุก invariant ถูกต้องก่อน ที่อ็อบเจ็กต์จะถือกำเนิด ถ้าอ็อบเจ็กต์เรียบง่ายและไม่มีกฎพิเศษ ใช้ constructor ธรรมดาก็เพียงพอ ไม่จำเป็นต้องใช้ factory เราใช้ factory ได้ทั้งตอนสร้างของใหม่ และตอนประกอบอ็อบเจ็กต์ขึ้นใหม่ (re-create) จาก persistent storage
Repository — คลังเก็บ Aggregate
หัวข้อที่มีชื่อว่า “Repository — คลังเก็บ Aggregate”RepositoryRepositoryตัวที่ทำให้เราเข้าถึง Aggregate ราวกับเป็น collection ในหน่วยความจำ ซ่อนรายละเอียดฐานข้อมูล มี1 repository ต่อ1 Aggregate RootTactical Design ทำให้เราเข้าถึง aggregate ราวกับเป็น collection ในหน่วยความจำ โดยซ่อนรายละเอียดของ persistence store ทำให้สร้าง model ได้โดยไม่ต้องคิดเรื่อง infrastructure เพราะ aggregate เป็น atomic unit เราจึง บันทึกบางส่วนของ aggregate ไม่ได้ — ต้องบันทึกทั้งก้อน
นักพัฒนาหลายคนติดกับดักทำให้ 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)
Emerging patterns — pattern ที่โผล่มาทีหลัง
หัวข้อที่มีชื่อว่า “Emerging patterns — pattern ที่โผล่มาทีหลัง”หลังตำราดั้งเดิมของ Evans มี2 pattern ที่กลายเป็นเครื่องมือสำคัญในการสร้าง model
Domain Event — บอกว่า “เกิดอะไรขึ้นแล้ว”
หัวข้อที่มีชื่อว่า “Domain Event — บอกว่า “เกิดอะไรขึ้นแล้ว””Domain EventDomain Eventสิ่งที่ “เกิดขึ้นแล้ว” ใน domain ซึ่งส่วนอื่นสนใจ แทนด้วยอ็อบเจ็กต์ที่เปลี่ยนแปลงไม่ได้ ตั้งชื่อเป็นอดีต เช่น CargoWasRouted ใช้สื่อสารข้าม AggregateTactical Design บ่งบอกว่ามีบางอย่าง “เกิดขึ้นแล้ว” ใน problem domain ที่ธุรกิจสนใจ ใช้บันทึกการเปลี่ยนแปลงแบบ audit หรือใช้เป็นช่องทางสื่อสาร ข้าม aggregate ได้ — บ่อยครั้งการกระทำบน aggregate root หนึ่งมี side effect นอกขอบเขตของมัน aggregate อื่นจึง “ฟัง” event แล้วทำงานตามได้

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) ของเหตุการณ์
ทุกครั้งที่ลูกค้าหยิบของลงตะกร้า ระบบต้องอัปเดต “สินค้าแนะนำ” — แทนที่จะให้ Basket รู้จัก Recommendation โดยตรง Basket แค่ raise ProductAddedToBasket แล้ว recommendation context ก็ handle เอง ทำให้2 bounded context หลวมตัวต่อกัน (loosely coupled) และวิวัฒน์แยกกันได้
Event Sourcing — เก็บ event ไม่ใช่ snapshot
หัวข้อที่มีชื่อว่า “Event Sourcing — เก็บ event ไม่ใช่ snapshot”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 การค้นหาที่ผู้ใช้เคยทำได้
ในฐานะ 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:
- 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?