🎯 什麼情境該想到我

★ 分工:這頁講「什麼邏輯不屬於任何實體,該獨立出來」(領域層);
服務層(PoEAA)講「交易邊界、授權、DTO 組裝」(應用層)。★★ 兩者是不同的層。

當某個重要的領域操作「不自然屬於任何一個實體或值物件」——例如轉帳牽涉兩個帳戶、計價牽涉多方規則——硬塞進其中一個物件會讓它職責失衡時。

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

意圖:把不屬於任何實體/值物件的重要領域操作,做成無狀態的服務。

  1. 先問:這動作能自然放進某個實體或值物件嗎?可以就別開服務。
  2. 不行、且它是個重要領域概念時,抽成領域服務。
  3. 讓服務無狀態:狀態仍留在實體與值物件裡,服務只承接一段操作。
  4. 以動詞/動作命名(如 TransferMoney、計算運費),並使用通用語言的詞彙。
  5. 介面用領域語言表達,實作可協調多個聚合完成該操作。

★★ 最重要的一件事:領域服務 ≠ 應用服務(而混用是最常見的錯誤)

★★ 三層「服務」,責任完全不同:

★ 應用服務(= [[服務層]])—— ★ 在領域層「之上」
  ★ 交易邊界、授權、載入聚合、呼叫領域邏輯、發事件、組 DTO
  ★★ 它「知道」自己在處理一個 use case

★★ 領域服務 —— ★ 在領域層「之內」
  ★ 表達一個「不屬於任何單一實體或值物件」的業務操作
  ★★ 它是純的(★ 不碰交易、不碰資料庫、不知道 use case)

★ 基礎設施服務 —— ★ 在領域層「之下」
  ★ 寄信、發訊息、存檔案(★ 透過 [[閘道]] 隔開)

★★ 判準:這個「服務」有沒有 ctx 與 error 以外的 I/O?
有 → ★ 它是應用服務或基礎設施服務,不是領域服務。
★ 領域服務應該是純函式或純方法——這是它和另兩種最可靠的區分。

★★ 什麼邏輯真的屬於領域服務:三個特徵

① ★★ 它跨越多個聚合,但不改變它們
   ★ func CalculateShipping(o Order, dest Address, rates RateTable) (Money, error)
   → ★ 它需要訂單、地址、費率表;★★ 但它不「屬於」任何一個

② ★★ 它是一個業務上有名字的計算或決策
   ★ 「匯率轉換」「信用評分」「最佳路徑」「分帳」
   → ★ 領域專家會用一個名詞稱呼它

③ ★ 把它塞進任何實體都很勉強
   ★ order.CalculateShipping(dest, rates)?→ ★★ Order 為什麼要知道費率表?
   ★ address.CalculateShipping(order, rates)?→ ★ 更怪
   → ★★ 這個「塞哪裡都怪」的感覺就是領域服務的訊號

★★ 但要小心 ③ 的誤判:

★ 「塞哪裡都怪」也可能是「你漏了一個概念」。
  ★ CalculateShipping(o, dest, rates) 塞哪裡都怪
  → ★★ 也許缺的是一個 ShippingQuote 值物件:
     func NewShippingQuote(o Order, dest Address, rates RateTable) (ShippingQuote, error)
→ ★ 先問「有沒有一個沒被命名的概念」,★★ 再決定是不是領域服務

★ 這是 提煉類別 在領域層的版本。

★★ Go 的實作:一個純函式就是領域服務

package pricing     // ★★ 不需要 PricingService 型別
 
// ★ 純函式:吃值、吐值、不碰 I/O ⇒ 可以窮舉測試
func Quote(o order.Order, dest Address, rates RateTable) (Money, error) {
    if len(o.Lines()) == 0 { return Money{}, ErrEmptyOrder }
    …
}
// ★ 需要注入依賴時才用 struct(★ 但依賴必須是「資料」而非「服務」)
type Calculator struct {
    rates RateTable      // ✅ 資料
    now   func() time.Time  // ✅ 時鐘(可測)
    // db  *sql.DB       // ★★ ❌ 有這個就不是領域服務了
}

