🎯 什麼情境該想到我

當你「一個類別塞了太多欄位、扛了太多責任,什麼事都往它身上放」的時候——它試圖做太多事,難免出現重複與混亂。

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

這味道是什麼徵兆:欄位一多,類別幾乎必然承擔了多個職責,內部也容易長出重複碼。一個好用的觀察法:看欄位(或方法)名字有沒有共同前綴/後綴,那常是可以分出去的一群。

通常用哪些重構手法對治:

  • 提煉類別(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/Confighttp.Request 有幾十個欄位、tls.Config 更多——它們是「一包參數」,本質上就該大
協定/格式的對映型別protobuf 產生的 struct、JSON schema 對映
★ 標準庫風格的「一個型別一個套件」time.Time 方法很多,但它就是一個完整概念
方法多但共用同一組欄位例如 strings.Builder——高內聚,不是過大

★ 判準不是「幾個欄位/幾個方法」,是「這些成員是不是圍繞同一個概念」。
tls.Config 的三十個欄位全都是「TLS 怎麼設定」;
UserManager 的三十個欄位分屬認證、帳務、物流——後者才是味道。

⚠️ 拆解的三個具體風險

  1. ★ JSON/DB schema 會變形。encoding/json 沒有 inline 選項;要維持扁平只能用匿名嵌入
    (實測見 提煉類別)。對外契約要用獨立的 DTO 隔開。
  2. ★ 拆完兩邊互相呼叫 = 切線錯了。好的切線讓兩邊幾乎不需要對話。
  3. 不要一次拆完。一次拆一個概念出去,留轉發、跑測試、commit。
    一個「把 God Object 拆成六個型別」的 PR 沒人 review 得動。

🧪 我實際套用的紀錄

  • (待填)

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

  • 拆分要沿著真正的職責界線;為了「行數」硬拆會製造出彼此黏膩的碎片與更多間接。

  • 有些領域核心物件天生較大,重點是「職責是否單一」,而非絕對大小。

  • ★ 純資料型別(Config、DTO、協定對映)本來就大,不是味道。判準是「成員是否圍繞同一個概念」。

  • ★★ 在同一個 package 內拆型別,沒有建立任何封裝——Go 的可見性邊界是 package。真的要隔離就拆 package。

  • ★ 拆 package 會撞循環 import(Go 的硬性編譯錯誤),要先想好依賴方向。

  • 對外 JSON/DB 形狀會跟著內部結構變,且不受編譯器保護。用獨立 DTO 隔開。

  • 一次拆一個概念,留轉發期。一個大 PR 沒人 review 得動。

  • ★ 名字叫 Manager/Service/Helper 才是真訊號,不是欄位數量。含糊的名字代表職責不明。

🔗 相關工具