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

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

保护原创,请勿转载!

相关推荐
陈旭金-小金子2 小时前
发现 Kotlin MultiPlatform 的一点小变化
android·开发语言·kotlin
二流小码农4 小时前
鸿蒙开发:DevEcoStudio中的代码提取
android·ios·harmonyos
江湖有缘4 小时前
使用obsutil工具在OBS上完成基本的数据存取【玩转华为云】
android·java·华为云
移动开发者1号5 小时前
Android 多 BaseUrl 动态切换策略(结合 ServiceManager 实现)
android·kotlin
移动开发者1号5 小时前
Kotlin实现文件上传进度监听:RequestBody封装详解
android·kotlin
AJi8 小时前
Android音视频框架探索(三):系统播放器MediaPlayer的创建流程
android·ffmpeg·音视频开发
柿蒂9 小时前
WorkManager 任务链详解:优雅处理云相册上传队列
android
峥嵘life10 小时前
Android xml的Preference设置visibility=“gone“ 无效分析解决
android·xml
用户20187928316710 小时前
通俗故事:驱动二进制文件在AOSP中的角色
android