Uncle Bob清洁架构中UseCase Interactor能否包含应用细节类Adapter/Gateway?
核心结论
Use Case Interactor 绝对不应该感知、判断甚至持有具体的Adapter/Gateway实例的存在,它的唯一职责就是承载纯业务规则运算,仅和Entity交互、返回纯POJO/业务DTO,所有和网络、数据库、API这类应用细节相关的操作、可用性判断、时序编排,全部剥离到Interactor外层处理。
这也是Uncle Bob在原始定义里没说透、但所有长期实践Clean Architecture的团队都会落地的编码层面的UseCase本质——你之前找不到精确定义,本质是很多公开解读把“UseCase定义了外部能力的契约边界”和“UseCase自己实现对外部能力的调用”混为一谈了。
为什么你写的第一种实现是错误的
你给出的包含网络、DB依赖的completeSale实现,核心问题根本不是用了Adapter,而是把应用细节的存在判断、异常分支处理全堆到了Interactor内部:
- 代码里的
if (networkAdapter && dbAdapter)判断,本身就是对应用细节的强耦合——纯业务逻辑根本不关心当前环境下有没有网络适配器、有没有数据库适配器,业务只关心“完成一笔交易后,交易状态如何计算、哪些字段属于合法的交易记录”这类核心规则 - 这类判断逻辑如果长期堆在Interactor里,很快就会被“没网、DB宕机、接口超时”这类系统层的边界场景分支挤占空间,最后Interactor变成混杂了业务规则、系统异常处理、细节调用的大杂烩,完全失去独立复用的价值
这种写法哪怕加了依赖注入,也只是把实现类从硬编码改成了可替换的形式,本质上Interactor还是在围绕应用细节的存在与否写逻辑,根本没做到真正的解耦。这也是你会觉得“加了注入还是没解耦”的核心原因。
正确的分层协作方式
你写的第二种无Adapter依赖的实现方向是完全正确的,只是很多人会觉得“这样的Interactor太单薄,没做实际工作”——但这恰恰是Clean Architecture的核心:越靠近核心的业务逻辑,越要纯粹,越要和所有可能变化的应用细节隔离开。
正确的协作边界是三层完全拆分的:
- Interactor层:纯业务运算,零应用细节感知
比如completeSale的逻辑,就是拿到Sale实体后,校验交易金额是否合法、计算最终交易状态(比如是否满足支付条件、有没有符合优惠规则)、构造合法的Transaction实体,返回对应的纯业务结果。整个过程没有任何分支判断系统能力状态,不调用任何网络、数据库方法,测试时直接传入实体就能运行,不需要任何Mock,速度极快,完全符合TDD的要求。 - 外层编排层(即传统定义里的Controller职责):所有应用细节逻辑的承载点
原来放在Interactor里的“调用网络支付接口完成交易校验、调用数据库保存记录、判断当前环境可用能力”的逻辑,全部移到这一层。这也刚好解决你提到的困惑:UML定义的用例步骤(比如“浏览商品、完成交易”),本质是对用户可感知的行为链路的抽象,不是要求Interactor把链路里每一步涉及应用细节的动作都自己走完——外层会根据当前设备的特性(比如离线平板就只调本地DB、在线手机就调网络+远程DB),先做好可用性判断、异常兜底(比如有网没DB时先把交易存在内存队列),把满足业务前提的输入数据整理好传给Interactor,拿到Interactor返回的纯结果后,再去做后续的持久化、网络调用操作。 - 抽象契约层:接口定义可以放在UseCase层,但绝不让Interactor判断实现是否存在
很多人说“UseCase要依赖Gateway抽象”,指的是UseCase层可以定义业务需要的能力契约(比如“需要能完成交易校验、能持久化交易记录”的接口),但这些接口的实现初始化、可用性校验、依赖注入的动作,全部在外层完成——Interactor拿到的永远是已经可以正常工作的接口实例,根本不需要写if(adapter)这类判断,也就不会被各种系统层的边界场景干扰核心逻辑。
对应你提到的跨端复用问题
按照这个设计,同一个Interactor逻辑根本不需要为不同端做定制:
- 在线手机端的外层,给Interactor传入网络版的交易校验适配器、远程DB的持久化适配器
- 离线平板端的外层,给Interactor传入本地离线交易校验的适配器、本地SQLite的持久化适配器
- Interactor本身的纯业务逻辑(金额计算、状态判定、实体构造)一行都不用改,直接跨端复用,完全符合“一次编写、处处运行”的目标。
内容的提问来源于stack exchange,提问作者GoreDefex
相关产品推荐
相关产品推荐

