🎯 什麼情境該想到我
當你看到「依物件的型別(type code)做 switch / if-else 分支」,而且同樣一組分支在程式各處反覆出現、每加一種新類型就得回頭改好幾個地方的時候。
⚙️ 怎麼用(步驟 / 公式)
意圖:把「依型別分支的條件邏輯」交給類別繼承與多型來處理。為每一種類型建立子類別,讓各子類別覆寫自己的行為;呼叫端只管呼叫方法,由多型自動分派到正確的實作。加新類型時只要新增一個子類別,不必去動既有的 switch。
小步安全進行:
- 建立繼承體系:若還沒有子類別,先做以子類取代型別碼(或用工廠函式建立適當子類)。
- 挑一個含 switch 的函式,用函式上移把它搬到超類別。
- 在某個子類別中覆寫該函式,把「屬於這個類型的那條分支」搬進來取代 super 的行為。
- 逐一為其他子類別重複,每搬一條分支就執行測試。
- 所有分支都被子類別覆寫後,把超類別的方法改成預設行為或宣告為抽象。
★★ 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 才是。
🔗 相關工具
- 以子類取代型別碼 Replace Type Code with Subclasses(常是本手法的前置步驟:先有子類才能談多型)
- 函式上移 Pull Up Method(把共同框架搬到超類別,再讓子類覆寫差異)
- 分解條件式 Decompose Conditional(分支不多時,先分解可能就夠了)
- 重構
- 以子類取代型別碼 —— 原書的前置步驟(Go 裡要改成「以介面取代型別碼」)
- 重複的 switch —— 觸發本手法的壞味道;只有一處 switch 就不要做
- 策略模式 —— 單一行為用 func、多行為用 interface 的同一判準
- 狀態模式 —— 分支依「狀態」而非「型別」時
- 引入特例 —— 分支只是在處理 null/特例時的輕量解
- 工具-判別聯集 —— ★ TS 的窮盡檢查
- 工具-Go嵌入機制 —— 為什麼 Go 的嵌入不能模擬覆寫