🎯 什麼情境該想到我

當你看到「依物件的型別(type code)做 switch / if-else 分支」,而且同樣一組分支在程式各處反覆出現、每加一種新類型就得回頭改好幾個地方的時候。

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

意圖:把「依型別分支的條件邏輯」交給類別繼承與多型來處理。為每一種類型建立子類別,讓各子類別覆寫自己的行為;呼叫端只管呼叫方法,由多型自動分派到正確的實作。加新類型時只要新增一個子類別,不必去動既有的 switch。

小步安全進行:

  1. 建立繼承體系:若還沒有子類別,先做以子類取代型別碼(或用工廠函式建立適當子類)。
  2. 挑一個含 switch 的函式,用函式上移把它搬到超類別。
  3. 在某個子類別中覆寫該函式,把「屬於這個類型的那條分支」搬進來取代 super 的行為。
  4. 逐一為其他子類別重複,每搬一條分支就執行測試。
  5. 所有分支都被子類別覆寫後,把超類別的方法改成預設行為或宣告為抽象。

★★ Go 沒有繼承,所以這個手法要整個重寫

Fowler 的原版是「建子類別 → 函式上移 → 各子類覆寫」。Go 做不到這件事:

type Base struct{}
func (b Base) Name() string  { return "base" }
func (b Base) Greet() string { return "hi, " + b.Name() }   // ★ b 永遠是 Base
 
type Child struct{ Base }
func (c Child) Name() string { return "child" }
 
Child{}.Greet()   // ★★ 回傳 "hi, base" —— 嵌入沒有虛擬分派

→ Go 的嵌入是組合,不是繼承(見 工具-Go嵌入機制)。所以在 Go 裡要用下面三種做法之一。

★ 做法一:介面 + 各自的具體型別(最接近原意)

// before
func Plumage(b Bird) string {
    switch b.Type {
    case "EuropeanSwallow": return "average"
    case "AfricanSwallow":
        if b.NumberOfCoconuts > 2 { return "tired" }
        return "average"
    case "NorwegianBlueParrot":
        if b.Voltage > 100 { return "scorched" }
        return "beautiful"
    }
    return "unknown"
}
// ★ 而且同一組 switch 還出現在 AirSpeedVelocity()、Diet()、…([[重複的 switch]])
 
// after
type Bird interface {
    Plumage() string
    AirSpeedVelocity() int
}
 
type EuropeanSwallow struct{}
func (EuropeanSwallow) Plumage() string { return "average" }
 
type AfricanSwallow struct{ NumberOfCoconuts int }
func (a AfricanSwallow) Plumage() string {
    if a.NumberOfCoconuts > 2 { return "tired" }
    return "average"
}

★ 收益不是「少了一個 switch」,是「每種鳥的資料只出現在它自己的型別上」——
NumberOfCoconuts 不再污染所有鳥共用的 struct。

★ 做法二:map[Kind]func(...) 分派表(最輕,常常就夠了)

type Handler func(Event) error
 
var handlers = map[EventKind]Handler{
    KindCreated: handleCreated,
    KindUpdated: handleUpdated,
    KindDeleted: handleDeleted,
}
 
func Dispatch(e Event) error {
    h, ok := handlers[e.Kind]
    if !ok { return fmt.Errorf("unknown kind %v", e.Kind) }   // ★ 一定要處理找不到的情況
    return h(e)
}

★ 判準:每種型別只有「一個」行為要換 → 分派表;有「一組相關行為」要換 → 介面。
(同 策略模式:單一行為用 func,多行為用 interface。)

★ 做法三:把「共同框架 + 差異」變成「函式欄位」

Go 沒有 Template Method,但可以用欄位注入差異:

type Report struct {
    header func() string           // ★ 差異點變成欄位,不是覆寫方法
    body   func() string
}
func (r Report) Render() string { return r.header() + r.body() + footer() }

⚠️ Go 最大的缺口:沒有 sum type,所以沒有窮盡檢查

switch b.Type {
case "EuropeanSwallow": …
// ★ 漏了 AfricanSwallow —— 編譯器完全不會提醒你
}

Go 的介面是開放的:任何 package 都能新增一個實作,你無法要求「就這三種」。
兩個可用的補償:

// ① 私有方法當封印(sealed interface)—— 外部 package 無法實作
type Bird interface {
    Plumage() string
    isBird()                      // ★ 未匯出方法 → 只有本 package 能實作
}
 
// ② 分派表配上明確的 default,並在 init 或測試裡驗證覆蓋完整
func TestAllKindsHandled(t *testing.T) {
    for _, k := range AllKinds { if _, ok := handlers[k]; !ok { t.Errorf("missing %v", k) } }
}

★ 第②點的測試是實務上最有效的一招——它把「編譯期窮盡檢查」換成「CI 窮盡檢查」。

TypeScript 反而比 Go 強:判別聯集 + never

type Bird =
  | { kind: 'european' }
  | { kind: 'african'; coconuts: number }
  | { kind: 'norwegian'; voltage: number };
 
function plumage(b: Bird): string {
  switch (b.kind) {
    case 'european':  return 'average';
    case 'african':   return b.coconuts > 2 ? 'tired' : 'average';   // ★ 這裡才看得到 coconuts
    case 'norwegian': return b.voltage > 100 ? 'scorched' : 'beautiful';
    default: { const _: never = b; return _; }    // ★★ 漏一個 case → 編譯錯誤
  }
}

★★ 在 TS 裡,這個重構往往不該做。 判別聯集 + never 窮盡檢查
已經解掉了「加型別會漏改」這個原始痛點,而且把所有分支留在一起比散到五個類別更好讀。
見 工具-判別聯集。

🧪 我實際套用的紀錄

  • (待填)

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

  • 只有一兩處簡單分支、且類型很少變動時,直接用條件式更簡單,不必大動干戈引入繼承。

  • 這是較重的手法,會引入類別階層;要確定「依型別變化的行為」確實會反覆出現、值得這個結構。

  • ★★ Go 沒有虛擬分派,嵌入不能模擬覆寫。原書的「建子類 + 函式上移」步驟在 Go 完全不適用。

  • ★★ Go 沒有窮盡檢查。散成五個型別後,「漏實作一種」變成執行期才發現——所以一定要配 default 分支或覆蓋率測試。

  • ★ 在 TypeScript 裡,判別聯集 + never 常常是更好的答案,不要盲目套用這個手法。

  • 只有一處 switch 且分支簡單 → 保留 switch。這是較重的手法,會把一個函式的邏輯散到 N 個型別。判準是「同一組分支是否重複出現在多處」(重複的 switch)。

  • ★ 分散之後「總覽」消失了。原本一眼能看完的五個分支,變成要開五個檔案。這是真實的代價,不是只有好處。

  • 共同部分很多、差異很少時不適用。五個實作有 90% 相同的碼 → 你製造了 重複程式碼。那該用分派表或函式欄位。

  • 不要為了「消滅 switch」而做。switch 本身不是壞味道;重複的、分散的 switch 才是。

🔗 相關工具