🎯 什麼情境該想到我

當你「想用一個類別集中處理某張表的所有列,以資料表為單位組織業務邏輯」的時候。

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

意圖:為資料庫中的每一張表(或視圖)建立一個類別,該類別的單一實例處理該表所有列的業務邏輯。

做法要點:

  • 一個 Table Module 實例對應「一張表」而非「一列」;方法多以列的識別碼為參數。
  • 常搭配 Record Set(記錄集):從資料庫查出資料放進記錄集,Table Module 對其操作。
  • 介於交易指令稿與領域模型之間,能複用邏輯又貼近表格資料結構。
  • 特別適合以資料集 / 記錄集為核心的平台環境。

★★ 誠實的結論:這個模式今天幾乎不用,但它的問題還活著

★ 它的原生環境是 .NET 的 DataSet / ADO.NET(2002 年):
  一個 OrdersModule 物件包住「orders 這張表的所有列」,
  方法收 orderID 參數:module.CalculateTotal(orderID)
→ ★★ 前提是「有一個強大的記錄集([[記錄集]])型別,且 UI 能直接綁定它」
→ 而那個前提在 Go、在現代 Web(JSON API + SPA)都不成立

★★ 所以這一頁的價值不是「教你怎麼用」,而是「認出你不小心寫成了它」。

★★ 你可能已經寫成表格模組了:Go 裡的樣貌

// ★ 這是表格模組 —— 只是沒有人這樣叫它
type OrderService struct{ db *sql.DB }
 
func (s *OrderService) CalculateTotal(ctx context.Context, orderID string) (Money, error)
func (s *OrderService) Cancel(ctx context.Context, orderID string) error
func (s *OrderService) IsEligibleForRefund(ctx context.Context, orderID string) (bool, error)
// ★★ 特徵:每個方法都收 orderID,自己去撈資料、算完就丟

★ 這個寫法的三個具體問題:

① ★★ 每個方法各撈一次資料 ⇒ N 次查詢,而且各自看到的版本可能不同
   IsEligibleForRefund 讀到的訂單狀態,可能和 Cancel 讀到的不一樣
② ★ 不變式無處可放 —— 沒有一個「訂單物件」能守住「已取消不可再取消」
③ ★ 每個方法都要重複「載入 + 檢查存在 + 檢查權限」

★★ 判準:你的「Service」的方法是不是每一個都以 entityID 開頭?
是 → 你在寫表格模組。升級路徑是 領域模型 或 交易指令稿(載入一次、傳物件)。

★ 它唯一還合理的場景:真正的「集合運算」

// ★ 有些操作本質上就是「對一整張表做事」,沒有單一實體可以承載
func (r *ReportQueries) MonthlyRevenueByRegion(ctx context.Context, m Month) ([]RegionRevenue, error) {
    // ★ 一個 GROUP BY 查詢,載入領域物件反而荒謬
}

★★ 這類「報表/批次/統計」的查詢不該經過領域模型——
它們是 CQRS 的讀側:直接寫 SQL、回傳扁平的 DTO。
→ ★ 而這時它不叫表格模組,叫 查詢物件 或「讀模型」。
不要為了「架構一致」而讓報表查詢也載入一萬個聚合。

★ 三種領域邏輯組織方式的取捨(一次看完)

交易指令稿領域模型表格模組
資料與行為★ 分開★ 在一起行為在型別上、資料在記錄集
一個型別負責一個操作一個實體★ 一整張表
適合的複雜度低—中★ 高中(且 UI 綁定記錄集)
★ 今天在 Go★★ 預設選擇複雜領域★★ 幾乎不用
主要風險規則重複過度工程、聚合過大★ N 次查詢、不變式無處放

★ 為什麼它在今天消失了(三個原因)

  1. ★ DataSet 那類「智慧記錄集」在現代技術棧裡不存在(Go 只有 sql.Rows)。
  2. ★ UI 不再直接綁資料表——JSON API + SPA 之間是 DTO,不是記錄集。
  3. ★ ORM 普及讓「載入一個物件」變得便宜,entityID 參數式的寫法失去理由。

🧪 我實際套用的紀錄

  • (待填)

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

  • 因為是「一實例對一表」,難以表達單一列自身的複雜行為與繼承。

  • 業務邏輯極複雜、需要豐富物件關係時,領域模型更合適。

  • 沒有記錄集基礎設施時吸引力下降。

  • ★★ 這個模式今天幾乎不該主動選用。它的前提(智慧記錄集 + UI 直接綁定)在 Go 與現代 Web 都不成立。

  • ★★ 但你可能已經不小心寫成它了:判準是「Service 的每個方法都收 entityID,自己撈資料」。

  • ★ 那個寫法的代價:N 次查詢、各方法看到不同版本、不變式無處放、載入與檢查邏輯重複。

  • ★ 報表/統計/批次查詢不該經過領域模型——那是 CQRS 的讀側,直接寫 SQL 回傳 DTO(查詢物件)。

  • ★ 不要為了「架構一致」讓報表也載入一萬個聚合。

  • 升級路徑是「載入一次、傳物件」:交易指令稿(簡單)或 領域模型(複雜)。

  • 這一頁的用途是「認出反模式」,不是「照著做」。

🔗 相關工具