概述 这份技能面向 Kotlin 中型、功能面较广的应用,讨论一个具体决策:是否需要在展示层(view model)与数据层(repository)之间保留交互器(interactor)或用例(use-case)层。其立场是:即使完全没有这一层,应用仍可以拥有名副其实的边界,因为边界从来不是交互器,而是接口。
替代结构 文档认为,去掉用例层后,以下四件事支撑边界:
- 领域模块中的仓库接口。view model 的构造函数只依赖 AlbumRepository,绝不引用 AlbumRepositoryImpl,后者是数据模块的 internal 类型。
- 数据模块内 internal 的纯映射函数,例如 internal fun ServiceDto.toTrack(): Track。它们是扩展函数,无类、无注入、无生命周期,承担 DTO 到领域模型的转换,也是用例原本唯一可能承载的“分层中的业务逻辑”。
- 仓库返回结果封装,使 view model 针对状态分支,而非针对传输层失败分支。
- 仓库按聚合划分,而非每应用一个;大约每个顶层概念一个接口,每个接口几十个方法。若某个接口有数百个方法,说明这一层并没有消失,只是被倒进了一个仓库里。
证明“不存在” 文档强调,“没有用例”这一断言会静默失效,应把它变成可执行检查。用例层至少有五种写法,单个 grep 会漏掉四种:明显的类名(UseCase/Interactor)、文件名(UseCase.kt/Interactor.kt)、包名(usecase/usecases/interactor/interactors)、单方法类被当函数使用的 operator fun invoke 形态、以及存在但为空的文件夹。需要排除 build 等生成目录。五项检查全部无输出才是证据;否则说明该层存在,真正的问题变成它是否一致。文档建议把这套检查放进评审清单,而不是写成注释,因为注释不是结构性证据。
去掉后失去什么 文档明确列出代价:跨仓库组合的流程没有自然归属,会落到 view model;失去不依赖展示框架的测试接缝(通过用例测试编排无需 view model 测试环境、主线程调度规则和生命周期所有者);失去可检索的操作名称(AddSongToPlaylist 比“fun onAddClicked 内的代码块”更易搜索);失去跨展示界面的复用(控件、后台任务和页面各自重新实现同一操作)。
何时局部加回 判断信号是:同一段编排被复制进第三个 view model。两个算巧合,共享私有函数即可;三个则是结构性信号。此时只为该操作重新引入一个类,注入原本注入的仓库,返回同样的结果封装。整层引入、为每个仓库方法配一个包装,不会带来上述四项收益,反而使调用链翻倍。次要信号是:后台组件(控件、定时任务、通知处理器)需要当前位于 view model 中的操作,应只把该操作抽出来,而不是把 view model 注入进去。
陷阱 文档列出若干需警惕的问题:
- 去掉该层并不等于允许 view model 塞逻辑,只是把上限转移了。会话级 view model 会吸收编排并无感增长,应按文件行数监控,而非凭感觉;某个文件比同级文件大一个数量级时,这一层以未命名、不可测试、绑定展示生命周期的形式回来了。
- “没有用例”不等于“没有领域层”。领域模块仍拥有接口、模型和能力端口;仓库实现被直接注入 view model 的架构根本没有边界,而从文件夹列表上看两者可能一模一样。
- 不要让仓库直接返回传输类型以省掉映射:一旦出现返回 ServiceSearchResponse 的方法,所有调用方都会导入服务模型。映射函数是承重部分,交互器才是可选的。
- 纯函数替代方案只有在保持为函数时才“纯”:若映射函数开始接收仓库、调度器或时钟,它就变成了披着 fun 关键字的用例,且失去了可发现性,此时应命名并注入。
- “干净”的评审仍须检查依赖方向,而非文件夹是否存在:应列出应用模块从数据模块导入的内容。没有用例且无泄漏才是干净;有用例层但页面直接导入仓库实现则不是。
验证 文档给出三项结构性检查,用来支撑上述边界主张:view model 不导入 RepositoryImpl(应无输出);纯映射函数存在且保持 internal(非零计数,且仅能从数据模块内部访问);仓库接口不导入集成模块的传输类型(用反向白名单检查,输出中不应出现集成模块的类型)。