大多数进行模块化的团队,都会在某个阶段创建名为 Common、Core 或 Utils 的模块。
当“很多地方都要用,先放这里”的判断不断积累后,这个模块不知不觉成了项目中最大、变化最频繁、所有人都依赖的模块。
先说结论:把模块命名为 Common,等于承认它没有内聚性。“公共”不能成为组织代码的标准。
本文将总结 Common 模块为何会让模块化倒退,以及如何通过分层结构(垂直拆分)和功能结构(水平拆分)来预防。
Common 模块为什么危险?
问题会分三个阶段发展。
第一阶段:变成垃圾桶。“不知道该放在哪里的代码”全部进入 Common。日期格式化工具旁边放着网络客户端,旁边又放着自定义按钮。它们彼此毫无关系。
第二阶段:全员依赖。所有功能模块都 import Common。此时 Common 位于依赖图最下游,也就是本应最稳定的位置。
第三阶段:整体重新构建。但 Common 是垃圾桶,所以它变化最频繁。上一篇文章的稳定依赖原则被彻底颠倒了。只改一个按钮颜色,所有模块都要重新构建。
当被依赖最多的模块变成变化最频繁的模块时,模块边界就形同虚设。
分层结构:垂直拆分
预防措施的第一条轴线,是按职责进行垂直拆分。常见的 3~4 层结构如下。
| 层级 | 内容 | 示例 |
|---|---|---|
| Feature | 页面/功能模块 | 首页、搜索、订单、设置 |
| Domain | 业务规则、模型、用例 | 订单策略、会员模型 |
| Core | 特定技术的薄封装 | 网络、存储、日志 |
| Shared | 真正通用的工具 | 日期格式化工具、字符串扩展 |
规则只有一条:依赖只能从上向下流动。
Feature 可以使用 Domain 和 Core,但 Core 不能知道 Feature。Feature 之间也不直接相互引用,而由上层应用 target 负责组装。
这里要注意它与 Common 的区别。Core 不是一个巨大的模块,而是按技术拆分出的多个模块。
- 例如 CoreNetwork、CoreStorage 和 CoreLogging,分别都是独立模块。
- 如果搜索功能只使用日志,就只 import CoreLogging。
这样即使修复网络代码,只使用日志的模块也不会重新构建。
功能结构:水平拆分
第二条轴线是按领域进行水平拆分。即使在同一层中,首页、搜索和订单也应该是不同的模块。
判断标准是第一篇文章中提到的问题:“这些代码是否因相同原因发生变化?”
- 如果搜索策略变化时不需要修改订单代码,它们就是不同的模块。
- 如果订单页面和订单用例总是同时变化,继续进行垂直拆分可能就过度了。
将垂直(分层)和水平(功能)叠加后,就形成了网格。实际模块会占据其中一个格子,例如“订单功能的 Domain”或“搜索功能的 Feature”。
如果 Common 已经变得过于庞大?
不必一次性摧毁现有的 Common。实践验证过的顺序如下。
- 禁止新增:先建立从今天起不再向 Common 添加新代码的规则。
- 调查使用位置:从变化最频繁的代码开始,确认它实际在哪里使用。
- 找到归属:只被一个功能使用,就移到该功能模块;如果是特定技术的封装,就移到 Core 系列模块。
- 重命名剩余内容:只将最后留下的真正通用代码,用 Shared 之类更精确的名称隔离。
这项工作需要几个月,但可以清楚地看到重新构建范围在每个阶段逐步缩小。
什么时候需要这种结构,什么时候又会过度?
- 开发者达到 4~5 人以上、功能领域达到 3 个以上时:非常值得建立分层规则。
- 如果是 1~2 人的项目:从 Feature/Shared 两层开始即可。先画完整网格会过度设计。
- 无论规模大小:建议从一开始就遵守“不创建名为 Common 或 Utils 的模块”这一规则。
面试时可以这样问
Q. 公共模块(Common)变大后会产生什么问题?应该如何设计?
所有人都依赖的模块变成变化最频繁的模块,因此即使是小修改,也需要整体重新构建并进行广泛的影响分析。这是稳定依赖原则被颠倒的状态。应按职责细分为网络、存储、日志等模块,只将真正通用的代码隔离在最小的 Shared 模块中。
Q. 请说明将模块结构划分为分层时的规则。
设置 Feature、Domain、Core 等按职责划分的层级,并只允许依赖从上向下单向流动。同层模块之间不能直接相互引用,而应在上层组装层连接。遵守这条规则后,变更影响范围就始终被限制在上方。
以上就是模块化原理三部曲。掌握边界、依赖方向和分层结构,就具备了所需的概念工具。
从下一篇开始,我们将把这些原则直接应用到 iOS 项目中,先介绍如何使用 Swift Package 实际拆分模块。

