在 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 UIKit 或 import SwiftUI,或者函数参数里有 UIImage,这就是严重的反向依赖。
- 对策:Interactor 应该只处理
Data或String。图片下载完成后,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. 什么时候划分失败了?(自查表)
如果你在代码中发现以下情况,说明边界已经坍塌:
- Interactor 需要知道 View 的层级结构(例如为了计算某个偏移量)。
- Presenter 包含复杂的
if-else业务计算(例如根据多个字段计算会员折扣)。 - 单元测试 Interactor 时必须 Mock 一个 View 实例(说明 Interactor 间接依赖了 UI)。
总结
Presenter 是 Interactor 的客户。 客户(Presenter)提出需求,供应商(Interactor)交付货物(Entity)。供应商不应该知道客户家里的装修风格(UI 实现),也不应该知道货物最后被摆在了哪个架子上。