网域层是位于界面层和数据层之间的可选层。
网域层负责封装复杂的业务逻辑,或者由多个 ViewModel 重复使用的简单业务逻辑。此层是可选的,因为并非所有应用都有这类需求。因此,您应仅在需要时使用该层,例如处理复杂逻辑或支持可重用性。
网域层具有以下优势:
- 避免代码重复。
- 改善使用网域层类的类的可读性。
- 改善应用的可测试性。
- 让您能够划分好职责,从而避免出现大型类。
为了使这些类保持简单轻量化,每个用例都应仅负责单个功能,且不应包含可变数据。您应在界面或数据层中处理可变的数据。
本指南中的命名惯例
在本指南中,用例以其负责的单一操作命名。具体命名惯例如下:
动词原形 + 名词/内容(可选)+ 用例。
例如:FormatDateUseCase
、LogOutUserUseCase
、GetLatestNewsWithAuthorsUseCase
或 MakeLoginRequestUseCase
。
依赖关系
在典型的应用架构中,用例类适合界面层的 ViewModel 与数据层的仓库。这意味着用例类通常依赖于仓库类,并且它们与界面层的通信方式和仓库与界面层的通信方式相同 - 使用回调(Java 代码)或协程(Kotlin 代码)。如需了解详情,请参阅数据层页面。
例如,您的应用中可能有一个用例类,用于从新闻仓库和作者仓库中提取数据并对它们进行合并:
class GetLatestNewsWithAuthorsUseCase(
private val newsRepository: NewsRepository,
private val authorsRepository: AuthorsRepository
) { /* ... */ }
由于用例包含可重复使用的逻辑,因此其他用例也可以使用这些用例。在网域层有多个用例层级是正常现象。例如,如果界面层中的多个类依赖时区在屏幕上显示适当的消息,则以下示例中定义的用例可以使用 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 文档。
生命周期
用例没有自己的生命周期,而是受限于使用它们的类。这意味着,您可以从界面层中的类、服务或 Application
类本身调用用例。由于用例不应包含可变数据,因此您每次将用例类作为依赖项传递时,都应该创建一个新实例。
线程处理
来自网域层的用例必须是主线程安全的;换句话说,从主线程调用它们必须是安全的。如果用例类执行长期运行的阻塞操作,那么它们负责将该逻辑移至适当的线程。不过,在执行此操作之前,请检查这些阻塞操作是否最好放置在层次结构的其他层中。通常,数据层中会进行复杂的计算,以支持可重用性或缓存。例如,如果某项结果需要缓存起来,以便在应用的多个屏幕上重复使用,那么在数据层中对大列表执行资源密集型操作比在网域层中执行会更好。
以下示例显示了一个在后台线程上执行工作的用例:
class MyUseCase(
private val defaultDispatcher: CoroutineDispatcher = Dispatchers.Default
) {
suspend operator fun invoke(...) = withContext(defaultDispatcher) {
// Long-running blocking operations happen on a background thread.
}
}
常见任务
本部分介绍如何执行常见网域层任务。
可重复使用的简单业务逻辑
您应将界面层中存在的可重复业务逻辑封装到用例类中。这样您就可以更轻松地在使用该逻辑的所有位置应用任何更改,以及单独测试该逻辑。
以前面介绍的 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
列表中的所有项;因此,即使数据层是主线程安全的,此工作也不应该阻塞主线程,因为您并不知道它会处理多少项。正因如此,该用例使用默认调度程序将工作移到后台线程。
其他使用方
除了界面层之外,其他类(如服务和 Application
类)也可以重复使用网域层。此外,如果其他平台(如 TV 或 Wear)与移动应用共享代码库,那么它们的界面层还可以重复使用用例,从而获得网域层的上述所有优势。
数据层访问权限限制
实现网域层时,还需要考虑应该仍然允许从界面层直接访问数据层,还是应该强制要求所有访问都必须通过网域层进行。
设置此限制的好处之一是,这会阻止界面绕过网域层逻辑,例如,当您对针对数据层的每个访问请求执行分析日志记录时。
不过,潜在的重要缺陷在于,您不得不添加相关用例,即使只是对数据层进行简单的函数调用时也是如此,而这可能会增加复杂性,却几乎没有什么好处。
一种很好的方法是仅在需要时才添加用例。如果您发现界面层几乎完全通过用例访问数据,那么仅以这种方式访问数据可能是合理的。
最终,是否限制对数据层的访问权限取决于您的具体代码库,以及您倾向于采用更严格的规则还是更灵活的方法。
测试
测试网域层时,请遵循通用测试指南。对于其他界面测试,开发者通常使用虚构代码库,因此在测试网域层时,最好使用虚构代码库。
示例
以下 Google 示例演示了如何使用网域层。请查看这些示例,了解如何实际运用本指南: