14-13.【模块化与包管理】如何在模块化项目中保证“逻辑层不可访问 UI 层”或“核心模块不可访问特定实现”?

4 阅读3分钟

在模块化项目中,保证依赖倒置(Dependency Inversion)分层隔离,本质上是防止“依赖污染”。如果核心逻辑模块(Domain/Core)能够直接引用 UI 模块,项目很快就会演变成难以测试且编译缓慢的“面条架构”。

以下是保证这种隔离性的四种核心策略:


1. 物理隔离:单向依赖图(Dependency Graph)

这是最基础也最强力的手段。在 SPM 或 Xcode 项目中,通过 dependencies 显式规定引用关系。

  • 配置原则:底层模块(Logic/Core)的 dependencies 列表里,绝对不能出现上层模块(UI/Feature)的名字。
  • 强制力:一旦你在逻辑层代码中 import UIKit 或试图调用 UI 类,编译器会直接报错,因为在物理路径上这些符号是不可见的。

2. 依赖倒置:协议(Protocols)作为隔离带

当你需要在逻辑层触发 UI 操作(如:逻辑层处理完数据后,需要弹出一个对话框)时,不要直接引用 UI 类。

  • 步骤

    1. 逻辑模块 定义一个接口协议:protocol AlertPresentable { func show(message: String) }
    2. 逻辑模块内部持有该协议的弱引用。
    3. UI 模块 中,让对应的 ViewControllerCoordinator 实现这个协议。
    4. 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 UIKitimport SwiftUI
  • XCLogParser / Dependency Graph 可视化:定期生成依赖图,观察是否有“逆向箭头”。

总结:依赖方向检查清单

需求场景错误做法(产生耦合)正确做法(隔离)
逻辑层调用 UIimport UIKit 并在方法中创建 UIView定义协议,由 UI 层实现并注入
核心层引用第三方库在基础模块直接集成 Alamofire在基础模块定义 NetworkService 协议,在独立实现包里封装 Alamofire
模块间跳转let vc = FeatureBViewController()通过路由(Router)或协调器(Coordinator)跳转