🎯 什麼情境該想到我
當你「一個類別已經沒剩多少責任、不再值得獨立存在」的時候(常見於重構後被逐步掏空的類別)——把它併回最常使用它的那個類別。也常用在「想把兩個類別的職責打散重分」時,先合再拆。
⚙️ 怎麼用(步驟 / 公式)
意圖:一個類別責任所剩無幾時,把它併回最常用它的另一個類別。
動作要點(小步安全進行):
- 選定「吸收方」類別(通常是最常使用來源類別的那個)。
- 在吸收方類別上,為來源類別的每個公開方法建立對應的(委託)方法,先轉呼叫過去。
- 把所有對來源類別的呼叫端,改成呼叫吸收方類別上的這些方法,測試。
- 用「搬移欄位」「搬移函式」把來源類別的欄位與方法逐一搬進吸收方類別。
- 全部搬完後,刪除已無內容的來源類別。
★ before / after(Go)
// before:TrackingInformation 已經沒什麼事做了,只是多一層
type TrackingInformation struct {
ShippingCompany string
TrackingNumber string
}
func (t TrackingInformation) Display() string { return t.ShippingCompany + ": " + t.TrackingNumber }
type Shipment struct{ tracking TrackingInformation }
func (s Shipment) TrackingInfo() string { return s.tracking.Display() } // ★ 純轉發
// after:欄位收回 Shipment,中間層消失
type Shipment struct {
ShippingCompany string
TrackingNumber string
}
func (s Shipment) TrackingInfo() string { return s.ShippingCompany + ": " + s.TrackingNumber }什麼時候該內聯(三個訊號)
| 訊號 | 說明 |
|---|---|
| 只剩轉發 | 型別的每個方法都只是 return s.inner.X() → 這就是 中間人 |
| 只有一個使用者,且沒有自己的不變式 | 一個 struct 只被一個型別持有、欄位可以是任何值 → 沒有存在價值 |
| ★ 要重新切線 | 兩個型別的職責都劃錯了 → 先全部合回去,再重新 提煉類別。 這是最重要的用法 |
★ Go 的版本:真正該內聯的通常是「單方法介面 + 單一實作」
// ❌ 很常見的過度抽象
type UserRepository interface { // ★ 只有一個實作,測試也沒在替換
FindByID(ctx context.Context, id string) (*User, error)
}
type postgresUserRepository struct{ db *sql.DB }
// ✅ 內聯掉介面,直接用具體型別
type UserStore struct{ db *sql.DB }
func (s *UserStore) FindByID(ctx context.Context, id string) (*User, error) { … }★ Go 的介面該定義在消費方(
accept interfaces, return structs)。
為每個 struct 配一個同名介面(Service+IService)在 Go 沒有任何好處
——不會更解耦,只多一層間接和一個要同步維護的檔案。見 工具-針對介面編程。
判準:現在有幾個實作?只有一個,而且測試不需要替換 → 內聯掉介面。
真的要在測試裡替換 → 介面寫在使用它的那個 package,只放用到的方法。
⚠️ Go 特有:內聯型別會改變 JSON / DB 形狀
// before
{"tracking": {"company": "FedEx", "number": "123"}}
// after ★ 變成扁平的,前端/下游全部要改
{"company": "FedEx", "number": "123"}對外 API 的形狀是契約。 內部要內聯,但對外不能變 → 保留一個獨立的 DTO 型別做序列化,
內部模型維持內聯後的樣子。別讓「內部結構」和「線上格式」共用同一個 struct。
機械化步驟
- 在吸收方(
Shipment)建立被吸收方的所有欄位與方法,先轉發給舊型別。 - 把所有對舊型別的引用改成呼叫吸收方(編譯器會列出每一處)。
- 舊型別零引用後刪掉(
go build ./...確認)。 - 每步都跑測試。
🧪 我實際套用的紀錄
- (待填)
⚠️ 注意 / 什麼時候不適用
-
若來源類別仍有清楚且值得獨立的職責,就別內聯,反而該保留或做提煉類別 Extract Class。
-
反向手法:提煉類別 Extract Class。
-
★ 不要因為「型別很小」就內聯。判準是「它有沒有自己的不變式與行為」。
Money、Email、DateRange很小但價值很高(見 以物件取代基本型別)。 -
★ 內聯之後欄位會變多。如果吸收方本來就接近 過大類別,內聯只是把問題搬過去——先確認吸收方撐得住。
-
★ 對外 JSON/DB schema 會變形,而且編譯器不會提醒。用獨立的 DTO 隔開內部模型與線上格式。
-
被多個型別使用的型別不要內聯——會變成 重複程式碼。
-
「先全部合回去再重切」要在一個 branch 內做完。合回去的中間狀態是比原本更糟的大型別,不要停在那裡。
🔗 相關工具
- 提煉類別 Extract Class(本手法的反向操作)
- 行內函式 Inline Function(同屬「去掉不必要間接層」的思路)
- 重構
- 提煉類別 —— 反向操作;「合回去再重切」的下一步
- 中間人 —— 只剩轉發這個壞味道
- 行內函式 —— 函式層級的同一件事
- 冗員元素 —— 沒有存在價值的型別
- 工具-針對介面編程 —— Go 的介面該定義在哪一邊
- 工具-Go介面小而隱式 —— 介面污染與只有一個實作的判斷