測試與程式碼品質

[程式碼異味 #4] 上帝物件(God Object)完整整理

上帝物件是不斷吸收周邊責任與資料而膨脹的類別,也是團隊中最常修改、最常衝突的檔案。本文整理辨識膨脹的訊號、拆解順序,以及拆分失敗的兩種方式。

閱讀 6 分鐘
[程式碼異味 #4] 上帝物件(God Object)完整整理 封面圖

每個專案都有那麼一個檔案。打開後,捲軸細得像一條線。

這是接續上一篇 程式碼異味 #3 的內容。

名稱可能是 AppManager、MainViewController 或 DataStore。它也是團隊中最常修改、最常發生衝突的檔案。

這就是上帝物件(God Object)。

1998 年出版的《AntiPatterns》稱它為 Blob(The Blob)(出版資訊)。這個詞指的是不斷吸收周邊責任與資料而膨脹的龐大物件。

什麼是上帝物件

不能只看行數。即使是 2,000 行的型別,只要只做一件事,它就只是大型型別(God Class 判定研究)。

區分上帝物件的關鍵是 知道的範圍。三者重疊時,幾乎可以確定。

  • **知道得太多。**網路、資料庫、畫面狀態、登入資訊和付款都集中在同一個型別中。import只看清單就能看出來。
  • **太多東西知道它。**專案各處都參照這個型別。若是 singleton,參照路徑不會顯示在程式碼中,情況更糟。
  • **狀態分散。**有三十個屬性,但沒人知道哪些組合有效。

當 isLoading 為 true 且 error 也不是 nil 時,是否可能成立,已經沒人能回答。

在 iOS 中,這個位置通常很明確:視圖控制器。

單一畫面的版面配置、網路呼叫、表格資料來源、畫面轉換與狀態管理,全都集中在一個檔案中。

這種結構稱為大型視圖控制器(Massive View Controller)。名稱中帶有 AppDelegate 和 Manager 的型別也很常見。

為什麼會持續膨脹

沒有人一開始就想把它做成這樣。這正是重點。

上帝物件不是設計決策,而是 重力 的結果。

假設現在要加入新功能。所需資料已經全在這個類別裡。

選項 成本
在既有類別新增一個方法 20 分鐘
建立新型別 傳遞相依性、初始化位置與生命週期,需 半天

每次都做合理選擇,但方向總是一樣。於是原本 200 行的檔案,三年後變成 4,000 行。

每次成長都有一個正當理由,因此很難回頭。

再加上破窗效應。在已經 3,000 行的檔案裡再加入 50 行,沒人會反對。

在 200 行檔案中加入 50 行,心理阻力完全不同。

同時了解網路、DB、工作階段與付款的 4000 行視圖控制器相依關係圖
箭頭數量就是測試難度

問題在哪裡

傷害會以四種形式出現。

  • **測試變得不可能。**建立這個型別需要網路、資料庫和使用者工作階段。想驗證純計算邏輯,卻必須先啟動伺服器。
  • **平行工作受阻。**三位團隊成員各自開發不同功能,卻都修改同一個檔案。衝突不斷,審查時變更也混在一起。
  • **無法知道變更影響範圍。**要預測修改一個屬性會破壞什麼,就得了解全部 4,000 行。實際上沒人知道,所以不修改而新增屬性,檔案就更大。
  • **無法重複使用。**其他畫面也需要其中的日期格式化邏輯,但取出來就會連整個類別一起帶走,只好複製貼上。

尤其第一點最致命。沒有測試就無法拆分,無法拆分就會持續膨脹。

這個循環才是上帝物件長期存在的真正原因。

如何察覺它正在膨脹

不要靠感覺,改用訊號。

訊號 1 · Git 變更頻率

這是最實用的指標。列出最近一年修改最多的檔案,上帝物件通常會排在前面。

git log --format=format: --name-only --since=1.year.ago \
  | grep '\.swift$' | sort | uniq -c | sort -rn | head -20

頻繁變更表示檔案會因不同理由而修改,是違反單一職責原則的實測值。

訊號 2 · 內聚力

如果方法只操作彼此不同的屬性集合,這個型別很可能其實是兩個型別。

五個只使用屬性 A、B 的方法,加上六個只使用 C、D 的方法,邊界候選已經很明顯。

訊號 3 · 型別長度規則

若使用 SwiftLint,type_body_length 和 file_length 預設已啟用。

型別本體 250 行、檔案 400 行是預設警告線,也可依專案調整。數字本身雖然任意,但越線時能引發討論,這就是它的價值。

拆解順序

一次全部重寫幾乎都會失敗。順序很重要。

# 階段 要做什麼
1 先織網 沒有測試時,先建立特性化測試
2 分離無狀態邏輯 先抽出日期格式化、字串驗證與金額計算
3 分離狀態群組 將一起變更的屬性組成一個型別
4 依角色分離 拆成資料來源、協調器、視圖模型與子視圖控制器
5 切斷吸收路徑 決定新程式碼的位置並設下門檻

特性化測試不是記錄程式碼是否正確,而是 目前如何運作 的測試。這是 Michael Feathers 在《Working Effectively with Legacy Code》中整理的技巧。

目標是在保留行為的同時搬移,因此目前行為就是基準線。

第二階段最容易,因為它處理不使用實例狀態的邏輯。這一步通常能讓檔案明顯縮小。

第三階段中,Swift 的 enum 很有用。如果三個畫面狀態總是一起變更,那就是一個狀態物件。

// 前:可以表示無效組合
var isLoading = false
var items: [Item] = []
var error: Error?

// 後:三者只能存在其一
enum ViewState {
    case loading
    case loaded([Item])
    case failed(Error)
}

用 enum 綁在一起,就能消除不可能的組合。

第四階段的 iOS 路徑很明確:表格資料來源獨立成型別,畫面轉換交給協調器,畫面狀態與顯示規則交給視圖模型,大型畫面拆成子視圖控制器。

缺少第五階段,六個月後就會恢復原狀。要避免下一個功能再次進入該檔案,必須明確規定新程式碼的位置,並以檔案長度規則或審查共識設下門檻。

將巨大單一類別逐步拆成多個小型別的拆解過程圖
目標不是增加片段,而是讓變更理由只剩一個

拆分失敗的兩種方式

失敗方式 會發生什麼
只改名稱 把 AppManager 拆成 UserManager、DataManager、NetworkManager 三個型別,結果三者互相全部參照。上帝物件只是變成了上帝叢集。
拆得過細 把 4,000 行類別拆成 40 個型別後,這次流程消失了。這就是上一篇提到的義大利麵程式碼。

拆分時也要確認相依方向是否只朝單一方向流動。

拆解的目標不是製造片段,而是讓 每個片段只因自己的理由而變更。片段數量只是結果。

總結

  • 上帝物件不是依大小,而是依知道的範圍判定。知道很多、被很多地方知道,且狀態分散,就符合條件。
  • 它不是因為設計很糟,而是每次都選擇最便宜的方案,結果逐漸膨脹。
  • 最大的傷害是無法測試。沒有測試就無法拆分,無法拆分就持續變大。
  • Git 變更頻率排行榜可作為實測指標。
  • 拆解順序是特性化測試 → 無狀態邏輯 → 狀態群組 → 角色分離,最後阻止它再次膨脹。

下一篇討論拆分上帝物件後遇到的問題:修改一個功能卻要改十二個檔案,也就是散彈槍手術。

來源與查證標準

  • AntiPatterns — Wiley · 官方資料 · 查證 2026-08-17 · 依據:1998 年 AntiPatterns 出版資訊與 The Blob 的脈絡
  • Human Perception on God Class Detection — Springer Nature · 論文原文 · 查證 2026-08-17 · 依據:不同人員與工具在 God Class 判定標準上的控制實驗

延伸閱讀