1. UseCase 的用处

Android 最新的架构标准中,引进了 Domain Layer(译为领域层or网域层),主张咱们运用 UseCase 来封装一些复杂的事务逻辑。

Android 最新架构:developer.android.com/topic/archi…

传统的 MVVM 架构中,咱们习惯用 ViewModel 来承载事务逻辑,跟着事务规模的扩展,ViewModel 变得越来越肥大,责任不清。

Android 官方架构中的 UseCase 该怎么写?

Clean Architecture 提出的关注点别离和单一责任(SRP)的规划准则被广泛认可,因此 Android 在最新架构中引进了 Clean Architecture 中 UseCase 的概念。ViewModel 归属 UI Layer,愈加聚焦 UiState 的办理,UI 无关的事务逻辑下沉 UseCase,UseCase 与 ViewModel 解耦后,也能够跨 ViewModel 供给公共逻辑。

Android 架构早期的示例代码 todo-app 中曾经引进过 UseCase 的概念,最新架构中只不过是将 UseCase 的思想更明晰了,最新的 UseCase 示例能够从官方的 NIA 中学习。

  • todo-app: github.com/android/arc…
  • NIA: github.com/android/now…

2. UseCase 的特色

官方文档以为 UseCase 应该具有以下几个特色:

2.1 不持有状况

能够界说自己的数据结构类型,可是不能持有状况实例,像一个纯函数相同作业。甚至直接引荐咱们将逻辑重写到 invoke 办法中,像调用函数相同调用实例。

下面是 NIA 中的一个示例:GetRecentSearchQueriesUseCase

Android 官方架构中的 UseCase 该怎么写?

2.2 单一责任

严格遵守单一责任,一个 UseCase 只做一件作业,甚至其命名便是一个具体行为。扫一眼 UseCase 的文件目录大概就知道 App 的大概功能了。

下面 NIA 中所有 UseCases:

Android 官方架构中的 UseCase 该怎么写?

2.3 可有可无

官方文档中将 UseCase 界说为可选的角色,按需界说。简单的事务场景中允许 UI 直接访问 Repository。假如咱们将 UseCase 作为 UI 与 Data 阻隔的角色,那么工程中会呈现许多没有太大价值的 UseCase ,可能就只有一行调用 Repoitory 的代码。

3. 怎么界说 UseCase

如上所述,官方文档虽然对 UseCase 给出了一些根本界说,可是毕竟是一个新新生概念,许多人在真正去写代码的时候仍然会感觉不明晰,短少有用指引。在终究怎么界说 UseCase 这个问题上,还有待咱们更广泛的讨论,形成可参阅的一致。本文也是带着这个意图而生,算是抛砖引玉吧。

3.1 Optional or Mandatory?

首先,官方文档以为 UseCase 是可选的,虽然其初衷是好的,咱们都不期望呈现太多 One-Liner 的 UseCase,可是作为一个架构标准切忌不置可否,这种“可有可无”的规则其结局往往便是“无”。

事务刚起步时由于比较简单往往界说在 Repository 中,跟着事务规模的扩展,应该适当得增加 UseCase 封装一些复杂的事务逻辑,可是实际项目中此刻的重构本钱会让开发者变得“懒惰”,UseCase 终究难产。

那抛弃 UseCase 呢?这可能会形成 Repository 的责任不清和无限膨胀,而且 Repository 往往不止有一个办法, ViewModel 直接依靠 Repository 也违反了 SOLID 中的另一个重要准则 ISP ,ViewModel 会由于不相关的 Repository 改动导致从头编译。

ISP(Interface Segregation Principle,接口阻隔准则) 要求将接口别离成更小的和更具体的接口,以便调用方只需知道其需求运用的办法。这能够进步代码的灵活性和可重用性,并减少代码的依靠性和耦合性。

为了降低前期判别本钱和后续重构本钱,假如咱们有事务持续壮大的预期,那不妨考虑将 UseCase 作为强制选项。当然,最好这需求研讨怎么降低 UseCase 带来的模板代码。

3.2 Class or Object?

官方主张运用 Class 界说 UseCase,每次运用都实例化一个新目标,这会做成一些重复开销,那么可否用 object 界说 UseCase 呢?

UseCase 理论上能够作为单例存在,但 Class 相对于 Object 有以下两个优势:

  • UseCase 期望像纯函数相同作业,普通 Class 能够保证每次运用时都会创立一个新的实例,从而避免状况共享和副效果等问题。
  • 普通类能够经过结构参数注入不同的 Repository,UseCase 更利于复用和单元测试

假如咱们强烈期望 UseCase 有更长的生命周期,那借助 DI 框架,普通类也能够简单的支撑。例如 Dagger 中只要添加 @Singleton 注解即可

