🎯 什麼情境該想到我
當你「業務邏輯不複雜,想把每個動作直接寫成一段從頭到尾的程序」的時候。
⚙️ 怎麼用(步驟 / 公式)
意圖:把單一業務動作(一次請求 / 一筆交易)組織成一段程序式的指令稿,直接完成驗證、計算、存取資料庫等所有工作。
做法要點:
- 每個業務動作對應一個程序(method / function),從輸入到輸出一條龍處理。
- 可直接寫 SQL 存取資料庫,或透過 Gateway 存取。
- 一個類別可放多個相關指令稿,或一個指令稿一個物件(Command)。
- 特點是直覺、上手快、每個動作彼此獨立。
★ Go 的長相:這其實是 Go 社群的預設寫法
// ★ 一個業務操作 = 一個函式,從頭到尾看得完
func (s *Service) PlaceOrder(ctx context.Context, req PlaceOrderRequest) (OrderID, error) {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil { return "", fmt.Errorf("begin: %w", err) }
defer tx.Rollback() // ★ 已 commit 後 Rollback 回 ErrTxDone,無害
stock, err := s.q.WithTx(tx).GetStockForUpdate(ctx, req.SKU) // ★ SELECT … FOR UPDATE
if err != nil { return "", fmt.Errorf("get stock: %w", err) }
if stock.Qty < req.Qty { return "", ErrInsufficientStock }
if err := s.q.WithTx(tx).DecStock(ctx, req.SKU, req.Qty); err != nil { … }
id, err := s.q.WithTx(tx).InsertOrder(ctx, …)
if err != nil { … }
return id, tx.Commit()
}★★ 這就是交易指令稿,而且它在 Go 裡通常是對的選擇。
Go 沒有繼承、沒有 ORM 文化、標準庫的database/sql是明確的交易 + 明確的查詢
——整個語言的紋理都偏向「一個請求一個函式,從上讀到下」。
★★ 它的真正優點常被低估
| 優點 | 說明 |
|---|---|
| ★ 一個操作的全貌在一個地方 | 不必在五個類別間跳躍就能回答「下單到底做了什麼」 |
| ★ 交易邊界顯而易見 | BeginTx 到 Commit 之間就是交易,不像 ORM 的 Unit of Work 隱形 |
| ★ 效能可預測 | 沒有延遲載入的 N+1、沒有你沒寫的查詢 |
| ★ 新人上手快 | 不需要先學一套物件協作的約定 |
| ★ 好測 | 只要能給 tx,整個函式就能測 |
★★ 什麼時候它會撐不住:三個可觀察的訊號
① ★★ 同一段業務規則出現在三個以上的指令稿裡
「VIP 折扣」在 PlaceOrder、Requote、Renew 各寫一次,而且已經不一致了
→ ★ 這是最可靠的訊號([[重複程式碼]])
② ★ 單一指令稿超過 200 行、或嵌套超過三層
→ ★ 先試 [[提煉函式]];抽出來的還是「程序」就還好,
★ 但如果抽出來的東西一直需要同一組資料 → 那是 [[領域模型]] 在敲門
③ ★★ 你開始寫「if 狀態 == A 且 狀態 != B 且 …」的組合檢查
→ 狀態機的不變式散落在多個指令稿 ⇒ ★ 一定會有一個漏檢查
★ 判準(Fowler 自己的說法):業務邏輯的複雜度。
規則少而穩 → 交易指令稿;規則多、互相牽連、常變 → 領域模型。
★ 而「複雜度」的操作化定義就是上面三個訊號。
★ 演進路徑:不必一次改成領域模型
第 1 步 ★ 把重複的規則抽成純函式(★ 吃參數、回傳值、不碰 DB)
func vipDiscount(tier Tier, subtotal Money) Money
→ ★ 這一步就解掉 90% 的「規則不一致」問題,成本極低
第 2 步 ★ 純函式群開始需要同一組資料 → 讓它們變成一個型別的方法
→ ★ 這時你已經有一個 [[值物件]] 或小型 [[領域模型]] 了
第 3 步 ★ 只有在「不變式必須被強制」時,才引入完整的聚合與 [[資料對應器]]
★★ 關鍵:不要因為「交易指令稿是最低階的模式」就急著升級。
多數服務一輩子停在第 1、2 步就是最佳解。
★ 兄弟辨析
| 差別 | |
|---|---|
| 領域模型 | ★ 資料與行為在一起;交易指令稿是資料(列)+程序(函式)分開 |
| 表格模組 | ★ 一個型別處理「一整張表」的所有列(.NET DataSet 式);★ 今天幾乎不用 |
| 服務層 | ★ 不是替代關係:服務層是「對外的操作邊界」,內部可以是交易指令稿 |
🧪 我實際套用的紀錄
- (待填)
⚠️ 注意 / 什麼時候不適用
-
業務變複雜時,指令稿之間會出現重複邏輯,難以維護與擴充。
-
邏輯量大、規則多變、共用行為多時,改用領域模型。
-
隨系統成長,重構成本會升高。
-
★ 這不是「初階模式」。Go 生態裡它常常是正確答案,多數服務一輩子不需要 領域模型。
-
★ 撐不住的訊號是「同一規則出現在三個以上指令稿且已不一致」,不是「行數太多」。
-
★ 第一步永遠是「把重複規則抽成純函式」,成本極低、收益最大。不要跳到完整領域模型。
-
★ 交易邊界要顯式而且只有一層。指令稿裡再開一個
BeginTx就是巢狀交易,database/sql不支援(要用 savepoint)。 -
★
defer tx.Rollback()是慣用寫法:commit 後再 Rollback 回傳sql.ErrTxDone,無害。漏了它才會在早期 return 時洩漏連線。 -
不要把 HTTP 的東西帶進指令稿(
*http.Request、ResponseWriter)。指令稿吃 command struct、回傳結果(見 服務層)。 -
狀態機的不變式散在多個指令稿 ⇒ 一定有一個漏檢查。 這是最該升級的情況。
-
指令稿之間互相呼叫要小心交易傳遞:被呼叫者不該自己開交易,要收
tx。
🔗 相關工具
- 領域模型 Domain Model(業務複雜時的替代方案)
- 表格模組 Table Module(介於兩者之間,以表為單位組織邏輯)
- 企業應用架構模式
- 領域模型 —— 規則複雜時的升級路徑
- 表格模組 —— 另一種組織方式(★ 今天幾乎不用)
- 服務層 —— 對外邊界,兩者可並存
- 值物件 —— 演進第二步的落點
- 工具-領域邏輯組織模式 —— 三種組織方式的選擇
- 工具-交易與隔離等級 —— 交易邊界與鎖