测试与代码质量

[代码异味 #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 标准差异的对照实验

延伸阅读