每個專案都有那麼一個檔案。打開後,捲軸細得像一條線。
這是接續上一篇 程式碼異味 #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 行,心理阻力完全不同。
問題在哪裡
傷害會以四種形式出現。
- **測試變得不可能。**建立這個型別需要網路、資料庫和使用者工作階段。想驗證純計算邏輯,卻必須先啟動伺服器。
- **平行工作受阻。**三位團隊成員各自開發不同功能,卻都修改同一個檔案。衝突不斷,審查時變更也混在一起。
- **無法知道變更影響範圍。**要預測修改一個屬性會破壞什麼,就得了解全部 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 判定標準上的控制實驗

![[程式碼異味 #4] 上帝物件(God Object)完整整理 封面圖](/assets/images/posts/c44a316d-1d8c-4de3-8558-2b5452bdbcfe/god-object-dependency-overload.jpg)