🎯 什麼情境該想到我

★ 分工:這頁講「什麼時候建立邏輯複雜到該搬出去」(模型問題);
工廠方法模式/抽象工廠模式/建造者模式 講「GoF 的三種結構與 Go 的長相」。

當建立一個物件或聚合要組裝很多零件、套很多規則,複雜到塞進建構子會讓呼叫端知道太多內部細節時——就把這段建立邏輯抽出來交給工廠。

⚙️ 怎麼用(步驟 / 公式)

意圖:封裝複雜物件/聚合的建立邏輯,確保一建立出來就處於一致且完整的狀態。

  1. 把複雜的組裝、預設值、關聯建立集中到工廠(方法、獨立類別或聚合根上的建立方法)。
  2. 讓工廠在回傳前就滿足所有不變式——不允許產出半成品或不一致的物件。
  3. 呼叫端只給必要的原料,不需了解內部如何拼裝。
  4. 建立整個聚合時,工廠負責一次產出根與其內部物件的一致整體。

★★ 判準:建立過程本身有業務規則嗎

// ★ ① 建立很簡單 → ★★ 不需要工廠,一個建構函式就好
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。

  • ★ 不該跨聚合建立(建訂單順便建客戶),那是邊界畫錯。

  • ★ 「只是想集中建構」不是工廠的理由,那是 登錄器 式的思維。

🔗 相關工具