@Singleton
class GetRecentSearchQueriesUseCase @Inject constructor(
    private val recentSearchRepository: RecentSearchRepository,
) {
    operator fun invoke(limit: Int = 10): Flow<List<RecentSearchQuery>> =
        recentSearchRepository.getRecentSearchQueries(limit)
}

3.3 Class or Function?

既然咱们想像函数相同运用 UseCase ,那为什么不直接界说成 Function 呢?比如像下面这样

fun GetRecentSearchQueriesUseCase : Flow<List<RecentSearchQuery>> 

这的确遵从了 FP 的准则,但又丧失了 OOP 封装性的优势:

  • UseCase 往往需求依靠 Repository 目标,一个 UseCase Class 能够将 Repository 封装为成员存储。而一个 UseCase Function 则需求调用方经过参数传入,运用本钱高不说,假如 UseCase 依靠的 Repository 的类型或许数量发生变化了,调用方需求跟着修正
  • 函数起不到阻隔 UI 和 Data 的效果,ViewModel 仍然需求直接依靠 Repository,为 UseCase 传参
  • UseCase Class 能够界说一些 private 的办法,相对于 Function 更能担任一些复杂逻辑的完成

可见,在 UseCase 的界说上 Function 没法取代 Class。当然 Class 也带来一些坏处:

  • 露出多个办法,破坏 SRP 准则。所以官方引荐用 verb in present tense + noun/what (optional) + UseCase 动词命名,也是想让责任更明晰。
  • 带着可变状况,这是咱们写 OOP 的惯性思想
  • 样板代码多

3.4 Function interface ?

经过前面的分析咱们知道:UseCase 的界说需求兼具 FP 和 OOP 的优势。这让我想到了 Function(SAM) Interface 。Function Interface 是一个单办法的接口,能够低本钱创立一个匿名类目标,保证目标只能有一个办法,一起具有必定封装性,能够经过“闭包”依靠 Repository。此外,Kotlin 对 SAM 供给了简化写法,必定程度也减少了样板代码。

Functional (SAM) interfaces: kotlinlang.org/docs/fun-in…

改用 Function interface 界说 GetRecentSearchQueriesUseCase 的代码如下:

fun interface GetRecentSearchQueriesUseCase : () -> Flow<List<RecentSearchQuery>>

用它创立 UseCase 实例的一起,完成函数中的逻辑

val recentSearchQueriesUseCase = GetRecentSearchQueriesUseCase {
    //...
}

我在函数完成中怎么 Repository 呢?这要靠 DI 容器获取。官方示例代码中都运用 Hilt 来解耦 ViewModel 与 UseCase 的,ViewModel 不关心 UseCase 的创立细节。下面是 NIA 的代码, GetRecentSearchQueriesUseCase 被主动注入到 SearchViewModel 中。

@HiltViewModel
class SearchViewModel @Inject constructor(
    recentSearchQueriesUseCase: GetRecentSearchQueriesUseCase // UseCase 注入 VM
    //...
) : ViewModel() { 
    //...
}

Function interface 的 GetRecentSearchQueriesUseCase 没有结构函数,需求经过 Dagger 的 @Module 安装到 DI 容器中,provideGetRecentSearchQueriesUseCase 参数中的 RecentSearchRepository 能够从容器中主动获取运用。

@Module
@InstallIn(ActivityComponent::class)
object UseCaseModule {
    @Provides
    fun provideGetRecentSearchQueriesUseCase(recentSearchRepository: RecentSearchRepository) =
        GetRecentSearchQueriesUseCase { limit ->
            recentSearchRepository.getRecentSearchQueries(limit)
        }
}

当时用 Koin 作为 DI 容器时也没问题,代码如下:

single<GetRecentSearchQueriesUseCase> {
    GetRecentSearchQueriesUseCase { limit ->
       recentSearchRepository.getRecentSearchQueries(limit) 
    }
}

4. 总结

UseCase 作为官方架构中的新概念,尚没有彻底家喻户晓,需求不断探究合理的运用方法,本文给出一些根本考虑:

  • 考虑到架构的扩展性,引荐在 ViewModel 与 Repository 之间强制引进 UseCase,即便眼下的事务逻辑并不复杂

  • UseCase 不持有可变状况但依靠 Repository,需求兼具 FP 与 OOP 的特性,更适合用 Class 界说而非 Function

  • 在引进 UseCase 之前应该先引进 DI 框架,保证 ViewModel 与 UseCase 的耦合。

  • Function Interface 是 Class 之外的另一种界说 UseCase 的方法,有利于代码愈加函数式