在模块化项目中,保证依赖倒置(Dependency Inversion)和分层隔离,本质上是防止“依赖污染”。如果核心逻辑模块(Domain/Core)能够直接引用 UI 模块,项目很快就会演变成难以测试且编译缓慢的“面条架构”。
以下是保证这种隔离性的四种核心策略:
1. 物理隔离:单向依赖图(Dependency Graph)
这是最基础也最强力的手段。在 SPM 或 Xcode 项目中,通过 dependencies 显式规定引用关系。
- 配置原则:底层模块(Logic/Core)的
dependencies列表里,绝对不能出现上层模块(UI/Feature)的名字。 - 强制力:一旦你在逻辑层代码中
import UIKit或试图调用 UI 类,编译器会直接报错,因为在物理路径上这些符号是不可见的。
2. 依赖倒置:协议(Protocols)作为隔离带
当你需要在逻辑层触发 UI 操作(如:逻辑层处理完数据后,需要弹出一个对话框)时,不要直接引用 UI 类。
-
步骤:
- 在 逻辑模块 定义一个接口协议:
protocol AlertPresentable { func show(message: String) }。 - 逻辑模块内部持有该协议的弱引用。
- 在 UI 模块 中,让对应的
ViewController或Coordinator实现这个协议。 - 在 App 启动层(通常是顶层容器),将 UI 实例注入给逻辑模块。
- 在 逻辑模块 定义一个接口协议:
收益:逻辑模块只知道有一个能弹窗的东西,但完全不知道它是
UIAlertController还是一个 SwiftUI 的View。
3. 跨模块工厂模式(Modular Factory / Coordinator)
为了防止“核心模块访问特定实现”,我们需要隐藏具体的初始化逻辑。
-
问题:核心模块需要跳转到某个功能页,但它不应该知道具体的
ProfileViewController。 -
解决:
- 在
Domain模块定义ProfileBuildable协议。 - 在
Profile模块实现该协议并返回UIViewController(类型抹除)。 - 核心模块通过 Dependency Injection (DI) 容器 获取
ProfileBuildable。
- 在
4. 访问权限锁死:Internal 与 Public
利用 Swift 的访问控制符(Access Control)来守门:
- 逻辑层内部实现:标记为
internal。即使 UI 层引用了逻辑模块,它也只能看到标记为public的接口。 - SPI(System Programming Interface) :如果你想让某个方法仅对“特定兄弟模块”可见,而不对 UI 层可见,可以使用
@_spi(虽然这是非正式 API,但在大型库中常用)或者将该方法放在一个特定的InternalKit模块中。
5. 架构扫描工具(Linting)
对于超大型团队,仅仅靠自觉是不够的。可以引入工具来自动检测违规依赖:
- SwiftLint / XcodeGen:可以编写自定义规则,检查特定文件路径下是否包含
import UIKit或import SwiftUI。 - XCLogParser / Dependency Graph 可视化:定期生成依赖图,观察是否有“逆向箭头”。
总结:依赖方向检查清单
| 需求场景 | 错误做法(产生耦合) | 正确做法(隔离) |
|---|---|---|
| 逻辑层调用 UI | import UIKit 并在方法中创建 UIView | 定义协议,由 UI 层实现并注入 |
| 核心层引用第三方库 | 在基础模块直接集成 Alamofire | 在基础模块定义 NetworkService 协议,在独立实现包里封装 Alamofire |
| 模块间跳转 | let vc = FeatureBViewController() | 通过路由(Router)或协调器(Coordinator)跳转 |