判断一个功能是应该留在原地,还是独立为 Target(包内模块)或 Package(独立包),本质上是在开发便利性与架构严谨性之间做权衡。
你可以通过以下四个维度来进行决策:
1. 变动频率与稳定性 (Volatility vs. Stability)
这是判断是否拆分的最直接标准。
-
留在原地:如果该功能与现有代码紧密耦合,且每次修改业务时都需要同步修改该功能,拆分会导致你在两个模块间频繁切换,增加认知负担。
-
拆分为 Target/Package:如果该功能非常稳定(如:自定义的加密工具、基础 UI 组件),一旦写好很少变动。
- 收益:稳定模块编译后会被缓存。当你修改业务代码时,编译器无需重新扫描这些稳定的 Target,从而大幅缩短增量编译时间。
2. 依赖对称性 (Dependency Symmetry)
观察该功能的依赖链条。
-
留在原地:如果该功能依赖了大量的业务逻辑、具体的 ViewModel 或数据库模型,它就不具备独立性。
-
拆分为 Target:如果该功能是**“纯净”**的(例如只依赖 Foundation),或者它被多个互不相关的业务模块共同依赖。
- 原则:如果模块 A 和模块 B 都需要代码 C,那么 C 必须独立出来。否则,A 依赖 B 或 B 依赖 A 都会造成不必要的代码“污染”。
3. 复用边界 (Reusability Boundary)
-
独立为 Target (包内模块) :当代码仅在当前项目的多个 Feature 之间复用时。例如:项目中通用的
LoadingView。 -
独立为 Package (独立包) :当代码具备跨项目复用的价值时。
- 例子:你开发了一个高性能的图片裁切工具,公司里的其他 App 也能用。
- 收益:Package 拥有独立的 Git 版本管理,可以跨工程分发,且能通过二进制化(XCFramework)进一步保护源码或加速构建。
4. 物理隔离的需求 (Safety & Encapsulation)
-
保持在内部:如果你希望利用 Swift 的
internal访问权限,让该功能对包内其他类可见,但不希望增加复杂的public接口声明。 -
拆分为独立模块:当你需要强制解耦时。
- 架构强制力:一旦拆分为独立 Target,除非显式在
Package.swift中声明依赖,否则无法访问其他模块。这能防止开发者因为“贪图方便”而写出跨模块的越权调用(例如在底层的网络库里直接调用上层的登录 UI)。
- 架构强制力:一旦拆分为独立 Target,除非显式在
决策决策矩阵 (Decision Matrix)
| 观察维度 | 留在原地 | 拆为 Target (模块) | 拆为 Package (独立包) |
|---|---|---|---|
| 复用范围 | 单一功能点 | 本项目内多处复用 | 多个项目间复用 |
| 编译耗时 | 极小,不影响 | 较大,需通过隔离加速 | 极大,或需二进制分发 |
| 维护归属 | 本业务组 | 跨组公用 | 公司架构组/开源 |
| 依赖关系 | 包含业务逻辑 | 仅依赖基础库 | 几乎零外部依赖 |
| 访问控制 | 利用 internal 方便访问 | 强制 public 接口隔离 | 严格的版本号管理 (SemVer) |
总结建议
- 先 Target,后 Package:不要一开始就创建几十个 Git 仓库。先在同一个 SPM Package 下拆分不同的
target。 - 关注“功能孤岛” :如果你发现某个文件夹下的代码从来不 import 宿主项目的任何东西,那么它就是一个完美的拆分候选者。
- 编译性能驱动:如果你的项目编译超过 3 分钟,优先把那些不常变动的底层工具类拆分成独立 Target。