本文译自「Making Android Code Cleaner with Use Cases: A Practical Approach Using Kotlin Coroutines」,原文链接proandroiddev.com/making-andr...,由Siarhei Krupenich发布于2025年4月11日。

介绍
之前,我们开发了一个 Android 应用,重点关注了清晰架构 (Clean Architecture)、输入/输出 MVVM 拆分和 Repository 模式。这种方法遵循了 Google Android 团队推荐的最佳实践,使代码库具有可扩展性、可维护性和可测试性。
在本文中,我们将深入探讨另一个重要概念------用例 (Use Case),并向你介绍它的用法及其背后的背景。采用这种模式将使你的代码更具可读性和可测试性------这是一个巨大的优势。
使用Interactors带来的问题
过去,我们经常使用 Interactors 作为层与层之间的中间件组件------例如,Presenter 可以使用 Interactors 与领域层(Domain layer)进行通信。这使我们能够将一些逻辑从 Presenter 中移出,并放入单独的可复用组件中。在当时,这是一种可靠的逻辑拆分解决方案,能够保持代码简洁。
我们来看一个简单的例子:
Kotlin
// Interactor
class UserInteractor {
private var username: String = "Guest"
fun getUsername(): String {
return username
}
fun saveUsername(name: String) {
username = name
}
}
// Presenter
class UserPresenter(
private val view: UserView,
private val interactor: UserInteractor
) {
fun loadUsername() {
val name = interactor.getUsername()
view.showUsername(name)
}
fun updateUsername(name: String) {
interactor.saveUsername(name)
view.showSavedMessage()
}
}
随着 Presenter 的增长,逻辑的复杂性也会随之增加------这通常会导致 Interactor 中方法数量的增加。Presenter 越大,Interactor 也就越庞大。最终,我们会得到一坨塞满状态、方法和变量的屎堆,它们都堆挤在一个地方。
显然,这样的代码库维护起来很困难,测试起来也很困难,甚至更难用合适的单元测试来覆盖。最重要的是,这种架构会陷入反模式的境地,违反 SOLID(尤其是单一职责原则) 和 KISS 等核心原则。
这就是为什么我要强调使用 Interactor 方法时容易遇到的以下坑:
- 一处塞太多东西
当一个类处理所有操作------读取、写入、删除------它最终会做太多事情。这会导致测试更加困难,并且很难在不破坏其他功能的情况下进行更改。
- 功能不明确
像 LoginUser() 这样的用例会清楚地告诉你发生了什么。但是,如果交互器(interactor)很大,就很难区分它的作用------它是关于用户的、设置的还是其他什么的?
- 无法复用
只完成一项工作的用例很容易插入到任何需要的地方。交互器会随着时间的推移而增长,变得过于混乱,无法复用。
- 扩展性差
想象一下,有 10 个功能,每个功能都有自己的交互器,并且包含 5 个以上的方法。这需要记住很多东西,也需要管理很多代码。
- 逻辑混乱
当所有内容都放在一个文件中时,很容易意外地将不该放在一起的内容放在一起------例如,登录逻辑与个人资料更新逻辑就会混杂在一起。
用例(Use Case):从 UML 到 Android
我们刚才讨论的所有问题都可以通过使用用例(Use Case)方法完全解决或至少部分解决。但在深入探讨在Android上的实现之前,让我们先快速了解一下用例在 UML 术语中的含义。在 UML 中,用例是关于一个明确的意图------它代表一个特定的业务逻辑或功能。
查看下面的示例,了解它通常是如何可视化的:

基本上,我们即将实现的用例遵循与 UML 相同的理念:一个意图,一个用例(One Intent, one Use Case)。这个简单的规则帮助我们解决了之前的所有问题------测试变得更简单,代码更具可扩展性,整体也更易于维护。
现在,让我们使用用例方法改进上面的代码片段:
Kotlin
// Use-Case 1
class GetUserNameUseCase(
val repository: UserRepository,
) {
operator fun invoke(): String {
return repository.getUserName()
}
}
// Use-Case 2
class SaveUsernameUseCase(
val repository: UserRepository,
) {
operator fun invoke(name: String) {
repository.save(name)
}
}
因此,我们刚刚将 Interactor 拆分成了两个用例。我建议使用 Invoke 操作函数,这样我们就可以将它们的用例名称视为函数。此外,这种方式测试起来也更加容易。
以下 ViewModel 演示了用例的用法:
Kotlin
// ViewModel
class ViewModel(
private val getUsername: GetUserNameUseCase,
private val saveUsername: SaveUsernameUseCase
) {
val userName: StateFlow<String>
fun loadUsername() {
userName.update(getUsername())
}
fun updateUsername(name: String) {
saveUsername(name)
}
}
简单的测试可以写如下:
Kotlin
@Before
fun setup() {
useCase = GetUserNameUseCase(repository)
}
@Test
fun `should return username from repository`() {
`when`(repository.getUserName()).thenReturn("JohnDoe")
val result = useCase()
assertEquals("JohnDoe", result)
verify(repository).getUserName()
}
@Test
fun `should save username to repository`() {
val testName = "JaneDoe"
useCase(testName)
verify(repository).save(testName)
}
如何将其纳入真正的 Repos 应用程序
我的建议是始终从抽象开始。鉴于用例的性质(通常只有一个公共方法),我建议使用运算符函数(例如,invoke 在这里就很有效)。可以实现以下抽象:
Kotlin
// 模板接口作为所有用例的抽象
interface SuspendUseCase<in T, out O> {
suspend operator fun invoke(param: T): O
}
它的名称带有前缀Suspend(译注:这里应该是前缀,原文有错误),表示它处理暂停的结果。这种通用方法允许我们为参数和返回类型定义特定的类型。例如,以下特定的用例接口可以进一步使用:
Kotlin
// 特定的用例接口
interface GetReposUseCase: SuspendUseCase<Boolean, ResultWithFallback<List<DomainRepoEntity>>>
根据其名称,它可以用于获取 Repos,并抛出其 Result 包装器。客户端可以以函数式的方式使用它(例如,val repos = getRepos(...))。它的实现如下:
Kotlin
// GetReposUseCase 的简单实现
internal class GetReposUseCaseImpl(
private val mapper: Mapper,
private val repository: ReposRepository
) : GetReposUseCase {
override suspend operator fun invoke(param: Boolean):
ResultWithFallback<List<DomainRepoEntity>> =
repository.getRepos(param).map(mapper::map)
}
现在,让我们让任务更具挑战性,并在用例结构中添加一个额外的抽象层。我想要实现仅与存储库交互的用例。这些用例将包含存储库的实例作为泛型中的附加参数类型。让我们看一下以下代码片段:
Kotlin
// 模板接口作为所有用例的抽象
interface SuspendUseCase<in T, out O> {
suspend fun execute(param: T): O
}
我修改了execute方法,使其能够从另一个抽象子类中调用。以下是更新后的代码片段:
Kotlin
// 扩展 SuspendUseCase 接口
interface RepositoryUseCase<in I, out O, R : Repository> :
SuspendUseCase<I, O> {
var repository: R
}
每个实现类型R的存储库的子类都将遵守该接口(Contract)。最合适的方法是使用抽象类。让我们实现一个抽象类来实现这一点:
Kotlin
// 确保遵守接口的抽象类
abstract class BaseRepositoryUseCase<in I, out O, R : Repository>(override var repository: R) :
RepositoryUseCase<I, O, R> {
suspend operator fun invoke(params: I? = null): O {
// 这是对 SuspendUseCase 接口的调用
return execute(params)
}
}
我们需要做的就是扩展 BaseRepositoryUseCase ,遵循其泛型接口,提供一个输入类、一个输出类以及一个被覆写的Repo实例。以下实现已经足够:
Kotlin
// 用例的实现
class GetReposUseCase(repository: RepoRepository):
BaseRepositoryUseCase<Boolean,ResultWithFallback<List<DomainRepoEntity>>, RepoRepository>(repository) {
override suspend fun execute(param: Boolean): ResultWithFallback<List<DomainRepoEntity>> =
// 使用存储库获取并返回结果,如 repository.getData()
}
}
结论
因此,我们探索了从零开始使用用例(Use Case)、捕捉客户意图(Client Intent)的最佳方法。我演示了如何以功能性的方式实现和使用它们,使其易于测试和集成。
欢迎搜索并关注 公众号「稀有猿诉」 获取更多的优质文章!
保护原创,请勿转载!