★★ 「依賴必須是資料而非服務」是關鍵紀律:
★ 一旦領域服務持有 repo 或 http client,它就變成應用服務,而且不再可窮舉測試。
→ ★ 正確做法:由應用服務先把資料查好,再傳給領域服務。

★★ 它最常見的濫用:變成貧血模型的藏身處

// ❌★★ 所有邏輯都在「服務」裡,實體只剩欄位
type OrderService struct{ … }
func (s *OrderService) CanCancel(o Order) bool           // ★★ 這該是 o.CanCancel()
func (s *OrderService) CalculateTotal(o Order) Money     // ★★ 這該是 o.Total()
func (s *OrderService) AddLine(o *Order, l Line) error   // ★★ 這該是 o.AddLine()
// → ★ 這是 [[表格模組]]/[[純資料類別]] 的樣貌,而不是領域服務

★★ 判準:這個操作只需要「一個聚合的資料」嗎?
是 → ★ 它是那個聚合的方法,不是領域服務。
需要多個聚合或外部資料(費率表、匯率)→ 才是領域服務。

★ 一個可操作的檢查:★★ 領域服務的第一個參數是不是一個聚合?
是,而且沒有其他參數 → 那個方法應該搬到聚合上(見 依戀情結)。

★ 命名:不要叫 XxxService

❌ ★ OrderService、PricingService、ShippingService
   → ★★ 「Service」沒有帶來任何資訊,而且它誘使你把所有東西都塞進去
✅ ★ pricing.Quote()、shipping.Router、settlement.Allocator
   → ★ 用「它做什麼」命名(★ 見 [[工具-Go命名慣例]]:★★ package 名已經是前綴)

★★ 「名字叫 Service 的東西會無限成長」——這和 過大類別 的
「名字含糊代表職責不明」是同一條規律。

★ 兄弟辨析

差別
服務層(PoEAA)★ 應用層:交易、授權、DTO;★★ 不同的層
聚合、實體★ 只需要單一聚合的邏輯屬於它們
工廠★ 建立邏輯
規格★ 「判斷」邏輯;領域服務常是「計算」邏輯
閘道★ 基礎設施服務的封裝

🧪 我實際套用的紀錄

  • (待填)

⚠️ 注意 / 什麼時候不適用

  • 別把本該放進實體的行為都倒進服務,導致「貧血模型」(物件只有資料沒有行為)。

  • 領域服務屬於領域層,不要和應用服務(協調用例、交易、對外)混為一談。

  • 領域服務應無狀態;需要保存狀態的是實體或值物件。

  • ★★ 領域服務 ≠ 應用服務(服務層)。 判準:有沒有 ctx/error 以外的 I/O?有 → 它不是領域服務。

  • ★★ 領域服務的依賴必須是「資料」而非「服務」。 持有 repo 或 http client 就變成應用服務,而且不再可窮舉測試。

  • ★★ 判準:這個操作只需要一個聚合的資料嗎? 是 → 它是那個聚合的方法,不是領域服務。

  • ★★ 「領域服務的第一個參數是一個聚合、而且沒有其他參數」→ 那個方法應該搬到聚合上(見 依戀情結)。

  • ★★ 「塞哪裡都怪」也可能是「你漏了一個概念」。 先問有沒有一個沒被命名的值物件。

  • ★ 不要叫 XxxService。 名字含糊代表職責不明,而且它會無限成長。用 pricing.Quote()。

  • ★ 領域服務是貧血模型最常見的藏身處:所有邏輯在「服務」裡,實體只剩欄位。

  • ★ 正確做法是「應用服務先把資料查好,再傳給領域服務」。

🔗 相關工具