🎯 什麼情境該想到我

當你「一個請求要依序交給多個處理者,直到有人處理它(或有人擋下它)」的時候——
審批流程、多層驗證、事件冒泡、middleware 的擋接、客服分級、日誌分級。

⚙️ 怎麼用

意圖

讓多個物件都有機會處理請求,從而避免請求的發送者與接收者之間的耦合。把這些物件連成一條鏈,並沿著鏈傳遞請求,直到有物件處理它為止。

Request ──▶ HandlerA ──▶ HandlerB ──▶ HandlerC ──▶ (沒人處理)
              │            │
           能處理就         ★ 任一環都可以「停下來不往下傳」
           處理並停止

★ 和 Decorator 的關鍵差別(最常被問)

結構幾乎一樣,差在「能不能中斷」:

責任鏈裝飾者模式
每一環的職責★ 決定「我處理,還是往下傳」一定會往下傳(只是前後加料)
可以中斷嗎★ 可以,而且這是重點不應該(會破壞語義)
最終誰處理鏈上的某一個(或都不處理)★ 一定是最內層的那個
語義「找到能處理的人」「層層包裝同一個處理」

★ 一句話:Decorator 是「所有人都參與」,責任鏈是「只有一個人(或沒人)真正處理」。

⚠️ 實務上的 middleware 兩者兼具:認證 middleware 擋下未授權請求(責任鏈的行為),
log middleware 前後加料(裝飾者的行為)。不用為了分類糾結——
重點是設計時要明確:這一環有沒有權力中斷?

★ Go 的長相

① 顯式鏈(經典版)

type Approver interface {
    Approve(ctx context.Context, req Request) (Decision, error)
    SetNext(Approver)
}
 
type manager struct{ next Approver }
 
func (m *manager) SetNext(n Approver) { m.next = n }
func (m *manager) Approve(ctx context.Context, req Request) (Decision, error) {
    if req.Amount <= 10_000 {
        return Decision{By: "manager", OK: true}, nil     // ★ 我處理,鏈到此為止
    }
    if m.next == nil {
        return Decision{}, ErrNoApprover                  // ★ 鏈末仍無人處理
    }
    return m.next.Approve(ctx, req)                       // ★ 往下傳
}

② 切片版(★ Go 更常用,順序一目了然)

type Rule func(ctx context.Context, req Request) (Decision, bool)   // bool = 我處理了嗎
 
func Chain(rules ...Rule) func(context.Context, Request) (Decision, error) {
    return func(ctx context.Context, req Request) (Decision, error) {
        for _, r := range rules {
            if d, handled := r(ctx, req); handled {
                return d, nil                              // ★ 有人處理就停
            }
        }
        return Decision{}, ErrNoApprover
    }
}
 
approve := Chain(autoApproveSmall, managerRule, directorRule, ceoRule)

★ 切片版勝過「每個 handler 持有 next 指標」的版本:
整條鏈的順序在一個地方看得到,而不是散落在各個 handler 的 SetNext 呼叫裡。
這和 狀態模式 選「轉換表」而非「狀態物件」是同一個理由。

③ middleware 版(可中斷)

func RequireAuth(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        if !authorized(r) {
            http.Error(w, "forbidden", http.StatusForbidden)
            return                                         // ★ 不呼叫 next —— 鏈中斷
        }
        next.ServeHTTP(w, r)
    })
}

真實案例

案例鏈
HTTP middlewareauth → rate limit → 業務 handler
DOM 事件冒泡★ 子元素 → 父元素 → document,stopPropagation() 就是中斷
日誌分級DEBUG handler → INFO handler → ERROR handler
審批流組長 → 經理 → 總監 → CEO(依金額)
例外處理★ 語言的 try/catch 本質上就是一條責任鏈
Servlet Filter / Express middleware同 HTTP middleware

TypeScript 的長相

type Rule = (req: Request) => Decision | undefined;   // undefined = 我不處理
 
const chain = (...rules: Rule[]) => (req: Request): Decision => {
  for (const r of rules) {
    const d = r(req);
    if (d) return d;
  }
  throw new Error("no approver");
};

★ Decision | undefined 比 (Decision, bool) 更自然——TS 用可選型別表達「我不處理」。
搭配 strictNullChecks(見 工具-tsconfig基準設定),編譯器會逼你處理 undefined 的情況。

🧪 我實際套用的紀錄

  • (待填)

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

  • ★ 最大的風險:沒有人處理,而且沒人發現。
    請求默默地走完整條鏈然後消失。
    → 鏈末一定要有一個明確的行為:回錯誤、丟到 dead letter、或一個保證處理的預設 handler。
    不要讓「沒人處理」變成靜默的 no-op。
  • ★ 順序就是語義,而且很容易錯。
    認證要在限流之前還是之後?(★ 限流應該在認證之後,否則未認證的請求也消耗配額;
    但若要防阻斷攻擊,廉價的 IP 級限流該在最前面——兩層都要。)
    → 把順序的理由寫成註解,並且測試它。
  • 除錯困難:「為什麼我的請求沒被處理」要沿著整條鏈追。
    → 每一環都 log「我處理了 / 我往下傳了」,並帶 trace id。
  • 效能:每個請求都要走過整條鏈。鏈很長且多數請求落在末端時,考慮用查表直接路由
    (就像 工廠方法模式 的註冊表取代長 switch)。
  • 不要做成雙向或有分支。責任鏈是線性的;需要分支就用明確的路由或狀態機。
  • ★ 只有兩三個固定的檢查 → 直接寫 if 就好。 責任鏈的價值在「環是可插拔、順序可調、數量會變」。
  • 鏈的建構要集中。散落在各處的 SetNext 會讓實際順序無法被讀出來。

🔗 相關工具