12-34.【SwiftUI】VIPER 中 Interactor 和 Presenter 的边界如何划分才不会“反向依赖”?

5 阅读3分钟

在 VIPER 架构中,Interactor 和 Presenter 之间的边界是最容易产生“纠缠”的地方。要彻底消除反向依赖,核心原则是:Presenter 决定“做什么”(业务意图),而 Interactor 决定“怎么做”(业务逻辑),且 Interactor 对 Presenter 的存在应当是“无感”的。

以下是防止反向依赖的防御式划分策略:


1. 核心边界:双向协议(Double Protocol)

在 VIPER 中,绝对禁止 Interactor 直接持有 Presenter 的具体类。必须通过两个方向的协议来锁定边界:

  • Interactor Input (Presenter -> Interactor) :Presenter 调用的接口。
  • Interactor Output (Interactor -> Presenter) :Interactor 回传结果的接口(通常由 Presenter 实现)。

2. 职责的“硬切分”

为了防止逻辑渗漏,请遵循以下分工准则:

维度Presenter (UI 逻辑)Interactor (业务逻辑)
数据形式处理 ViewModel 或原生类型(用于显示)。处理 Entity(模型对象、数据库记录)。
关注点关注“用户点击了什么”、“什么时候转圈”。关注“如何计算折扣”、“如何并发下载”。
状态持有可以持有 UI 相关状态(如 isSearching)。无状态(Stateless)或仅持有业务状态。
错误处理决定如何弹窗提醒用户。决定错误的原因(如 NetworkError.timeout)。

3. 如何避免反向依赖的“陷阱”?

A. 禁止 Interactor 引用 UI 相关类型

如果你的 Interactor 出现了 import UIKitimport SwiftUI,或者函数参数里有 UIImage,这就是严重的反向依赖。

  • 对策:Interactor 应该只处理 DataString。图片下载完成后,Interactor 返回 Data,由 Presenter 将其转换为视图可用的图像。

B. 结果回传的“被动性”

Interactor 不应该主动命令 Presenter 去“刷新 UI”。

  • 错误presenter.refreshUI()
  • 正确output.didFetch(data: [Entity])

逻辑本质:Interactor 只是在完成工作后,对着名为 output 的扩音器喊了一声结果。它并不关心谁在听,也不关心听的人拿这些数据去刷 UI 还是去写日志。

C. 业务决策的“隔离”

  • 场景:用户点击登录。
  • Presenter:调用 interactor.login(user, pass)
  • Interactor:执行网络请求,验证逻辑。
  • 关键点:如果登录失败,Interactor 不应 决定“显示哪种样式的 Alert”,它只通过 output.loginFailed(error) 报告失败。如何呈现这个失败,是 Presenter 的特权。

4. 进阶防御:利用实体(Entity)作为防火墙

Interactor 与外界交换的应当是纯粹的 Entity

  • 隔离层:当 Interactor 从网络模块拿到一个复杂的 JSON Model 时,它应该将其转换为简洁的 Entity 再交给 Presenter。
  • 目的:防止 Presenter 渗透进网络层的具体实现逻辑。这样即使你更换了底层 API 结构,只要 Entity 不变,Presenter 就完全不需要改动。

5. 什么时候划分失败了?(自查表)

如果你在代码中发现以下情况,说明边界已经坍塌:

  1. Interactor 需要知道 View 的层级结构(例如为了计算某个偏移量)。
  2. Presenter 包含复杂的 if-else 业务计算(例如根据多个字段计算会员折扣)。
  3. 单元测试 Interactor 时必须 Mock 一个 View 实例(说明 Interactor 间接依赖了 UI)。

总结

Presenter 是 Interactor 的客户。 客户(Presenter)提出需求,供应商(Interactor)交付货物(Entity)。供应商不应该知道客户家里的装修风格(UI 实现),也不应该知道货物最后被摆在了哪个架子上。