详解Android官方架构中UseCase

Isadora ·
更新时间:2024-09-20
· 1512 次阅读

目录

1. UseCase 的用途

2. UseCase 的特点

2.1 不持有状态

2.2 单一职责

2.3 可有可无

3. 如何定义 UseCase

3.1 Optional or Mandatory?

3.2 Class or Object?

3.3 Class or Function?

3.4 Function interface ?

4. 总结

1. UseCase 的用途

Android 最新的架构规范中,引入了 Domain Layer(译为领域层or网域层),建议大家使用 UseCase 来封装一些复杂的业务逻辑。

传统的 MVVM 架构中,我们习惯用 ViewModel 来承载业务逻辑,随着业务规模的扩大,ViewModel 变得越来越肥大,职责不清。

Clean Architecture 提出的关注点分离和单一职责(SRP)的设计原则被广泛认可,因此 Android 在最新架构中引入了 Clean Architecture 中 UseCase 的概念。ViewModel 归属 UI Layer,更加聚焦 UiState 的管理,UI 无关的业务逻辑下沉 UseCase,UseCase 与 ViewModel 解耦后,也可以跨 ViewModel 提供公共逻辑。

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

NIA: github.com/android/now…

2. UseCase 的特点

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

2.1 不持有状态

可以定义自己的数据结构类型,但是不能持有状态实例,像一个纯函数一样工作。甚至直接推荐大家将逻辑重写到 invoke 方法中,像调用函数一样调用实例。

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

2.2 单一职责

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

下面 NIA 中所有 UseCases:

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 的方式,有利于代码更加函数式

以上就是详解Android官方架构中UseCase的详细内容,更多关于Android官方架构UseCase 的资料请关注软件开发网其它相关文章!



Android 架构

需要 登录 后方可回复, 如果你还没有账号请 注册新账号