架构
功能模块不要互相引用
App 目标是组装的地方。功能之间只通过协议和路由模型说话。
模块切分的目的不是文件夹更好看,而是改一个功能时不用编译整个应用,也不会在无意中调用到另一个功能的内部类型。如果 FeatureA import FeatureB,边界就已经破了。
依赖方向
- 功能模块依赖基础库和自己的接口,不依赖其他功能模块。
- App 目标依赖所有功能,负责创建它们、把实现注进去。这是组合根。
- 基础库不知道任何功能。一旦基础库出现“feed 的模型”,它就开始变成杂物间。
两个功能都要用的类型,先问它是不是真的稳定。用户 id、金额、一个通用的加载态,可以进基础库。某个信息流的卡片样式,留在那个功能里。过早共享,会让基础库成为所有人都能修改的上帝模块,编译时间和耦合一起变差。
接口放在谁那里
// Feed 模块定义自己需要什么,不定义账号模块的实现
protocol AccountSession: AnyObject {
var userID: String? { get }
}
final class FeedStore {
private let session: AccountSession
init(session: AccountSession) { self.session = session }
}
账号模块提供一个符合 AccountSession 的类型。App 在启动时把它传给 FeedStore。Feed 的测试传一个假的 session,不需要把登录界面编进来。
Swift Package 还是内部框架
包的边界应该和编译边界一致。一个包里一百个 target,和一个 target 里一百个文件夹,前者只是更难打开。独立演进、需要单独测试、会被多个 App 使用的,再拆包。只在一个 App 里用的功能,可以是同一个工程里的 framework 或 package target,重点是 import 关系,不是仓库数量。
检查方式很具体:对功能模块搜 import,出现另一个功能的名字就处理。循环依赖不要用类型前向声明糊过去,那说明两边都拿了对方的具体类型。