🎯 什麼情境該想到我

當你看到「好幾個各自獨立的條件判斷,但它們的結果/動作其實完全一樣」的時候——一連串 if 各自 return 相同的東西,讀起來像是好多個檢查,其實是同一件事。

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

意圖:把一串「導向相同結果」的條件判斷,用邏輯運算子(&&、||)合併成單一條件,再提煉成一個有名字的函式。這樣做能明白宣告「這其實是一個判斷」,而不是好幾個彼此無關的檢查。

小步安全進行:

  1. 確認這些條件式都沒有副作用(純判斷);有副作用要先處理。
  2. 用 ||(其中之一成立)或 &&(全部成立)把相鄰、結果相同的條件合併成一個判斷式。
  3. 執行測試。
  4. 對合併後的條件施以提煉函式,取一個說明「為什麼」的名字(如 isNotEligibleForDisability())。

★ before / after(Go)

// before:三個條件、同一個結果 —— 讀者要讀三次才知道它們其實是一件事
func disabilityAmount(e Employee) float64 {
    if e.Seniority < 2 { return 0 }
    if e.MonthsDisabled > 12 { return 0 }
    if e.IsPartTime { return 0 }
    …
}
 
// after:一個條件 + 一個名字
func disabilityAmount(e Employee) float64 {
    if isNotEligibleForDisability(e) { return 0 }
    …
}
 
func isNotEligibleForDisability(e Employee) bool {
    return e.Seniority < 2 || e.MonthsDisabled > 12 || e.IsPartTime
}

★ 合併本身只是第一步,價值在「現在這串條件有一個名字了」——
也就是接著做 分解條件式/提煉函式。合併完停在 if a || b || c 沒有太大收益。

★ 前提檢查:這三個條件真的等價嗎

唯一的判準:它們的「後果」完全一樣。

if e.Seniority < 2   { return 0 }
if e.MonthsDisabled > 12 { return 0 }
if e.IsPartTime      { log.Warn("part-time"); return 0 }   // ★ 多了一行!不能合併

差一行日誌、差一個錯誤訊息、差一個 metric —— 就不是同一件事。
硬合併會讓你丟掉那個差異,而且通常沒有測試會發現。

★★ Go 的關鍵陷阱:合併會改變求值次數與順序

// before:後面的條件只在前面為假時才求值
if u == nil          { return errNoUser }
if !u.Active         { return errInactive }
if u.Quota() <= 0    { return errNoQuota }     // ★ Quota() 是一次 DB 查詢
 
// after ★★ 壞了兩次
if u == nil || !u.Active || u.Quota() <= 0 { return errBad }
//    ↑ 這裡還是短路的(|| 也短路),所以 nil 安全
//                                    ↑ 但三個不同的錯誤變成一個 —— ★ 呼叫端分不清原因了

兩件事要分開看:

  • && 和 || 在 Go 都是短路的 → nil 保護不會壞。
  • ★ 但「不同的後果」被合併掉了。回傳不同 error 的條件不能合併,這是實務上最常見的誤用。

★ 兩種合併:|| 與 &&

// || —— 「任何一個成立就走這條」(同一結果的多個前提)
if a || b || c { return 0 }
 
// && —— 「巢狀的 if 每層都沒有 else」
if a { if b { if c { doIt() } } }      // before
if a && b && c { doIt() }              // after ★ 純 && 的嵌套一律該扁平化

★ && 這一種是最沒有爭議的重構——巢狀三層卻沒有任何 else,扁平化後絕對更好讀。

⚠️ 什麼時候反而該拆開(反向操作)

// ★ 條件太長、或每個條件需要各自的錯誤訊息時,衛述句更好
if u == nil       { return fmt.Errorf("user required") }
if !u.Active      { return fmt.Errorf("user %s inactive", u.ID) }
if u.Quota() <= 0 { return fmt.Errorf("user %s out of quota", u.ID) }

Go 的慣例偏向這一種(見 以衛述句取代嵌套條件式):
每個前提一行、各自的錯誤訊息、加新前提不用碰既有的行。
→ ★ 判準:呼叫端需要區分失敗原因嗎?需要 → 不要合併。

TypeScript

// ★ 別把 ?? 和 || 搞混 —— 合併條件時很容易誤用
const n = count || 10;    // ★ count 為 0 時給 10(0 是 falsy)
const n = count ?? 10;    // ✅ 只有 null/undefined 才給 10

🧪 我實際套用的紀錄

  • (待填)

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

  • 若這些條件本來就代表「彼此獨立、只是碰巧結果相同」的不同判斷,合併會抹掉它們的差異,反而誤導——此時不該合併。

  • 條件式帶有副作用時,合併可能改變執行行為,需先移除副作用。

  • ★★ 後果不同就不能合併。回傳不同 error、記不同 log、發不同 metric 的條件是三件事,不是一件事。這是最常見的誤用。

  • ★ Go 的慣例常常偏向不合併:一連串衛述句、各自的錯誤訊息,比一個長 || 更符合 Go 的錯誤處理風格。

  • 合併後的條件超過三項就該提煉成具名函式,否則 if a || b || c || d || e 只是換一種難讀。

  • 混用 && 和 || 要加括號。Go 的 && 優先於 ||,但依賴優先序的程式碼 review 時沒人敢改。

  • 純 && 的巢狀(沒有 else)扁平化幾乎總是對的——這是本手法最安全的用法。

  • TS:|| 對 0 / '' / false 也會走 fallback,該用 ?? 的地方用 || 是靜默的 bug。

🔗 相關工具