领域层

领域层是一个*可选*层,它位于 UI 层和数据层之间。

When it is included, the optional domain layer provides dependencies to
    the UI layer and depends on the data layer.
图 1. 领域层在应用架构中的作用。

领域层负责封装复杂的业务逻辑,或由多个 ViewModel 重用的简单业务逻辑。此层是可选的,因为并非所有应用都有这些要求。您只应在需要时使用它,例如为了处理复杂性或优先考虑可重用性。

领域层提供以下好处:

  • 它避免了代码重复。
  • 它提高了使用领域层类的类的可读性。
  • 它提高了应用的可测试性。
  • 它允许您通过职责划分来避免大型类。

为了使这些类保持简单和轻量,每个用例应仅负责单一功能,并且它们不应包含可变数据。您应该改为在 UI 层或数据层中处理可变数据。

本指南中的命名约定

在本指南中,用例以其负责的单一操作命名。约定如下:

动词现在时态 + 名词/什么(可选) + UseCase

例如:FormatDateUseCaseLogOutUserUseCaseGetLatestNewsWithAuthorsUseCaseMakeLoginRequestUseCase

依赖项

在典型的应用架构中,用例类位于 UI 层的 ViewModel 和数据层的存储库之间。这意味着用例类通常依赖于存储库类,并且它们与 UI 层通信的方式与存储库相同——使用回调(对于 Java)或协程(对于 Kotlin)。要了解更多信息,请参阅数据层页面

例如,在您的应用中,您可能有一个用例类,它从新闻存储库和作者存储库中获取数据,并将它们组合起来:

class GetLatestNewsWithAuthorsUseCase(
  private val newsRepository: NewsRepository,
  private val authorsRepository: AuthorsRepository
) { /* ... */ }

由于用例包含可重用逻辑,它们也可以被其他用例使用。领域层中有多个层次的用例是很正常的。例如,下例中定义的用例可以使用 FormatDateUseCase 用例,如果 UI 层的多个类都依赖于时区来在屏幕上显示正确的消息:

class GetLatestNewsWithAuthorsUseCase(
  private val newsRepository: NewsRepository,
  private val authorsRepository: AuthorsRepository,
  private val formatDateUseCase: FormatDateUseCase
) { /* ... */ }
GetLatestNewsWithAuthorsUseCase depends on repository classes from the
    data layer, but it also depends on FormatDataUseCase, another use case class
    that is also in the domain layer.
图 2. 依赖于其他用例的用例的依赖关系图示例。

在 Kotlin 中调用用例

在 Kotlin 中,您可以通过使用 operator 修饰符定义 invoke() 函数,使use case 类实例可以像函数一样被调用。请看以下示例:

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 示例。如果将来您对日期格式化的业务要求发生变化,您只需要在一个中心位置更改代码。

组合存储库

在新闻应用中,您可能有 NewsRepositoryAuthorsRepository 类,它们分别处理新闻和作者数据操作。NewsRepository 公开的 Article 类只包含作者的姓名,但您想在屏幕上显示更多关于作者的信息。作者信息可以从 AuthorsRepository 中获取。

GetLatestNewsWithAuthorsUseCase depends on two different repository
    classes from the data layer: NewsRepository and AuthorsRepository.
图 3. 从多个存储库组合数据的用例的依赖关系图。

由于逻辑涉及多个存储库并可能变得复杂,因此您创建了一个 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 layer cannot access data layer directly, it must go through the Domain layer
图 4. 显示 UI 层被拒绝访问数据层的依赖关系图。

进行此限制的一个优点是,它可以阻止您的 UI 绕过领域层逻辑,例如,如果您正在对数据层的每个访问请求执行分析日志记录。

但是,一个**潜在的显著缺点**是,它会强制您添加用例,即使它们只是对数据层的简单函数调用,这可能会增加复杂性,但收益甚微。

一个好的方法是仅在需要时添加用例。如果您发现您的 UI 层几乎完全通过用例访问数据,那么**只**通过这种方式访问数据可能是有意义的。

最终,限制数据层访问的决定取决于您的具体代码库,以及您是偏爱严格规则还是更灵活的方法。

测试

测试领域层时,适用通用测试指南。对于其他 UI 测试,开发者通常使用假存储库,在测试领域层时使用假存储库也是一个好习惯。

示例

以下 Google 示例演示了领域层的使用。请探索它们,以了解本指南的实际应用: