领域层是一个可选层,位于 UI 层和数据层之间。
领域层负责封装复杂的业务逻辑,或者被多个 ViewModel 重复使用的简单业务逻辑。这个层是可选的,因为并非所有应用都需要这些功能。只有在需要时才应使用它——例如,处理复杂性或偏好可重用性。
领域层提供以下好处:
- 它避免了代码重复。
- 它提高了使用领域层类的类的可读性。
- 它提高了应用的可测试性。
- 它通过允许您拆分职责来避免大型类。
为了保持这些类的简单和轻量级,每个用例都应只负责单一功能,并且它们不应包含可变数据。您应该在您的 UI 或数据层中处理可变数据。
本指南中的命名约定
在本指南中,用例以它们负责的单个操作命名。约定如下:
现在时态的动词 + 名词/宾语(可选) + UseCase。
例如:FormatDateUseCase
、LogOutUserUseCase
、GetLatestNewsWithAuthorsUseCase
或 MakeLoginRequestUseCase
。
依赖项
在典型的应用架构中,用例类位于 UI 层的 ViewModel 和数据层的存储库之间。这意味着用例类通常依赖于存储库类,并且它们与 UI 层的通信方式与存储库相同——使用回调(对于 Java)或协程(对于 Kotlin)。要了解更多信息,请参阅数据层页面。
例如,在您的应用中,您可能有一个用例类,它从新闻存储库和作者存储库中获取数据,并将它们组合在一起。
class GetLatestNewsWithAuthorsUseCase(
private val newsRepository: NewsRepository,
private val authorsRepository: AuthorsRepository
) { /* ... */ }
因为用例包含可重用的逻辑,所以它们也可以被其他用例使用。在领域层中拥有多个级别的用例是很正常的。例如,下面示例中定义的用例如果 UI 层的多个类依赖于时区在屏幕上显示正确的消息,则可以使用 FormatDateUseCase
用例。
class GetLatestNewsWithAuthorsUseCase(
private val newsRepository: NewsRepository,
private val authorsRepository: AuthorsRepository,
private val formatDateUseCase: FormatDateUseCase
) { /* ... */ }
在 Kotlin 中调用用例
在 Kotlin 中,您可以通过使用 operator
修饰符定义 invoke()
函数,使用例类实例可作为函数调用。请参见以下示例:
class FormatDateUseCase(userRepository: UserRepository) {
private val formatter = SimpleDateFormat(
userRepository.getPreferredDateFormat(),
userRepository.getPreferredLocale()
)
operator fun invoke(date: Date): String {
return formatter.format(date)
}
}
在此示例中,FormatDateUseCase
中的 invoke()
方法允许您像调用函数一样调用类的实例。invoke()
方法不受任何特定签名的限制——它可以接受任意数量的参数并返回任何类型。您也可以在类中使用不同的签名重载 invoke()
。您可以按如下方式调用上面示例中的用例:
class MyViewModel(formatDateUseCase: FormatDateUseCase) : ViewModel() {
init {
val today = Calendar.getInstance()
val todaysDate = formatDateUseCase(today)
/* ... */
}
}
要了解有关 invoke()
运算符的更多信息,请参阅Kotlin 文档。
生命周期
用例没有它们自己的生命周期。相反,它们的范围限定为使用它们的类。这意味着您可以从 UI 层的类、服务或 Application
类本身调用用例。因为用例不应包含可变数据,所以您应该在每次将用例类作为依赖项传递时创建一个新的实例。
线程
领域层的用例必须是主线程安全的;换句话说,它们必须可以在主线程中安全调用。如果用例类执行长时间阻塞操作,则它们负责将该逻辑移动到合适的线程。但是,在此之前,请检查这些阻塞操作是否最好放在层次结构的其他层中。通常,复杂计算发生在数据层中,以鼓励可重用性或缓存。例如,如果结果需要缓存以便在应用的多个屏幕上重用它,则对大型列表进行资源密集型操作最好放在数据层中,而不是在领域层中。
以下示例显示了一个在后台线程上执行其工作的用例:
class MyUseCase(
private val defaultDispatcher: CoroutineDispatcher = Dispatchers.Default
) {
suspend operator fun invoke(...) = withContext(defaultDispatcher) {
// Long-running blocking operations happen on a background thread.
}
}
常见任务
本节介绍如何执行常见的领域层任务。
可复用的简单业务逻辑
您应该将UI层中可重复使用的业务逻辑封装在用例类中。这使得在逻辑使用到的所有地方应用任何更改都更容易。它还允许您独立测试逻辑。
考虑前面描述的FormatDateUseCase
示例。如果将来关于日期格式的业务需求发生变化,您只需要在一个中心位置更改代码。
组合仓库
在一个新闻应用程序中,您可能拥有NewsRepository
和AuthorsRepository
类,它们分别处理新闻和作者数据操作。NewsRepository
公开的Article
类仅包含作者的姓名,但您希望在屏幕上显示有关作者的更多信息。作者信息可以从AuthorsRepository
获取。
由于逻辑涉及多个仓库并且可能变得复杂,因此您创建了一个GetLatestNewsWithAuthorsUseCase
类,以将逻辑从ViewModel中抽象出来并使其更易于阅读。这也使逻辑更容易独立测试,并可在应用程序的不同部分重复使用。
/**
* 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()
// This is not parallelized, the use case is linearly slow.
for (article in news) {
// The repository exposes suspend functions
val author = authorsRepository.getAuthor(article.authorId)
result.add(ArticleWithAuthor(article, author))
}
result
}
}
该逻辑映射news
列表中的所有项目;因此,即使数据层是主线程安全的,这项工作也不应该阻塞主线程,因为您不知道它将处理多少项目。这就是为什么用例使用默认调度程序将工作移到后台线程。
其他使用者
除了UI层之外,领域层还可以被其他类(例如服务和Application
类)重复使用。此外,如果其他平台(例如电视或可穿戴设备)与移动应用程序共享代码库,它们的UI层也可以重复使用用例,以获得上述所有领域层优势。
数据层访问限制
实现领域层时,另一个需要考虑的问题是是否仍然允许UI层直接访问数据层,或者强制所有内容都通过领域层。
实施此限制的一个优点是它可以阻止您的UI绕过领域层逻辑,例如,如果您在对数据层的每次访问请求上执行分析日志记录。
但是,潜在的重大缺点是它会强制您添加用例,即使它们只是对数据层的简单函数调用,这也会增加复杂性,而好处却很少。
一个好的方法是仅在需要时添加用例。如果您发现您的UI层几乎完全通过用例访问数据,那么只通过这种方式访问数据可能是有意义的。
最终,是否限制对数据层的访问取决于您的个人代码库,以及您是偏好严格的规则还是更灵活的方法。
测试
测试领域层时,适用通用的测试指南。对于其他UI测试,开发人员通常使用模拟仓库,在测试领域层时使用模拟仓库也是一个好习惯。
示例
以下Google示例演示了领域层的使用。您可以探索它们以了解此指南的实践。