使用用例(Use Case)以让Android代码更简洁

本文译自「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)的最佳方法。我演示了如何以功能性的方式实现和使用它们,使其易于测试和集成。

欢迎搜索并关注 公众号「稀有猿诉」 获取更多的优质文章!

保护原创,请勿转载!

相关推荐
阿巴斯甜4 小时前
Android 报错:Zip file '/Users/lyy/develop/repoAndroidLapp/l-app-android-ble/app/bu
android
Kapaseker4 小时前
实战 Compose 中的 IntrinsicSize
android·kotlin
xq95275 小时前
Andorid Google 登录接入文档
android
黄林晴6 小时前
告别 Modifier 地狱,Compose 样式系统要变天了
android·android jetpack
冬奇Lab19 小时前
Android触摸事件分发、手势识别与输入优化实战
android·源码阅读
城东米粉儿1 天前
Android MediaPlayer 笔记
android
Jony_1 天前
Android 启动优化方案
android
阿巴斯甜1 天前
Android studio 报错:Cause: error=86, Bad CPU type in executable
android
张小潇1 天前
AOSP15 Input专题InputReader源码分析
android
_小马快跑_1 天前
Kotlin | 协程调度器选择:何时用CoroutineScope配置,何时用launch指定?
android