🎯 什麼情境該想到我
當你「一個類別塞了太多欄位、扛了太多責任,什麼事都往它身上放」的時候——它試圖做太多事,難免出現重複與混亂。
⚙️ 怎麼用(步驟 / 公式)
這味道是什麼徵兆:欄位一多,類別幾乎必然承擔了多個職責,內部也容易長出重複碼。一個好用的觀察法:看欄位(或方法)名字有沒有共同前綴/後綴,那常是可以分出去的一群。
通常用哪些重構手法對治:
- 提煉類別(Extract Class):把一群相關的欄位與行為抽成獨立類別。
- 提煉超類(Extract Superclass):把共通部分上提到父類。
- 以子類取代型別碼(Replace Type Code with Subclasses):若類別因某個型別欄位而分支,用子類拆解。
先找出「常一起被用到」的欄位群,它們是最自然的切分線。
★★ Go 沒有 class,所以這個味道有三種化身
| 化身 | 樣貌 | 對治 |
|---|---|---|
| ① 巨大的 struct | 三十個欄位,每個使用者只碰其中三個 | 提煉類別、搬移欄位 |
| ② 方法爆炸的型別 | *Service 上掛了五十個方法 | 提煉類別、拆 package |
| ③ ★ 巨大的 package | 一個 internal/service 裡塞了八十個檔案 | ★ Go 真正的邊界是 package,這才是最該處理的一種 |
★ 怎麼具體認出來:欄位使用矩陣
UserID Email Addr CardNo CVV OrderIDs Total
Login() ✔ ✔
SendMail() ✔
Charge() ✔ ✔
Ship() ✔
Summary() ✔ ✔
↑───認證───↑ ↑─物流─↑ ↑──付款──↑ ↑───訂單───↑
★ 四個幾乎不重疊的欄位群 = 四個型別。 這個矩陣是最可靠的判斷工具——
比「欄位超過 N 個」或「行數超過 N 行」都準,因為它直接量的是內聚度。
其他訊號:
- 命名前綴成群:
billingAddr/billingCity/billingZip在喊「我是一個Address」。 - ★ 型別的名字含糊:叫
Manager/Service/Handler/Helper的型別幾乎一定過大——
名字含糊是因為職責不明。 - 註解在分區:
// ---- 付款相關 ----就是切線。 - ★ 測試很難寫:建一個實例要準備二十個欄位(見 工具-測試先行的設計)。
★ Go 的關鍵差異:先拆 package,再拆型別
// ❌ Java 式:在同一個 package 裡拆成十個小型別
internal/service/ user.go userAuth.go userBilling.go userShipping.go …
// ★ 它們互相能存取所有小寫欄位 —— 封裝邊界完全沒有建立起來,只是檔案變多
// ✅ Go 式:按領域拆 package
internal/auth/ user.go
internal/billing/ customer.go
internal/shipping/ recipient.go
// ★ 現在跨領域只能透過匯出的 API,編譯器會替你守住邊界
★★ Go 的可見性邊界是 package,不是 struct。
同一個 package 裡把大 struct 拆成五個小 struct,沒有建立任何新的封裝——
它們仍然看得見彼此的私有欄位。真正的隔離只有「拆 package」一招。
⚠️ 但拆 package 會撞到 循環 import(Go 的硬性編譯錯誤)。三個解法見 搬移函式。
★★ 什麼時候這其實不是味道
| 情況 | 說明 |
|---|---|
| ★ 純資料的 DTO/Config | http.Request 有幾十個欄位、tls.Config 更多——它們是「一包參數」,本質上就該大 |
| 協定/格式的對映型別 | protobuf 產生的 struct、JSON schema 對映 |
| ★ 標準庫風格的「一個型別一個套件」 | time.Time 方法很多,但它就是一個完整概念 |
| 方法多但共用同一組欄位 | 例如 strings.Builder——高內聚,不是過大 |
★ 判準不是「幾個欄位/幾個方法」,是「這些成員是不是圍繞同一個概念」。
tls.Config的三十個欄位全都是「TLS 怎麼設定」;
UserManager的三十個欄位分屬認證、帳務、物流——後者才是味道。
⚠️ 拆解的三個具體風險
- ★ JSON/DB schema 會變形。
encoding/json沒有inline選項;要維持扁平只能用匿名嵌入
(實測見 提煉類別)。對外契約要用獨立的 DTO 隔開。 - ★ 拆完兩邊互相呼叫 = 切線錯了。好的切線讓兩邊幾乎不需要對話。
- 不要一次拆完。一次拆一個概念出去,留轉發、跑測試、commit。
一個「把 God Object 拆成六個型別」的 PR 沒人 review 得動。
🧪 我實際套用的紀錄
- (待填)
⚠️ 注意 / 什麼時候不適用
-
拆分要沿著真正的職責界線;為了「行數」硬拆會製造出彼此黏膩的碎片與更多間接。
-
有些領域核心物件天生較大,重點是「職責是否單一」,而非絕對大小。
-
★ 純資料型別(Config、DTO、協定對映)本來就大,不是味道。判準是「成員是否圍繞同一個概念」。
-
★★ 在同一個 package 內拆型別,沒有建立任何封裝——Go 的可見性邊界是 package。真的要隔離就拆 package。
-
★ 拆 package 會撞循環 import(Go 的硬性編譯錯誤),要先想好依賴方向。
-
對外 JSON/DB 形狀會跟著內部結構變,且不受編譯器保護。用獨立 DTO 隔開。
-
一次拆一個概念,留轉發期。一個大 PR 沒人 review 得動。
-
★ 名字叫
Manager/Service/Helper才是真訊號,不是欄位數量。含糊的名字代表職責不明。
🔗 相關工具
- 發散式變化 Divergent Change(過大類別常因此而多因變化)
- 純資料類別 Data Class/過長函式 Long Function(大類別內常見的伴生味道)
- 重構
- 提煉類別 —— 主要對治手法
- 搬移欄位、搬移函式 —— 拆解的實際動作
- 發散式變化 —— 過大類別最常見的後果
- 資料泥團 —— 欄位成群出現就是切線
- 純資料類別 —— 拆過頭的反效果
- 編寫內聚的程式碼 —— 切線的判準