🎯 什麼情境該想到我
當你發現「好幾個子類別各自有一份內容相同(或幾乎相同)的方法」的時候——改動一處就得記得同步其他每一份,重複散落在繼承體系裡。
⚙️ 怎麼用(步驟 / 公式)
意圖:把各子類別重複的方法上移到共同父類別,消除重複。以後只需維護一份,邏輯也集中在一處。相關的還有「欄位上移」與「建構式本體上移」,一併把共通的資料與初始化搬上去。
小步安全進行:
- 檢查各子類別中這些方法是否真的完全等價;若只是相似,先重構到彼此一致(統一參數、統一其引用的欄位名)。
- 若方法引用了子類別才有的欄位或方法,先把那些欄位/方法一併上移,或在父類別宣告為抽象。
- 把其中一份方法搬進父類別。
- 執行測試。
- 逐一刪除其他子類別中重複的那份方法,每刪一個就測一次。
★★ Go 沒有超類別,所以這個手法對應「搬進共用的地方」
原書:「兩個子類有一模一樣的方法 → 搬到超類去。」
Go 沒有 extends,所以「上移」在 Go 裡是這三件事之一:
// before:兩個型別有字面上完全相同的方法
type Employee struct{ name string }
func (e Employee) AnnualCost() float64 { return e.monthlyCost * 12 }
type Contractor struct{ name string }
func (c Contractor) AnnualCost() float64 { return c.monthlyCost * 12 } // ★ 一模一樣① 搬進嵌入的共用 struct(最接近原意)
type monthlyCosted struct{ monthlyCost float64 }
func (m monthlyCosted) AnnualCost() float64 { return m.monthlyCost * 12 } // ★ 只有一份
type Employee struct{ monthlyCosted; name string }
type Contractor struct{ monthlyCosted; name string }
e.AnnualCost() // ★ 自動轉發到嵌入的 monthlyCosted② ★ 變成一個自由函式(Go 最常見、也常是最好的答案)
func AnnualCost(monthlyCost float64) float64 { return monthlyCost * 12 }★ Go 不強迫邏輯掛在型別上。 如果那個方法只是在算一個純函式,
一個 package-level 函式比任何繼承結構都清楚——而且可以直接測試。
③ 變成介面的預設行為?→ ★ Go 做不到
type Costed interface {
AnnualCost() float64 // ★ 介面只能放簽章,不能放實作
}Go 的介面沒有 default method(Java 8 有、C# 有)。
每個實作都得自己寫——這正是嵌入存在的理由(方案①)。
★★ 硬前提:方法必須「字面上完全等價」
原書要求兩個方法一模一樣,Go 更嚴格,因為還要檢查這些:
// ① 接收者的值/指標形式必須一致
func (e Employee) AnnualCost() float64 // ★ 值接收者
func (c *Contractor) AnnualCost() float64 // ★ 指標接收者 —— 方法集不同,不能直接上移
// ② 用到的欄位必須也一起搬上去
func (e Employee) AnnualCost() float64 { return e.monthlyCost * 12 }
// ↑ ★ monthlyCost 必須先做 [[搬移欄位]]
// ③ 方法內若呼叫了「各自不同」的方法 —— ★★ 這是最大的地雷
func (m monthlyCosted) Total() float64 { return m.AnnualCost() + m.Tax() }
// ↑ ★ 永遠是 monthlyCosted.Tax()
// 不會分派到 Employee.Tax()★★ 第 ③ 點是從 Java 轉 Go 最容易踩的坑:
原書的「函式上移」常常搭配「留一個抽象方法給子類實作」(即 範本方法模式),
那個模式在 Go 完全不成立。要表達「骨架共用、細節不同」,
必須把差異點變成函式欄位或介面參數注入進去。
★ 兩個方法「幾乎相同」時怎麼辦
func (e Employee) AnnualCost() float64 { return e.monthlyCost * 12 }
func (c Contractor) AnnualCost() float64 { return c.monthlyCost * 12 * 1.1 } // ★ 差一個係數先讓它們變成字面相同,再上移:
- 把差異參數化:
func annualCost(monthly, multiplier float64) float64。 - 兩邊都改成呼叫它(各自傳不同的 multiplier)。
- 這時兩個方法只剩
return annualCost(x.monthlyCost, 1.0)—— 差異在資料而非程式碼了。 - 差異變成欄位後,方法就真的可以共用。
★ 這個「先參數化再上移」的順序是關鍵——直接上移一個「幾乎相同」的方法,
你會忍不住在裡面加 if,那就製造了 以多型取代條件式 要消滅的東西。
⚠️ Go 的嵌入代價(決定要不要用方案①)
- 嵌入型別的匯出方法會全部出現在外層——
monthlyCosted之後新增方法,Employee的 API 自動變大。
→ 讓共用 struct 未匯出(小寫),把它當純實作細節。 - 嵌入欄位的名字就是型別名:
e.monthlyCosted是可存取的(未匯出時僅限同 package)。 - 兩個嵌入型別有同名方法 → 呼叫時編譯錯誤(歧義),必須明確指定。
- JSON:未匯出的嵌入不會被序列化;匯出的嵌入會讓欄位變扁平(見 提煉類別 的實測)。
🧪 我實際套用的紀錄
- (待填)
⚠️ 注意 / 什麼時候不適用
-
若各子類別的方法只是「看起來像」但語意其實不同,上移會強行綁死本該獨立演化的邏輯——不要為了消重複而合併不同概念。
-
方法依賴大量子類別專屬狀態時,上移前得先處理那些依賴,成本可能不划算。
-
★★ Go 的嵌入沒有虛擬分派。共用方法內呼叫的方法永遠是共用 struct 自己的,不會分派到外層型別。原書「上移骨架、留抽象方法給子類」的做法在 Go 不成立。
-
★ 介面不能有預設實作。Go 沒有 default method,共用實作只能靠嵌入或自由函式。
-
★ 「幾乎相同」要先參數化再上移,不要上移後在裡面加
if。 -
接收者的值/指標形式不一致就不能直接上移(方法集不同)。
-
用到的欄位要先 搬移欄位,否則共用方法拿不到資料。
-
★ 巧合相同不要上移。兩個
AnnualCost()剛好都是× 12,但一個是薪資、一個是租金——它們會各自演化。見 工具-DRY原則。 -
共用 struct 要未匯出,否則它的方法會擴散到所有嵌入它的型別的公開 API。