Factory & Repository — สร้างของใหม่ กับหาของเก่า
ตลอดเจ็ดบทที่ผ่านมาเราไล่เติมเนื้อให้ Order ทีละชั้น — Value Object, Entity, Aggregate, state machine, Domain Event, Domain Service, Specification — จนตอนนี้มันเป็น domain ที่รักษากฎธุรกิจของตัวเองครบแล้ว คำถามที่เหลืออยู่สองข้อคือคำถามที่สุดโต่งที่สุดในวงจรชีวิตของ object ทุกตัว: “ของใหม่เกิดขึ้นมาได้ยังไงให้ถูกต้องตั้งแต่แรก” และ “ของเก่าที่เคยสร้างไปแล้วจะหาเจอได้ยังไง” สองคำถามนี้คือหน้าที่ของ Factory กับ Repository ตามลำดับ — และเป็นบทปิดท้ายของคอร์สนี้
code เต็มของคอร์สนี้อยู่ที่ repo kaen-food-ordering (กำลังจัดทำ) — บทนี้เกี่ยวข้องกับ path FoodOrdering.Application/Orders/ (IOrderRepository, IMenuCatalog, OrderFactory ตัวใหม่) และ FoodOrdering.Domain/Orders/ (Order.Place ที่เป็น factory method อยู่ในตัวมันเองแล้วตั้งแต่บทที่ 1)
Factory — สร้าง aggregate ที่ซับซ้อนให้ถูกต้องแต่แรกเกิด
หัวข้อที่มีชื่อว่า “Factory — สร้าง aggregate ที่ซับซ้อนให้ถูกต้องแต่แรกเกิด”Eric Evans จัดให้ FactoryFactoryตัวที่ห่อหุ้มตรรกะการ 'สร้าง' Aggregate ที่ซับซ้อน และรับประกันว่าอ็อบเจ็กต์ใหม่ถูกต้องตาม invariant ตั้งแต่แรกเกิด — 'สร้างของใหม่' (repository หา 'ของเก่า')Tactical Design เป็นหนึ่งใน tactical building block ดั้งเดิมของ DDD คู่กับ Entity, Value Object, Aggregate และ Repository — หลักการของมันคือ “แยกการใช้ออกจากการสร้าง” (separate use from construction): เมื่อการสร้าง object ตัวหนึ่งซับซ้อนพอ (ต้องประกอบหลายส่วนเข้าด้วยกัน ต้องพิสูจน์ invariant ตั้งแต่ต้น หรือต้องพึ่งข้อมูลจากที่อื่น) ตรรกะการสร้างนั้นสมควรมีที่อยู่ของตัวเอง แยกออกจากตรรกะการ “ใช้งาน” ตามปกติของ object
จริงๆ แล้วคอร์สนี้มี Factory อยู่แล้วตั้งแต่บทที่ 1 โดยที่เราไม่ได้เรียกมันด้วยชื่อนี้ตรงๆ — ก่อนอื่น recall ชิ้นส่วนย่อยที่ Order ต้องใช้จาก Domain Reference ให้ครบก่อน แล้วค่อย recall Order เต็มๆ:
// recall จาก Domain Reference (คอร์ส Clean Architecture .NET + คอร์สนี้บทที่ 1–2)public sealed record OrderId(Guid Value);public sealed record OrderLineId(Guid Value);public sealed record ProductId(Guid Value);public sealed record Quantity(int Value);public sealed record Money(decimal Amount, string Currency) // guard: Amount<0 → "จำนวนเงินต้องไม่ติดลบ"; Currency!="THB" → "ตอนนี้รองรับเฉพาะสกุลเงิน THB"{ public static Money Thb(decimal amount) => new(amount, "THB"); public static Money operator +(Money a, Money b); // ต่างสกุลเงิน → InvalidOperationException "บวกเงินต่างสกุลกันไม่ได้" public static Money operator *(Money unitPrice, int quantity);}
// recall จากบทที่ 3 — OrderLine ถูกยกระดับเป็น entity เทียบด้วย identity แล้ว ไม่ใช่ record เทียบด้วยค่าแบบบทที่ 1–2public sealed class OrderLine : Entity<OrderLineId> // Entity<TId>.Equals/GetHashCode เทียบด้วย Id ล้วน ๆ ไม่แตะ ProductId/Quantity/UnitPrice เลย{ public ProductId ProductId { get; } public Quantity Quantity { get; } public Money UnitPrice { get; }
public OrderLine(OrderLineId id, ProductId productId, Quantity quantity, Money unitPrice) : base(id) { ProductId = productId; Quantity = quantity; UnitPrice = unitPrice; }}
// recall จากบทที่ 6 — สัญญากลางที่ domain event ทุกตัวต้องเคารพpublic interface IDomainEvent{ DateTimeOffset OccurredOn { get; }}
// recall จากบทที่ 6 — OrderPlaced ผูกกับ IDomainEvent แล้วpublic sealed record OrderPlaced(OrderId OrderId, Money Total, DateTimeOffset OccurredOn) : IDomainEvent;// recall จาก FoodOrdering.Domain/Orders/Order.cs (คอร์ส Clean Architecture .NET บทที่ 2 + คอร์สนี้บทที่ 1 และ _domainEvents ยกระดับเป็น List<IDomainEvent> ในบทที่ 6)public sealed class Order{ private readonly List<OrderLine> _lines = new(); private readonly List<IDomainEvent> _domainEvents = new(); public OrderId Id { get; } public IReadOnlyCollection<OrderLine> Lines => _lines.AsReadOnly(); public Money Total { get; private set; } public IReadOnlyCollection<IDomainEvent> DomainEvents => _domainEvents.AsReadOnly();
private Order(OrderId id, IEnumerable<OrderLine> lines) { Id = id; _lines.AddRange(lines); Total = _lines.Aggregate(Money.Thb(0), (sum, line) => sum + line.UnitPrice * line.Quantity.Value); }
public static Order Place(OrderId id, IReadOnlyList<OrderLine> items) { if (items.Count == 0) throw new InvalidOperationException("ตะกร้าว่างเปล่า สร้างออเดอร์ไม่ได้");
var order = new Order(id, items); order._domainEvents.Add(new OrderPlaced(order.Id, order.Total, DateTimeOffset.UtcNow)); return order; }}Order.Place(...) คือ factory method ในความหมายที่ตรงตัวที่สุด — method static ที่เป็นทางเดียวที่จะได้ Order มา (constructor เป็น private) และรับประกันว่า Order ทุกตัวที่หลุดออกจากมันผ่าน invariant ①(ตะกร้าต้องไม่ว่าง) มาแล้วเสมอ ไม่มีทางสร้าง Order ที่ผิดกฎหลุดออกมาได้เลยแม้แต่ตัวเดียว — DevIQ เรียกรูปแบบคลาสสิกของแนวคิดนี้ว่า Factory Method (ของเดิมมาจาก Gang of Four ตัดสินใจว่าจะสร้าง subclass ไหน ส่วนใน DDD ความหมายกว้างกว่านั้นเล็กน้อย — Factory คือของใดก็ตามที่ห่อหุ้มตรรกะการสร้างเพื่อรักษา invariant ตั้งแต่กำเนิด ไม่จำเป็นต้องมี subclass เกี่ยวข้องเลยก็ได้)
เมื่อการสร้างต้องพึ่งข้อมูลจากข้างนอก — Factory ที่ยกระดับขึ้นไปอีกชั้น
หัวข้อที่มีชื่อว่า “เมื่อการสร้างต้องพึ่งข้อมูลจากข้างนอก — Factory ที่ยกระดับขึ้นไปอีกชั้น”Order.Place(...) ใช้ได้ก็ต่อเมื่อมี IReadOnlyList<OrderLine> พร้อมราคาอยู่แล้ว แต่ในความเป็นจริงจุดเริ่มต้นของออเดอร์คือ ตะกร้า — รายการ (product, quantity) ดิบๆ ที่ยังไม่รู้ราคาต่อหน่วยเลยด้วยซ้ำ ต้องมีใครสักคนไปถามราคาจาก IMenuCatalog ก่อน ค่อยประกอบเป็น OrderLine แล้วส่งต่อให้ Order.Place(...) — recall port ตัวที่ยังขาดจาก Domain Reference ก่อน:
// recall จาก FoodOrdering.Application/Orders/IMenuCatalog.cs (คอร์ส Clean Architecture .NET บทที่ 3)public interface IMenuCatalog{ Task<Money> GetPriceAsync(ProductId productId, CancellationToken ct);}สังเกตว่าตรรกะนี้ มีอยู่แล้ว ในคอร์ส Clean Architecture .NET — มันถูกฝังอยู่กลาง PlaceOrderHandler.Handle(...) (บทที่ 3) ปนอยู่กับงาน “ตัดเงิน” และ “บันทึก” ที่ไม่เกี่ยวกับการสร้างเลย: foreach วนถามราคาผ่าน IMenuCatalog ประกอบเป็น OrderLine แล้วค่อยเรียก Order.Place(...) — สามขั้นตอนนี้คือนิยามของ Factory แบบเป๊ะๆ เพียงแต่ไม่เคยถูกตั้งชื่อและแยกที่อยู่ของตัวเองมาก่อน บทนี้แค่ดึงมันออกมา:
// ใหม่ในบทนี้ — ตะกร้าเป็นแค่ input ดิบ ราคายังไม่ถูกล็อก และ quantity ยังเป็น int ธรรมดาpublic sealed record CartLine(ProductId ProductId, int Quantity);
public sealed class Cart{ private readonly List<CartLine> _lines = new(); public IReadOnlyCollection<CartLine> Lines => _lines.AsReadOnly();
public void Add(ProductId productId, int quantity) => _lines.Add(new CartLine(productId, quantity));}
// FoodOrdering.Application/Orders/OrderFactory.cspublic sealed class OrderFactory{ private readonly IMenuCatalog _menuCatalog;
public OrderFactory(IMenuCatalog menuCatalog) { _menuCatalog = menuCatalog; }
public async Task<Order> CreateFromCartAsync(OrderId id, Cart cart, CancellationToken ct) { var items = new List<OrderLine>(); foreach (var line in cart.Lines) { var unitPrice = await _menuCatalog.GetPriceAsync(line.ProductId, ct); items.Add(new OrderLine(new OrderLineId(Guid.NewGuid()), line.ProductId, new Quantity(line.Quantity), unitPrice)); }
return Order.Place(id, items); // invariant ยังรักษาอยู่ที่ Order.Place เหมือนเดิม — factory ไม่ได้แย่งหน้าที่นี้ไป }}สังเกตตำแหน่ง file: OrderFactory วางอยู่ใน FoodOrdering.Application/Orders/ ไม่ใช่ FoodOrdering.Domain/ — เพราะมันต้องพึ่ง IMenuCatalog ซึ่งประกาศอยู่ที่ FoodOrdering.Application เช่นกัน (recall บทที่ 3) และ Dependency Rule ห้าม FoodOrdering.Domain อ้างอิงอะไรใน FoodOrdering.Application เด็ดขาด — นี่คือบทเรียนสำคัญ: Factory ไม่ได้ต้องอยู่ใน Domain layer เสมอไป ตัวที่ไม่พึ่งอะไรจากข้างนอกเลยอย่าง Order.Place(...) อยู่ใน Domain ได้สบายๆ แต่พอไหนที่ต้องพึ่ง port อย่าง IMenuCatalog (I/O แบบ async) มันก็ต้องขยับขึ้นไปอยู่ชั้นที่มองเห็น port นั้น — ตรงนี้เองที่ Mark Seemann ย้ำเรื่อง constructor injection ไว้ตลอด: OrderFactory รับ IMenuCatalog ผ่าน constructor ตัวเดียว ไม่มี service locator ไม่มี “new” ของแท้ปนอยู่ในตัวมันเอง ทำให้มัน test ได้ง่ายพอๆ กับ PlaceOrderHandler ที่มันจะไปแทนที่ code ข้างในบางส่วน
สร้างใหม่ vs สร้างคืน — creation กับ reconstitution
หัวข้อที่มีชื่อว่า “สร้างใหม่ vs สร้างคืน — creation กับ reconstitution”มีอีกเส้นทางหนึ่งที่ Order ถือกำเนิดขึ้นมาในหน่วยความจำ ซึ่ง ไม่ผ่าน Order.Place(...) เลย นั่นคือตอนที่ระบบโหลดออเดอร์เก่ากลับมาจากฐานข้อมูล — เรียกว่า reconstitution (สร้างคืน) ตรงข้ามกับ creation (สร้างใหม่)
ทำไมถึงห้ามผ่าน Order.Place(...) ตอนโหลดของเก่า? เพราะ Place(...) ทำสองอย่างที่ไม่ควรเกิดซ้ำ: มันเช็ก invariant ① ใหม่ทั้งหมด (ทั้งที่แถวนี้เคยผ่านมาแล้วตอนบันทึกครั้งแรก) และมัน Add เหตุการณ์ OrderPlaced เข้า _domainEvents (ทั้งที่ออเดอร์นี้ถูก “วาง” ไปนานแล้ว ไม่ใช่เพิ่งวางตอนนี้) ถ้า reconstitution เรียก Place(...) ซ้ำ ระบบจะยิง OrderPlaced ปลอมออกไปทุกครั้งที่แค่อ่านออเดอร์เก่ากลับมาดู ซึ่งพังทันที
สิ่งที่ reconstitution ทำจริงๆ คือใช้ private constructor ตัวเดียวกัน (private Order(OrderId id, IEnumerable<OrderLine> lines) ที่ recall ไว้ข้างบน) ผ่านกลไกพิเศษของ ORM — EF Core ใช้ reflection เข้าถึง private constructor และ backing field โดยตรงเพื่อ “ประกอบร่าง” Order ให้ตรงกับแถวในฐานข้อมูล โดยไม่ต้องพิสูจน์ invariant ซ้ำ เพราะแถวนั้นพิสูจน์ตัวเองไปแล้วตอนถูกบันทึกครั้งแรก — creation ต้องพิสูจน์ตัวเองใหม่ทุกครั้ง (born valid) ส่วน reconstitution ไว้ใจข้อมูลที่มาจากแหล่งที่เชื่อถือได้อยู่แล้ว (reborn trusted) กลไกนี้เองคือจุดที่ EF Core adapter (EfOrderRepository — คอร์ส Clean Architecture .NET) ทำงานอยู่เบื้องหลัง คอร์สนี้ไม่ลงรายละเอียด EF mapping ซ้ำ แค่ต้องเข้าใจว่า reconstitution เป็นการบายพาส invariant โดยตั้งใจ ไม่ใช่รูรั่วที่หลุดออกไปโดยไม่มีใครควบคุม
Repository — contract ที่ทำให้ domain ไม่รู้จักฐานข้อมูล
หัวข้อที่มีชื่อว่า “Repository — contract ที่ทำให้ domain ไม่รู้จักฐานข้อมูล”RepositoryRepositoryตัวที่ทำให้เข้าถึง Aggregate ราวกับเป็น collection ในหน่วยความจำ ซ่อนรายละเอียดฐานข้อมูล — interface (contract) อยู่วงใน, implementation อยู่ Infrastructure; 1 repository ต่อ1 Aggregate RootTactical Design ทำให้การเข้าถึง aggregate ที่มีอยู่แล้วรู้สึกเหมือนคุยกับ in-memory collection ทั้งที่ข้างหลังอาจเป็นฐานข้อมูล, cache หรือ HTTP call ก็ได้ — domain ไม่จำเป็นต้องรู้ (และไม่ควรรู้) เลยว่าข้างหลังคืออะไร หลักการนี้เรียกว่า persistence ignorance — recall contract ตัวจริงจาก Course A:
// recall จาก FoodOrdering.Application/Orders/IOrderRepository.cs (คอร์ส Clean Architecture .NET บทที่ 3)public interface IOrderRepository{ Task SaveAsync(Order order, CancellationToken ct); Task<Order?> FindAsync(OrderId id, CancellationToken ct);}interface นี้ประกาศอยู่ใน FoodOrdering.Application — วงใน ตาม Dependency Inversion Principle เดิมที่วางไว้ตั้งแต่คอร์สก่อน — ส่วน adapter ที่ implement มันจริงด้วย EF Core (EfOrderRepository) อยู่ใน FoodOrdering.Infrastructure (วงนอก) คอร์สนี้ ไม่ย้อนไปสอน EF mapping ซ้ำ เพราะนั่นคือของที่ Course A ทำไปแล้ว สิ่งที่บทนี้เพิ่มให้คือทำให้ contract ตัวนี้ สื่อความหมายและใช้ซ้ำได้ดีขึ้น
Vaughn Vernon ใน Implementing Domain-Driven Design (และตัวอย่าง code จริงใน IDDD_Samples) เน้นว่า repository ที่ดีควรมี method หน้าตาแบบ collection — Save/Add, Find/Get — ไม่ใช่ชื่อที่หลุดจากภาษา domain เช่น InsertRow หรือ ExecuteQuery และควรมี หนึ่ง aggregate rootAggregate RootEntity หนึ่งตัวที่เป็น 'ประตูเดียว' เข้าสู่ Aggregate การอ้างอิงจากภายนอกทำได้กับ root เท่านั้น root จึงควบคุมกฎความถูกต้องของทั้งกลุ่มTactical Design ต่อ1 repository เท่านั้น — OrderLine ไม่มี IOrderLineRepository ของตัวเอง เพราะมันไม่ใช่ aggregate root มันเข้าถึงได้ทางเดียวคือผ่าน Order เท่านั้น (ตามที่บทที่ 4 วางกฎไว้ว่า aggregate root เป็นประตูเดียว) ยิ่งมี repository เยอะเท่าไหร่ที่ไม่ตรงกับ aggregate root ยิ่งเป็นสัญญาณว่าเส้นแบ่ง aggregate อาจวางผิดที่
Spec-driven queries — ยืมกฎเดียวกับที่ domain ใช้ไปใช้ query
หัวข้อที่มีชื่อว่า “Spec-driven queries — ยืมกฎเดียวกับที่ domain ใช้ไปใช้ query”ปัญหาของ IOrderRepository ตอนนี้คือมันหา Order ได้แค่ทีละตัวผ่าน OrderId เท่านั้น — ถ้าอยากได้ “ออเดอร์ทั้งหมดที่ยังยกเลิกได้” ล่ะ? ทางที่มักเกิดขึ้นคือเพิ่ม method ใหม่ทีละอันไปเรื่อยๆ (FindCancellableAsync, FindConfirmedAsync, …) แล้วเขียนเงื่อนไข Status ซ้ำทุกที่ ทั้งที่เงื่อนไขเดียวกันนี้เคย encode ไว้แล้วในบทที่ 7 recall ชิ้นส่วนที่เกี่ยวข้อง:
// recall จากบทที่ 5 — Order มี state machine แล้วpublic enum OrderStatus { Placed, Confirmed, Preparing, PickedUp, Delivered, Rejected, Cancelled }
// recall จากบทที่ 5 — เพิ่มเข้าไปใน Order (สมาชิกอื่นเหมือนที่ recall ไว้ข้างบนทุกตัว)public sealed class Order{ // ... เหมือนเดิม public OrderStatus Status { get; private set; } = OrderStatus.Placed;}
// recall จากบทที่ 7public interface ISpecification<T>{ bool IsSatisfiedBy(T candidate);}
// recall จากบทที่ 7 — เข้ารหัส invariant ⑥ (ยกเลิกได้ก่อนถึงขั้น PickedUp เท่านั้น)public sealed class CancellableOrderSpec : ISpecification<Order>{ public bool IsSatisfiedBy(Order candidate) => candidate.Status is OrderStatus.Placed or OrderStatus.Confirmed or OrderStatus.Preparing;}CancellableOrderSpec ตัวนี้ถูกใช้เป็น guard ตอนเรียก Order.Cancel() อยู่แล้ว (บทที่ 7) — แทนที่จะเขียนเงื่อนไข Status แบบเดียวกันซ้ำอีกรอบตอน query เราแค่ให้ IOrderRepository รับ specificationSpecificationpattern ห่อ 'กฎการคัดเลือก/เงื่อนไข' เป็นอ็อบเจ็กต์ที่มี IsSatisfiedBy(...) นำมาประกอบ (and/or/not) และใช้ซ้ำได้ เช่น EligiblePromotionSpec, CancellableOrderSpecTactical Design เข้าไปตรงๆ ได้เลย:
// ต่อยอด IOrderRepository (recall interface เดิมจากด้านบนแล้วเพิ่ม method นี้เข้าไป)public interface IOrderRepository{ Task SaveAsync(Order order, CancellationToken ct); Task<Order?> FindAsync(OrderId id, CancellationToken ct);
// ใหม่ในบทนี้ — spec-driven query Task<IReadOnlyList<Order>> FindAsync(ISpecification<Order> spec, CancellationToken ct);}
// ตัวอย่างการใช้งาน — เช่นใน background job ที่หาออเดอร์ที่ยังยกเลิกได้ทั้งหมดpublic async Task<IReadOnlyList<Order>> GetCancellableOrdersAsync( IOrderRepository orders, CancellationToken ct){ return await orders.FindAsync(new CancellableOrderSpec(), ct);}นี่คือแนวทางเดียวกับ library Specification ของ Ardalis (Steve Smith) ที่ให้ repository รับ specification เป็น parameter แล้วให้ adapter ฝั่ง Infrastructure เป็นคนแปลงมันเป็น query จริง (เช่น LINQ ต่อ IQueryable<Order> ใน EfOrderRepository) — Domain/Application ไม่ต้องรู้เลยว่าข้างหลังแปลเป็น SQL แบบไหน รู้แค่ว่ากฎ “ยกเลิกได้ไหม” ถูกเขียนไว้ ที่เดียว แล้วใช้ซ้ำได้ทั้งตอนรักษา invariant และตอน query — ปิดวงจรที่บทที่ 7 เปิดไว้พอดี
Factory สร้างของใหม่ / Repository หาของเก่า
หัวข้อที่มีชื่อว่า “Factory สร้างของใหม่ / Repository หาของเก่า”flowchart LR
Cart["Cart<br/>(ตะกร้า ยังไม่ล็อกราคา)"] -->|"Factory<br/>(ตั้งราคาผ่าน IMenuCatalog)"| Order["Order<br/>เกิดมาถูกต้องแล้ว (born valid)"]
Order <-->|"Repository<br/>(SaveAsync / FindAsync)"| Storage[("ที่เก็บข้อมูล")]
คำบรรยายภาพ: Factory สร้างของใหม่ — แปลง Cart ให้กลายเป็น Order ที่ผ่าน invariant ตั้งแต่แรกเกิด ส่วน Repository หาของเก่า — คุยกับที่เก็บข้อมูลราวกับ Order ทุกตัวนั่งรออยู่ใน collection เดียวกันอยู่แล้ว สองตัวนี้ไม่เคยทำหน้าที่ของกันและกันเลย — Factory ไม่เคย “หา” อะไร มันสร้างอย่างเดียว ส่วน Repository ไม่เคย “สร้าง” อะไรใหม่ มันหาของที่มีอยู่แล้วอย่างเดียว
สรุปทั้งคอร์ส + สะพานไปคอร์สถัดไป
หัวข้อที่มีชื่อว่า “สรุปทั้งคอร์ส + สะพานไปคอร์สถัดไป”ย้อนดูสิ่งที่คอร์สนี้เติมให้ domain Order เดิมจากคอร์ส Clean Architecture .NET ทีละบท: เริ่มจากตั้งชื่อผู้ร้าย domain กลวง (บทที่ 1) แล้วไล่แก้ทีละมุม — Value Object ที่ทำให้ค่า “ผิดไม่ได้” (บทที่ 2), Entity ที่เทียบด้วย identity ไม่ใช่ค่า (บทที่ 3), Aggregate ที่มี root เป็นประตูเดียวรักษา invariant (บทที่ 4), state machine ที่ทำให้เปลี่ยนสถานะผิดกฎไม่ได้ (บทที่ 5), Domain Event ที่สื่อสารข้าม aggregate แบบ eventual consistency (บทที่ 6), Domain Service กับ Specification ที่ให้บ้านกับกฎที่ไม่ได้เป็นของ object ตัวเดียว (บทที่ 7) และปิดท้ายด้วย Factory กับ Repository ในบทนี้ — วงจรชีวิตของ aggregate ตั้งแต่เกิดจนถูกเก็บและค้นคืน
สิ่งที่น่าสังเกตคือ: object เกือบทั้งหมดที่เราสร้างตลอดคอร์สนี้ — Money, Quantity, OrderLine, Order, PromotionEngine, CancellableOrderSpec — เป็น pure type ล้วนๆ ไม่พึ่ง network, ไม่พึ่งฐานข้อมูล, ไม่พึ่งเวลาจริง (นาฬิกาเข้ามาทาง IClock เป็น port ต่างหาก) นั่นแปลว่ามัน unit test ได้ทันทีโดยไม่ต้องใช้ mock framework สักตัวเดียว — ยกเว้นแค่ขอบที่สัมผัส I/O จริงอย่าง OrderFactory และ adapter ของ IOrderRepository ที่ต้องใช้ test double แทน คอร์สถัดไปในซีรีส์ — testing-in-practice — จะพาไปดูรายละเอียดตรงนี้แบบเต็มๆ ว่าทำไม pure domain model แบบนี้ถึงทดสอบได้เร็วและมั่นใจกว่าการ mock ทุกอย่างตั้งแต่ต้น
ถ้าอยากอ่านต้นตอของ Factory และ Repository ในภาษา DDD แบบเจาะลึกกว่านี้ กลับไปอ่านต่อได้ที่คอร์ส ddd-patterns บทที่ 20 (Factories) และ บทที่ 21 (Repositories) — สองบทนั้นวางรากฐานเชิงทฤษฎีไว้ก่อน ส่วนคอร์สนี้พาลงมาเห็นว่ามันหน้าตาเป็นยังไงจริงๆ ใน code C# บน domain เดียวกันตลอดทั้งซีรีส์
เจาะลึกแนวคิดในบทนี้ต่อได้ที่คลังอ้างอิง DevIQ และคอร์ส ddd-patterns:
- Factory Method — creational pattern ต้นทางของแนวคิด encapsulate การสร้าง object
- Repository — abstraction ของ persistence ที่มี interface คล้าย collection
- ddd-patterns บทที่ 20 — Factories — ทฤษฎีเต็มของ Factory ใน DDD
- ddd-patterns บทที่ 21 — Repositories — ทฤษฎีเต็มของ Repository ใน DDD
เช็กความเข้าใจ — บทที่ 8
ข้อ 1 / 3อะไรคือความต่างหลักระหว่าง Factory กับ Repository ในการจัดการ Order?