项目经理和技术总监可能是同一个人。这种方式可以非常容易地应用在小型团队中,这样的团队可能有3~6个程序员,而在更大的项目中则不容易获得应用。原因有两个:第一,同时具有管理技能和技术技能的人很难找到。第二,更大的项目中,每个角色必须全职工作,甚至还要加班。——《人月神话》
什么是 Domain 层
中间层(Domain)是用在UI & Data 之间的可选层。在两种情况下,可以考虑增加 Domain 层:
- 复杂业务逻辑封装
- 提升代码可读性
- 提升可测试性
- 避免类的体积膨胀
- 业务逻辑在不同 ViewModel 之间复用
- 避免代码重复
Domain 层的命名
Domain 层是概念,而 UseCase 则是它的具体实现。
命名格式:动词现代时+名词+UseCase,例如
FormatDateUseCaseLogOutUserUseCaseGetLatestNewsWithAuthorsUseCaseMakeLoginRequestUseCase
Domain 层所处的位置与依赖关系
Domain 层位于 UI 层的 ViewModel 与 Data 层的 Repository 之间,它依赖 Repository,并向 ViewModel 提供数据操作的接口,在两者之间起到隔离作用。
思考这样一个用例:UseCase 从新闻 Repository 中获取新闻列表、从作者 Repository 中获取作者信息,并且将两者组装在一起。
class GetLatestNewsWithAuthorsUseCase(
private val newsRepository: NewsRepository,
private val authorsRepository: AuthorsRepository
) { /* ... */ }
不同的 UseCase 之间,可以存在依赖关系,例如这里增加上一个日期格式化的用例,用来在 UI 层展示不同市区的日期信息。
class GetLatestNewsWithAuthorsUseCase(
private val newsRepository: NewsRepository,
private val authorsRepository: AuthorsRepository,
private val formatDateUseCase: FormatDateUseCase
) { /* ... */ }
利用 Kotlin 中的 invoke 进行操作符重载
可以利用invoke对 UseCase 类进行重载,使其具备同名的函数,从而简化代码写法。 例如在下面这个例子中,可以将 formatDateUseCase.format(date) 简化为 formatDateUseCase(data)。
// ===> 定义 UseCase
class FormatDateUseCase(userRepository: UserRepository) {
private val formatter = SimpleDateFormat(
userRepository.getPreferredDateFormat(),
userRepository.getPreferredLocale()
)
operator fun invoke(date: Date): String { // ===> 重载 FormatDateUseCase()函数
return formatter.format(date)
}
}
// ===> 使用该 UseCase
class MyViewModel(formatDateUseCase: FormatDateUseCase) : ViewModel() {
init {
val today = Calendar.getInstance()
val todaysDate = formatDateUseCase(today) // ===> 直接通过参数对象调用 invoke 函数
/* ... */
}
}
线程切换
UseCase 应当对自身的线程切换负责,做到主线程安全,防止因为耗时操作产生 ANR。
对于需要耗时操作的场景,可以在参数里传入(借助依赖翻转)Dispatcher,将控制权交由外部。
另外一种做法是在参数里传入 CoroutineScope,这样便于从外部控制任务取消。
class MyUseCase(
private val defaultDispatcher: CoroutineDispatcher = Dispatchers.Default
) {
suspend operator fun invoke(...) = withContext(defaultDispatcher) { // ===> 不新建协程,只是切换上下文
// Long-running blocking operations happen on a background thread.
}
}
常见的业务需求开发用例
不要在 Util 类中填写业务逻辑: 在某些情况下,UseCase 中存在的逻辑可以改为使用 Util 类中的静态方法。然而,后者并不推荐,因为 Util 类通常很难找到,而且它们的功能也很难发现。此外,UseCase 可以共享基类中的常见功能,例如线程和错误处理,这可以使规模更大的团队受益。
考虑前文提到的,将新闻与作者信息进行拼装的GetLatestNewsWithAuthorsUseCase。
/**
* This use case fetches the latest news and the associated author.
*/
class GetLatestNewsWithAuthorsUseCase(
private val newsRepository: NewsRepository,
private val authorsRepository: AuthorsRepository,
private val defaultDispatcher: CoroutineDispatcher = Dispatchers.Default
) {
suspend operator fun invoke(): List<ArticleWithAuthor> =
withContext(defaultDispatcher) {
val news = newsRepository.fetchLatestNews()
val result: MutableList<ArticleWithAuthor> = mutableListOf()
// ===> 由于这里没有并行,导致 UseCase 执行时非常耗时
for (article in news) {
// The repository exposes suspend functions
val author = authorsRepository.getAuthor(article.authorId)
result.add(ArticleWithAuthor(article, author))
}
result // ===> withContext 代码块需要最终显式返回
}
}
仅仅在需要时才添加 Domain 层
在代码中一旦设计了 Domain 层,就必须在后续的逻辑中坚持使用它,而不是试图绕过。这是维护架构统一性所必须付出的代价,要么不用,要么一直用。
因此,请谨记仅仅在必需 Domain 层时才添加它。