Factories — แยก "การใช้" ออกจาก "การสร้าง"
เราจะ สร้าง บันทึก และดึง domain object กลับมาได้อย่างไร โดยที่ domain modelDomain ModelModel ของ Domain ที่ฝังกฎและกระบวนการทางธุรกิจไว้ใน code จริง หัวใจของ DDD คือการพัฒนา domain model ที่ “เข้าใจธุรกิจอย่างลึกซึ้ง”Strategic Design ยังไม่ถูกรบกวนด้วยเรื่องเชิงเทคนิค? วงจรชีวิตของ object ที่ซับซ้อนมักต้องอาศัยการ “ประสานงาน” ทั้งตอนถือกำเนิดและตอนถูกบันทึก และการรับประกันว่า invariantInvariantกฎทางธุรกิจที่ “ต้องเป็นจริงเสมอ” ภายใน Aggregate เช่น ยอดรวมคำสั่งซื้อต้องเท่ากับผลรวมของรายการสินค้า — ต้องถูกต้องภายใน transaction เดียวTactical Design ถูกต้องตั้งแต่ตอนสร้างนั้นทำได้ด้วย FactoryFactoryตัวที่ห่อหุ้มตรรกะการ “สร้าง” Aggregate ที่ซับซ้อน และรับประกันว่าอ็อบเจ็กต์ใหม่ถูกต้องตาม invariant ตั้งแต่แรกเกิด — “สร้างของใหม่” (repository หา “ของเก่า”)Tactical Design — หนึ่งใน creational pattern ของ Gang of Four ที่ Eric Evans หยิบมาปรับใช้ใน DDD
บทนี้จะพาดูว่า Factory แก้ปัญหาอะไร ใช้ตอนไหน และทำไมการ “แยกการใช้ออกจากการสร้าง” (separate use from construction) จึงทำให้ code ของเราสื่อความหมายและดูแลรักษาง่ายขึ้น
บทบาทของ Factory
หัวข้อที่มีชื่อว่า “บทบาทของ Factory”เมื่อ AggregateAggregateกลุ่มของ Entity และ Value Object ที่ถูกมองเป็นหนึ่งหน่วยเดียวเพื่อรักษาความถูกต้องของข้อมูล มีขอบเขตชัดเจน และเป็นหน่วยของ transactionTactical Design, EntityEntityอ็อบเจ็กต์ที่มี “ตัวตน” (identity) ต่อเนื่องตามเวลา แม้ค่าภายในเปลี่ยน ก็ยังเป็นสิ่งเดิม เช่น ลูกค้า, คำสั่งซื้อ — แยกแยะด้วย id ไม่ใช่ด้วยค่าTactical Design และ Value ObjectValue Objectอ็อบเจ็กต์ที่สำคัญที่ “ค่า” ของมัน ไม่มีตัวตนเฉพาะ สองตัวที่ค่าเท่ากันถือว่าเท่ากัน ควรเป็น immutable (เปลี่ยนค่าไม่ได้) เช่น เงิน, วันที่, พิกัดTactical Design เริ่มซับซ้อนใน domain ที่ใหญ่และเต็มไปด้วยตรรกะ การสร้าง instance ที่ “ถูกต้อง” อาจต้องอาศัยความรู้เชิงลึกเกี่ยวกับกฎภายในของ object อื่น หากความรู้เช่นนี้รั่วไปอยู่ผิดที่ จะทำให้ domain อ่านยากและ ละเมิดหลัก Single Responsibility Principle (SRP) เพราะ class หนึ่งต้องไปรับรู้ invariant ของอีก class หนึ่ง
Evans จึงแนะนำให้ แยก “การใช้” ออกจาก “การสร้าง” (separate use from construction) แล้วห่อหุ้มตรรกะการสร้างไว้ใน factory object เมื่อตรรกะนั้นซับซ้อนหรือเมื่อมันสื่อความหมายได้ดีกว่าเดิม การสร้าง object ไม่ใช่ “เรื่องของ domain” โดยตรง แต่ก็อาศัยอยู่ใน domain layer — คุณแทบไม่เคยพูดเรื่อง factory กับ domain expert แต่มันก็มีบทบาทสำคัญอยู่เบื้องหลัง
FactoryFactoryตัวที่ห่อหุ้มตรรกะการ “สร้าง” Aggregate ที่ซับซ้อน และรับประกันว่าอ็อบเจ็กต์ใหม่ถูกต้องตาม invariant ตั้งแต่แรกเกิด — “สร้างของใหม่” (repository หา “ของเก่า”)Tactical Design ทำหน้าที่ ซ่อนความซับซ้อนของการสร้าง object — ตัดสินใจว่าจะสร้าง class ไหน ตรวจสอบ invariant และสื่อ “เจตนา (intent)” ของการสร้างแต่ละแบบให้ชัดเจน ซึ่งเป็นสิ่งที่ constructor เปล่าๆ ทำได้ยาก จำหลักสั้นๆ ไว้ว่า Factory สร้าง “ของใหม่” ส่วน RepositoryRepositoryตัวที่ทำให้เราเข้าถึง Aggregate ราวกับเป็น collection ในหน่วยความจำ ซ่อนรายละเอียดฐานข้อมูล มี1 repository ต่อ1 Aggregate RootTactical Design หา “ของเก่า”
วัตถุประสงค์หลักของ factory pattern คือ ซ่อนความซับซ้อนของการสร้าง object โดยความซับซ้อนนั้นอาจเป็นการตัดสินใจว่าจะ instantiate class ไหน (เมื่อ client ทำงานกับ abstraction) หรืออาจเป็นการตรวจสอบ invariant ส่วนวัตถุประสงค์รองคือ สื่อเจตนาเบื้องหลังการสร้างที่มีหลายรูปแบบ ให้ชัดเจน
รูปแบบมาตรฐานคือมี static method ที่คืนค่าเป็น abstract class หรือ interface โดย client ส่งข้อมูลบางอย่างเข้าไป แล้ว factory ใช้ข้อมูลนั้นตัดสินใจว่าจะสร้าง subclass ตัวไหนคืนกลับ ผลคือ client ไม่จำเป็นต้องรู้เลยว่า dependent class ถูกสร้างขึ้นอย่างไร — เป็นไปตามหลัก Dependency Inversion Principle และยังได้ประโยชน์เพิ่มเติมคือ รวม0 code การสร้าง ไว้ที่เดียว หากวิธีสร้างเปลี่ยน ก็แก้เพียงจุดเดียวโดยไม่กระทบ code ที่เรียกใช้
Encapsulate โครงสร้างภายในของ Aggregate
หัวข้อที่มีชื่อว่า “Encapsulate โครงสร้างภายในของ Aggregate”เมื่อต้องเพิ่ม element เข้าไปใน Aggregate สิ่งสำคัญคือ อย่าเปิดเผยโครงสร้างภายในของ Aggregate ลองดูปัญหาก่อน: สมมติ application service ต้องรู้รายละเอียดการประกอบ BasketItem เองทั้งหมด
public class AddProductToBasket{ public void Add(Product product, Guid basketId) { var basket = _basketRepository.FindBy(basketId); // service ต้องรู้กฎการประกอบ BasketItem เอง — ไม่ใช่หน้าที่มัน var rate = TaxRateService.ObtainTaxRateFor(product.Id, country.Id); var item = new BasketItem(rate, product.Id, product.price); basket.Add(item); }}Application serviceApplication Serviceบริการที่ “ประสานงาน” use case — จัดการ transaction, ความปลอดภัย, การบันทึก, แปลง DTO — แต่ไม่มีกฎธุรกิจอยู่ในตัวมันเองTactical Design ควรมีหน้าที่ ประสานงาน (coordination) เท่านั้น ไม่ใช่ไปรับรู้ว่า BasketItem ประกอบขึ้นอย่างไร เราเลี่ยงการรั่วไหลของโครงสร้างภายในได้ด้วยการย้ายความรับผิดชอบกลับเข้าไปใน domain layer — ให้ Basket เองเป็นผู้รับ product แล้วจัดการเพิ่มรายการภายใน
public class AddProductToBasket{ public void Add(Product product, Guid basketId) { var basket = _basketRepository.FindBy(basketId); basket.Add(product); // client เรียบง่ายลงมาก ไม่รู้โครงสร้างภายใน }}
public class Basket // domain layer{ public void Add(Product product) { if (Contains(product)) GetItemFor(product).IncreaseItemQuantityBy(1); else { var rate = TaxRateService.ObtainTaxRateFor(product.Id, country.Id); _items.Add(new BasketItem(rate, product.Id, product.price)); } }}ตอนนี้ Basket ในฐานะ Aggregate รักษาความถูกต้องของ collection ภายในได้เอง เพราะมันบังคับ invariant ของตัวเองได้ แต่ยังเหลือปัญหาอยู่: Basket ต้องรับผิดชอบการสร้าง dependency ที่มันไม่ได้เป็นเจ้าของ (อย่าง tax rate) เพราะมันต้องรู้ทั้งวิธีประกอบ item ที่ถูกต้องและรู้ว่าจะหา tax rate ที่ถูกต้องได้จากที่ใด — กลายเป็นความรับผิดชอบส่วนเกิน
ทางแก้คือ แทรก factory object เข้ามาห่อหุ้มการสร้าง BasketItem รวมถึงการไปดึง tax rate ที่ถูกต้อง
public class Basket{ public void Add(Product product) { if (Contains(product)) GetItemFor(product).IncreaseItemQuantityBy(1); else _items.Add(BasketItemFactory.CreateItemFor(product, deliveryAddress)); }}
public class BasketItemFactory{ public static BasketItem CreateItemFor(Product product, Country country) { var rate = TaxRateService.ObtainTaxRateFor(product.Id, country.Id); return new BasketItem(rate, product.Id, product.price); }}ถ้าวิธีคำนวณ tax rate เปลี่ยน หรือ BasketItem ต้องการข้อมูลชนิดอื่นเพิ่ม class Basket จะไม่ได้รับผลกระทบเลย
คุณ “อาจ” เรียก service ภายใน constructor ของ BasketItem ได้ แต่นั่นเป็นแนวทางที่ไม่ดี — constructor ควรเรียบง่ายเข้าไว้ หากคุณพบว่ามี code ซับซ้อนแฝงอยู่ใน constructor นั่นมักเป็นสัญญาณว่าคุณต้องการ Factory
ในระบบ e-commerce Basket คือ Aggregate ส่วนการประกอบ BasketItem ที่ถูกต้อง (ต้องมี tax rate ตามประเทศปลายทาง) คือตรรกะที่ซับซ้อน เมื่อย้ายมันไปไว้ที่ BasketItemFactory ทั้ง application service และตัว Basket เองก็ไม่ต้องแบกความรู้เรื่องภาษีอีกต่อไป — แต่ละส่วนรับผิดชอบหน้าที่เดียวอย่างชัดเจน
ซ่อนการตัดสินใจว่าจะสร้าง “ชนิดไหน”
หัวข้อที่มีชื่อว่า “ซ่อนการตัดสินใจว่าจะสร้าง “ชนิดไหน””Factory ใน domain layer ยังใช้ ซ่อนการตัดสินใจเลือกชนิด (type) ได้ เมื่อมีตัวเลือกหลายแบบและการเลือกนั้นไม่ใช่หน้าที่ของ client โดย client เขียน code อิงกับ interface หรือ abstract class แล้วปล่อยให้ factory เป็นผู้สร้าง concrete type ที่เหมาะสม
ลองดูตัวอย่างจากโลกการขนส่ง: Order สามารถสร้าง Consignment (พัสดุที่จะส่ง) ได้ — ซึ่งตัว method นี้ก็คือ factory method ในตัวเอง ที่น่าสนใจคือการจะสร้าง consignment ที่ถูกต้องนั้นต้องเลือก ผู้ขนส่ง (Courier) แต่ Order ไม่รู้ว่าควรใช้ courier รายใด จึง delegate งานนี้ให้ CourierFactory แล้วทำงานกับ abstract class Courier
public class Order{ public Consignment CreateFor(IEnumerable<Item> items, DeliveryAddress destination) { var courier = CourierFactory.GetCourierFor(items, destination.Country); var consignment = new Consignment(items, destination, courier); SetAsDispatched(items, consignment); return consignment; }}
public class CourierFactory{ public static Courier GetCourierFor( IEnumerable<Item> consignmentItems, DeliveryAddress destination) { if (AirMail.CanDeliver(consignmentItems, destination)) return new AirMail(consignmentItems, destination); if (TrackedService.CanDeliver(consignmentItems, destination)) return new TrackedService(consignmentItems, destination); return new StandardMail(consignmentItems, destination); }}CourierFactory ถาม implementation แต่ละตัวว่ารับงานนี้ได้ไหม (CanDeliver) แล้วคืน concrete type ที่เหมาะสม — Order ไม่ต้องรู้เลยว่ามี courier กี่ชนิดหรือเลือกอย่างไร
ในระบบขนส่งสินค้า การเลือกว่าจะส่งพัสดุด้วย AirMail, TrackedService หรือ StandardMail ขึ้นอยู่กับน้ำหนัก ปลายทาง และข้อจำกัดอื่นๆ — เป็นการตัดสินใจที่ Order ไม่ควรต้องแบกไว้ ปล่อยให้ CourierFactory เป็นผู้ตัดสินใจ แล้ว Order ทำงานกับ abstraction Courier เท่านั้น ทำให้เพิ่มผู้ขนส่งรายใหม่ได้โดยไม่ต้องแตะ Order
Factory Method บน Aggregate
หัวข้อที่มีชื่อว่า “Factory Method บน Aggregate”Factory ไม่จำเป็นต้องเป็น static class แยกต่างหากเสมอไป — factory method สามารถอยู่บน Aggregate เองได้ เพื่อซ่อนความซับซ้อนของการสร้างจาก client
ตัวอย่าง: Basket เปิดความสามารถ “ย้ายสินค้าไป Wish List” โดยแปลง BasketItem ให้เป็น WishListItem (ซึ่งเป็น Entity ของ Aggregate WishList) ตัว Basket ไม่ได้ข้องเกี่ยวกับ WishListItem หลังสร้างเสร็จ แต่มันถือ “ข้อมูล” ที่จำเป็นต่อการสร้างอยู่ — การวาง factory method ไว้ตรงนี้จึงเป็นธรรมชาติ และ client ก็ไม่ต้องรู้วิธีดึงข้อมูลออกจาก BasketItem มาประกอบเป็น WishListItem เอง
public class Basket{ public WishListItem MoveToWishList(Product product) { if (BasketContainsAnItemFor(product)) { var wishListItem = WishListItemFactory.CreateFrom(GetItemFor(product)); RemoveItemFor(product); return wishListItem; } }}factory method ยังสามารถ สร้าง Aggregate ตัวใหม่ ได้ด้วย เช่น ใช้ Account สร้าง Order โดยตรวจสอบก่อนว่ามี credit เพียงพอหรือไม่ — นี่คือจุดที่ factory method ช่วย “บังคับ invariant ตั้งแต่แรกเกิด” ได้อย่างสวยงาม
public class Account{ public Order CreateOrder() { if (HasEnoughCreditToOrder()) return new Order(this.Id, this.PaymentMethod, this.Address); else throw new InsufficientCreditToCreateAnOrder(); }
// factory method อีกตัวที่สื่อ "เจตนา" ต่างออกไป — ข้ามการเช็ก credit public Order CreateAnOrderIgnoringCreditRating() { return new Order(this.Id, this.PaymentMethod, this.Address, PaymentType.PayBeforeShipping); }}หาก Order มี constructor หลายตัวเพื่อจุดประสงค์ต่างกัน ย่อมเข้าใจยากว่าตัวไหนทำอะไร แต่เมื่อใช้ factory method ที่ตั้งชื่อสื่อความหมาย — CreateOrder() กับ CreateAnOrderIgnoringCreditRating() — เจตนาก็ชัดเจนในทันที นี่คือคุณค่าหลักอย่างหนึ่งของ Factory
Factory สำหรับ Reconstitution (ปั้น object เก่ากลับมา)
หัวข้อที่มีชื่อว่า “Factory สำหรับ Reconstitution (ปั้น object เก่ากลับมา)”หากคุณไม่ได้ใช้ ORM ที่ map data model เข้ากับ domain model ได้โดยตรงด้วย reflection หรือคุณดึง domain object มาจากระบบเก่า (legacy) ผ่าน web service หรือ file flat คุณจำเป็นต้อง ปั้น (reconstitute) domain object กลับมาพร้อมรับประกันว่า invariant ครบถ้วน ซึ่งการ reconstitute ด้วย factory จะซับซ้อนกว่าการสร้างของใหม่อยู่เล็กน้อย
ในตัวอย่างนี้ RepositoryRepositoryตัวที่ทำให้เราเข้าถึง Aggregate ราวกับเป็น collection ในหน่วยความจำ ซ่อนรายละเอียดฐานข้อมูล มี1 repository ต่อ1 Aggregate RootTactical Design ดึง persistence model ของ basket มาในรูป raw data ล้วนๆ แล้ว delegate ให้ BasketFactory (ซึ่งอยู่ใน domain layer) ปั้น Basket ขึ้นมาจาก raw data นั้น
public class BasketRepository : IBasketRepository // infrastructure{ public Basket FindBy(Guid id) { BasketDTO rawData = ExternalService.ObtainBasket(id.ToString()); return BasketFactory.ReconstituteBasketFrom(rawData); }}
public class BasketFactory // domain layer{ public static Basket ReconstituteBasketFrom(BasketDTO rawData) { Basket basket; // ... var deliveryOption = DeliveryFactory.Create(rawData.DeliveryOptionId); if (deliveryOption.CanBeUsedFor(rawData.Items)) basket.Set(deliveryOption); else // ใช้ Null Object pattern แทนการ throw เพื่อให้ผู้ใช้เลือกใหม่ได้ basket.Set(DeliveryFactory.CreateNonChosen()); return basket; }}BasketFactory สร้าง DeliveryOption จาก raw data แล้วตรวจสอบว่าใช้กับ item ในตะกร้านี้ได้หรือไม่ หาก ใช้ไม่ได้ มันก็จะไม่ใช้ตัวนั้น แต่หันไปใช้ Null Object pattern เพื่อบังคับให้มีการเลือก DeliveryOption ใหม่แทน
เมื่อปั้น Aggregate กลับมา การรักษา invariant ถือเป็นเรื่องสำคัญมาก — แต่ก็ต้องยอมรับความจริงว่า object นั้น “มีตัวตนอยู่ในสถานะที่ถูกบันทึกไว้แล้ว” ทางเลือกหนึ่งคือ throw exception แล้วเปิด workflow ซ่อมแซม แต่ในกรณีข้างต้น การปั้น Aggregate ขึ้นมาแล้วแทนที่ delivery option ด้วย placeholder (Null Object) ให้ผู้ใช้เลือกใหม่ ง่ายและสมจริงกว่า
ใช้ Factory อย่างมีวิจารณญาณ
หัวข้อที่มีชื่อว่า “ใช้ Factory อย่างมีวิจารณญาณ”Factory ช่วย ลดความรกของ domain model ให้ยังคงสื่อความหมายได้ดี แต่ อย่าใช้อย่างพร่ำเพรื่อทุกครั้งที่ต้อง instantiate object ควรใช้ factory เมื่อ:
- มันสื่อความหมายได้ดีกว่า constructor เปล่าๆ
- มันให้ความสะดวกเมื่อมี constructor หลายตัวจนชวนสับสน
- element ที่ต้องใช้ในตรรกะการสร้าง ไม่ใช่หน้าที่ ของ dependent class
flowchart TD
Q1{ตรรกะการสร้างซับซ้อน<br/>หรือต้องเช็ก invariant?}
Q2{มี constructor หลายตัว<br/>จนเจตนาไม่ชัด?}
Q3{client ไม่ควรรู้ว่าจะ<br/>สร้าง subclass ไหน?}
USE[ใช้ Factory]
CTOR[constructor ธรรมดาก็พอ]
Q1 -->|ใช่| USE
Q1 -->|ไม่| Q2
Q2 -->|ใช่| USE
Q2 -->|ไม่| Q3
Q3 -->|ใช่| USE
Q3 -->|ไม่| CTOR
- Factory แยก “การใช้” ออกจาก “การสร้าง” (separate use from construction) ภายใน domain
- ความรู้ในการสร้าง domain object ที่ซับซ้อนถูก ห่อหุ้มไว้จาก client และ domain object อื่น ได้ด้วย factory
- Factory เลือกสร้าง instance ที่เหมาะสมตามความต้องการของผู้เรียกได้ โดยซ่อนการตัดสินใจเลือก subclass ไว้เบื้องหลัง
- factory method ห่อหุ้มสถานะภายในของ Aggregate ได้ในตัว
- เมื่อมี constructor หลายตัวเพื่อจุดประสงค์ต่างกัน ให้ใช้ factory ที่ตั้งชื่อสื่อความหมายเพื่อให้เจตนาชัดเจนยิ่งขึ้น
เจาะลึกแนวคิดในบทนี้ต่อได้ที่คลังอ้างอิง DevIQ:
- Factory Method — GoF pattern ต้นทางของแนวคิด factory method ที่บทนี้ใช้ห่อหุ้มการสร้าง object และบังคับ invariant ตั้งแต่แรกเกิด
- Abstract Factory — ใกล้เคียงกับกรณี
CourierFactoryที่ต้องเลือกสร้าง concrete subclass (AirMail, TrackedService, StandardMail) โดยที่ client ทำงานกับ abstraction เท่านั้น - Aggregate — Factory มีหน้าที่สร้างและ reconstitute Aggregate พร้อมรับประกันว่า invariant ของมันถูกต้องตั้งแต่ถือกำเนิด
- Repository — คู่หูของ Factory ตามหลัก “Factory สร้างของใหม่ ส่วน Repository หาของเก่า” ที่บทนี้ใช้เป็นหลักจำ
- Null Object — เทคนิคที่
BasketFactoryใช้แทน delivery option ที่ใช้ไม่ได้ตอน reconstitute แทนการ throw exception
เช็กความเข้าใจ — บทที่ 20
ข้อ 1 / 4วัตถุประสงค์ 'หลัก' ของ Factory pattern ตามหนังสือคืออะไร?