Arch - Domain Layer
Android 官方 App 架构指南系列文章
概述
📌 名词解释
Domain Layer
:介于 Data Layer 和 UI Layer 之间的可选层,由UseCase
组成。UseCase
:是组成Domain Layer
的具体执行类,其仅通过一个 public 函数提供能力。
Domain Layer 主要是可以做以下两件事情:
- 封装复杂的业务逻辑,避免出现大型类
- 封装多 ViewModel 通用的业务逻辑,避免代码重复
需要回答的问题:
- 如何定义封装 UseCase 类?
- UseCase 之间如何交互?
- UseCase 如何提供数据?
为了使 UseCase
保持简单轻量化,每个 UseCase
都应仅负责单个功能,且不应包含可变数据。
如何定义 UseCase ?
💡 命名规范:
在本指南中,用例以其负责的单一操作命名。具体命名惯例如下:
一般现在时动词 + 名词/内容(可选)+ UseCase。
如:FormatDateUseCase
、LogOutUserUseCase
、GetLatestNewsWithAuthorsUseCase
或 MakeLoginRequestUseCase
。
在 Kotlin 中,您可以通过使用 operato
修饰符定义 invoke()
函数,将用例类实例作为函数进行调用。请参阅以下示例:
class FormatDateUseCase(userRepository: UserRepository) {
// 1、operator + invoke 来提供统一函数入口
// 2、传参及返回值可以自由定义
operator fun invoke(date: Date): String {
return userRepository.format(date)
}
}
复制代码
这种方式对于对传参及返回值的扩展性都比较好,若采用统一的封装可能会破坏其灵活性,而且会增加一些模板代码。
来自网域层的用例必须具有主线程安全性的*,*也就是可以在主线程上被调用。但是这并不是说要在 Domain 层做线性调度相关的事情,而是由数据的产生方做处理。若 DataLayer 中没有处理线程处理的的,需要在 Domain 中添加一层防护。
UseCase 之间如何交互?
UseCase
通常是调用 Repository
逻辑,也可以对多个 Repository
进行组合调用,也可以调用其他的 UseCase
。其依赖关系大致如下:
UseCase
除了封装重复的代码之外,还可以整合多个 Repository
中的逻辑,比如将 NewsRepository
与 AuthorsRepository
组合成一个新的数据列表进行返回。示例代码如下:
/**
* 将新闻与作者关联起来
*/
class GetLatestNewsWithAuthorsUseCase(
private val newsRepository: NewsRepository,
private val authorsRepository: AuthorsRepository,
private val defaultDispatcher: CoroutineDispatcher = Dispatchers.Default
) {
suspend operator fun invoke(): List<ArticleWithAuthor> = withContext(defaultDispatcher) {
newsRepository.fetchLatestNews().map { article ->
val author = authorsRepository.getAuthor(article.authorId)
// 整合两种数据
ArticleWithAuthor(article, author)
}
}
}
复制代码
UseCase 如何提供数据 ?
UseCase
处理业务的结果一般是要通过数据的形式通知到调用方,大致会有以下几种方式提供数据。
直接返回
最常见的方式就是直接返回数据了,如下:
class UseCaseNormal(userRepository: UserRepository) {
operator fun invoke(date: Date): String {
return userRepository.format(date)
}
}
复制代码
这种方式对耗时操作而言,可能会存在主线程调用安全的问题。
Suspend 函数
对于 Kotlin 而言可以使用 suspend
函数替代:
class MyUseCaseWithSuspend {
suspend operator fun invoke(): MyUiState {
val result: Boolean = doSomething()
return if (result) {
MyUiState.Successful("OK")
} else {
MyUiState.Error(-1)
}
}
}
复制代码
Callback
对于 Kotlin 而言比较常见的方式就是回调了,如将 MyUiState
通过回调的方式通知到调用者:
class MyUseCaseWithCallback {
operator fun invoke(onData: (MyUiState) -> Unit) {
val result: Boolean = doSomething()
if(result) {
onData(MyUiState.Successful("OK"))
} else {
onData(MyUiState.Error(-1))
}
}
}
复制代码
回调方式不仅可以处理一次性任务,还可以处理多次任务,比如进度条的结果回调。但是回调使用不当可能会导致“回调地狱” 问题。
Flow
Kotlin 中对于多次结果回调可以使用 Flow,如下:
class MyUseCaseWithFlow {
operator fun invoke(): Flow<MyUiState> = flow {
emit(MyUiState.Loading)
val result: Boolean = doSomething()
if (result) {
emit(MyUiState.Successful("OK"))
} else {
emit(MyUiState.Error(-1))
}
}
}
复制代码
上述四种方式可以根据其具体的使用场景自行选择,但是都可以实现的方式,建议按照维护成本由低到高采用,建议顺序如下:直接返回 > suspend 函数 > flow > Callback 。
总结
Domain Layer 主要是可以做以下两件事情:
- 封装复杂的业务逻辑,避免出现大型类
- 封装多 ViewModel 通用的业务逻辑,避免代码重复
想要做好上述两件事情,就需要回答的问题:
- 如何定义封装 UseCase 类?
- UseCase 之间如何交互?
- UseCase 如何提供数据?
核心总结如下:
- 使用
operato
+invoke
的方式定义 UseCase,保证其灵活性及扩展性; UseCase
可以依赖UseCase
,也可以依赖一个或多个Repository
;UseCase
暴露数据的方式有直接返回、suspend 函数、Callback、Flow,建议尽量避免采用 Callback 形式。
更多内容会第一时间发布在微信公众号中,欢迎大家关注: