每个项目都有这么一个文件。打开它,滚动条细得像一根线。
本文接续上一篇 代码异味 #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)