🎯 什麼情境該想到我

當你「有一個一般的服務(普通方法),想把它接到訊息通道,讓它既能被訊息觸發、也能照常被程式直接呼叫」的時候。

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

意圖:在訊息通道與一個服務之間搭一座橋,把到達的訊息轉成對服務的呼叫,使服務可被訊息驅動,同時仍能支援非訊息(直接)呼叫。

做法 / 結構要點:

  • Service Activator 監聽通道、收訊息。
  • 把訊息內容解析成服務方法的參數並呼叫服務。
  • 若需回覆,把服務結果包成訊息送回。服務本身不需知道自己被訊息呼叫。

★ 核心價值:業務邏輯只寫一次,入口可以有很多個

HTTP handler ──┐
gRPC handler ──┤
訊息消費者    ──┼──▶ ★ 同一個 Service.Do(ctx, cmd) ──▶ 領域邏輯
CLI 指令      ──┤
排程器        ──┘
// ★ 業務層:不知道自己被誰呼叫
package fulfilment
type Service struct{ repo Repo; pub Publisher }
func (s *Service) Dispatch(ctx context.Context, cmd DispatchCommand) (Result, error) { … }
 
// ★ 各個「啟動器」:只負責把外界的形狀轉成 cmd
package httpapi
func (h *Handler) Dispatch(w http.ResponseWriter, r *http.Request) {
    cmd, err := decodeCommand(r)                     // ★ HTTP 特有
    if err != nil { http.Error(w, …); return }
    res, err := h.svc.Dispatch(r.Context(), cmd)
    writeJSON(w, res, err)                           // ★ HTTP 特有
}
 
package msgapi
func (a *Activator) Handle(ctx context.Context, msg Message) error {
    cmd, err := toCommand(msg)                       // ★ 訊息特有
    if err != nil { return fmt.Errorf("%w: %v", ErrInvalidMessage, err) }
    _, err = a.svc.Dispatch(ctx, cmd)
    return err                                        // ★ 由消費迴圈決定 ack/nack
}

★★ 這個模式最大的價值:測試策略

★ 業務邏輯的測試:★★ 直接呼叫 svc.Dispatch(ctx, cmd) —— 不需要 HTTP、不需要 broker
★ 啟動器的測試:  只測「轉換」與「錯誤對應」,量少且簡單
→ ★ 這讓「業務邏輯的測試」數量多、跑得快;「整合測試」數量少、只驗管線
   (見 [[測試金字塔]])

★★ 反過來說,沒有這個模式的症狀很明顯:
業務邏輯寫在 HTTP handler 裡 → 要測一條業務規則就得起一個 HTTP server、
組一個 request;而同一條規則從訊息進來時,你只能複製一份程式碼。

★★ 但兩個入口的語義差異必須明確處理

HTTP/gRPC訊息
重複★ 呼叫端重試才會重複★★ 一定會重複(至少一次)
錯誤處理回 4xx/5xx 給呼叫端★ 決定 ack/nack/DLQ
超時呼叫端會放棄★ 沒人在等,超時是自己設的
併發由入站流量決定★ 由 prefetch/worker 數決定
授權★ 有使用者身分(JWT/session)★★ 沒有使用者身分——只有訊息

★★ 最容易出錯的是「授權」這一列:
HTTP 路徑上有「這個使用者能不能做這件事」的檢查,
而訊息路徑上沒有 → ★ 如果檢查寫在 HTTP handler 裡,訊息路徑就繞過了它。
★ 對策:授權檢查要在業務層(cmd 裡帶 actor),不在 handler 裡。

★ 第二容易出錯的是冪等:

// ★ 冪等該放哪?
// ① 放業務層 → ★ 兩個入口都受保護,但 HTTP 也要傳 idempotency key
// ② 放訊息啟動器 → ★ HTTP 路徑沒有保護
// ★★ 判準:這個操作從 HTTP 進來也可能被重試嗎?會 → 放業務層

★ 什麼時候不需要這個模式

  • ★ 只有一個入口,而且不會增加 → 直接寫在 handler 裡更簡單。
    ⚠️ 但「之後要加訊息入口」的機率不低,而事後拆很貴。
  • ★ 兩個入口的語義差異太大(例如訊息路徑是批次、HTTP 是單筆)
    → 硬共用會讓 cmd 長出 IsBatch bool 這種欄位(見 移除旗標參數)。

★ 兄弟辨析

差別
訊息閘道★ 封裝「往外發」與 broker API;服務啟動器是「從外進來」
通道配接器★ 在應用程式外面;服務啟動器在裡面
訊息對應器服務啟動器內部用它做型別轉換
服務層★ PoEAA 的同一想法(業務邏輯的統一入口)
遠端外觀粗粒度的遠端介面

🧪 我實際套用的紀錄

  • (待填)

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

  • 服務要能同時服務訊息與非訊息呼叫,本體須維持與傳輸無關。

  • 錯誤 / 回覆處理要在 Activator 一層做好。

  • 常與訊息閘道對稱:一個負責送出、一個負責接入觸發。

  • ★★ 授權檢查要在業務層(cmd 帶 actor),不要寫在 HTTP handler 裡——否則訊息路徑會繞過它。這是這個模式最危險的漏洞。

  • ★★ 冪等的位置取決於「HTTP 路徑會不會也被重試」。會 → 放業務層,且 HTTP 也要傳 idempotency key。

  • ★ 兩個入口的語義差異必須明確列出並處理:重複、錯誤處理、超時、併發、授權。

  • ★ 業務邏輯寫在 handler 裡的症狀:要測一條規則得起 HTTP server;同一規則從訊息進來只能複製程式碼。

  • ★ 兩個入口語義差異太大時不要硬共用。cmd 長出 IsBatch bool 就是訊號。

  • 錯誤型別要能被兩邊翻譯:業務層回 ErrNotFound,HTTP 譯成 404、訊息路徑譯成「永久錯誤 → DLQ」。

  • 只有一個入口時這是過度設計——但事後拆很貴,值得權衡。

🔗 相關工具