🎯 什麼情境該想到我
★ 分工:這頁講「什麼時候建立邏輯複雜到該搬出去」(模型問題);
工廠方法模式/抽象工廠模式/建造者模式 講「GoF 的三種結構與 Go 的長相」。
當建立一個物件或聚合要組裝很多零件、套很多規則,複雜到塞進建構子會讓呼叫端知道太多內部細節時——就把這段建立邏輯抽出來交給工廠。
⚙️ 怎麼用(步驟 / 公式)
意圖:封裝複雜物件/聚合的建立邏輯,確保一建立出來就處於一致且完整的狀態。
- 把複雜的組裝、預設值、關聯建立集中到工廠(方法、獨立類別或聚合根上的建立方法)。
- 讓工廠在回傳前就滿足所有不變式——不允許產出半成品或不一致的物件。
- 呼叫端只給必要的原料,不需了解內部如何拼裝。
- 建立整個聚合時,工廠負責一次產出根與其內部物件的一致整體。
★★ 判準:建立過程本身有業務規則嗎
// ★ ① 建立很簡單 → ★★ 不需要工廠,一個建構函式就好
func New(customerID CustomerID) *Order { … }
// ★★ ② 建立需要「不屬於這個聚合的知識」→ ★ 需要工廠
func NewOrderFromCart(c Cart, pricing PricingRules, tax TaxTable) (*Order, error)
// → ★ Order 不該知道定價規則與稅表 ⇒ ★★ 把組裝邏輯搬到工廠
// ★ ③ 建立要產生「一整組互相關聯的物件」→ ★ 需要工廠
// ★ 一張訂單 + 它的品項 + 初始的狀態事件★★ 判準的反面更有用:★ 如果建構函式的參數列裡出現了「別的聚合」或「服務」,
那就該有一個工廠。
★ 因為那意味著建立需要協調,而協調不屬於被建立的那個物件。
★★ Go 的三種工廠形態(不需要 GoF 的類別階層)
// ★★ ① 套件層函式(最常見、最 Go)
package order
func NewFromCart(c Cart, p PricingRules) (*Order, error) { … }
// → ★ 不需要 OrderFactory 型別;★★ Go 的 package 就是命名空間
// ★ ② 有狀態的工廠(需要注入依賴時)
type Factory struct {
pricing PricingRules // ★ 建立時需要的外部知識
idGen func() ID // ★★ 注入 ID 產生器 → 測試可控
now func() time.Time // ★★ 注入時鐘 → 測試可控
}
func (f Factory) FromCart(c Cart) (*Order, error) { … }
// → ★ 這是實務上最有用的形態:★★ 它讓「建立時的不確定性」(ID、時間)可測
// ★ ③ 重建用的工廠(★ 和建立完全不同,見下)
func Reconstitute(row db.Order, lines []db.OrderLine) (*Order, error) { … }★★ ② 的 idGen 與 now 是最實用的一招:
// ★ 測試裡
f := Factory{idGen: func() ID { return "ord_test_1" }, now: func() time.Time { return fixedTime }}
// → ★★ 「建立出來的訂單長什麼樣」變成一個確定的、可以用 golden file 比對的東西★★ 建立(New)與重建(Reconstitute)是兩件事
★★ 這是工廠最重要、也最常被搞混的一點:
★ New(建立一個「新的」):
① ★ 執行所有業務驗證(★ 空訂單不可建立)
② ★ 產生新的 ID
③ ★ 設定初始狀態(Draft)
④ ★ 產生 OrderCreated 事件
★★ Reconstitute(從資料庫重建一個「已存在的」):
① ★★ 不執行業務驗證 —— ★ 資料庫裡的資料已經是有效的
→ ★ 否則:★★ 你新增一條驗證規則,所有舊訂單都載不出來
② ★★ 使用既有的 ID 與 version
③ ★★ 不產生任何事件 —— ★ 它不是「發生了什麼」,只是「讀出來」
④ ★ 只做「解析」層級的檢查(★ status 字串能不能解析成 Status)
★★ 忘記這個區分的具體後果:
★ 用 New 重建 → ① 舊資料載不出來 ② ★★ 每次讀取都發出 OrderCreated 事件
→ ★ 第 ② 點會造成真實的災難(★ 下游以為有一堆新訂單)
→ ★ 見 資料對應器 的 Reconstitute 討論。
★★ 工廠不該做的四件事
① ★★ 不該碰資料庫
★ NewOrderFromCart 去查商品價格 → ★ 那是 [[領域服務]] 或呼叫端的責任
→ ★ 工廠收「已經查好的資料」(PricingRules),不自己去查
→ ★★ 這讓工廠是純函式 ⇒ 可以窮舉測試
② ★★ 不該發布事件
★ 工廠產生事件放進聚合的 events slice ✅
★★ 工廠自己 publish 到訊息系統 ❌ → ★ 那是 [[服務層]] 在交易 commit 後做的事
(見 [[工具-Transactional-Outbox-Pattern]])
③ ★ 不該跨聚合建立
★ 「建立訂單時順便建立客戶」→ ★★ 兩個聚合一個交易,邊界畫錯了
④ ★ 不該有「選哪一種實作」以外的分派邏輯
→ ★ 那是 [[領域服務]]
★ 什麼時候真的不需要工廠
- ★★ 建構函式參數少、沒有外部依賴 → ★ 直接
New(...)。 - ★ 建立邏輯只有一處用 → ★ 寫在那裡就好。
- ★ 你只是想「集中建構」 → ★★ 那是 登錄器 式的思維,不是工廠。
★ 兄弟辨析
| 差別 | |
|---|---|
| 工廠方法模式 | ★ GoF:用子類決定建立哪個具體型別(★ Go 用分派表) |
| 抽象工廠模式 | ★ GoF:建立一整組相關物件 |
| 建造者模式 | ★ GoF:分步組裝(★ Go 用 Functional Options) |
| 聚合 | ★ 工廠建立整個聚合,不只聚合根 |
| 儲存庫(DDD) | ★ 重建與持久化;工廠負責「新建」 |
🧪 我實際套用的紀錄
- (待填)
⚠️ 注意 / 什麼時候不適用
-
建立很單純時不需要工廠,直接用建構子即可,別過度設計。
-
工廠負責「建立新物件」,儲存庫(DDD)負責「取回既有物件」,兩者職責不要混。
-
若建立規則本身是重要領域概念,考慮讓它顯性成為領域的一部分。
-
★★ 建立(New)與重建(Reconstitute)是兩件事。 用 New 重建 → 舊資料載不出來,而且每次讀取都發出 Created 事件(下游以為有一堆新訂單)。
-
★★ 判準:建構函式參數列裡出現了「別的聚合」或「服務」嗎? 有 → 該有工廠。
-
★★ 工廠不該碰資料庫。收「已經查好的資料」,這讓它是純函式、可窮舉測試。
-
★★ 工廠不該自己 publish 事件。把事件放進聚合的 events slice,由服務層在 commit 後發(Outbox)。
-
★ 注入
idGen與now是最實用的一招,讓「建立出來的物件」變成確定的、可用 golden file 比對的東西。 -
★ Go 不需要
OrderFactory型別,package 層函式就是工廠。只有需要注入依賴時才用 struct。 -
★ 不該跨聚合建立(建訂單順便建客戶),那是邊界畫錯。
-
★ 「只是想集中建構」不是工廠的理由,那是 登錄器 式的思維。