Слой домена

Уровень домена — это дополнительный уровень, который находится между уровнем пользовательского интерфейса и уровнем данных.

Если он включен, дополнительный уровень домена обеспечивает зависимости от уровня пользовательского интерфейса и зависит от уровня данных.
Рисунок 1. Роль доменного уровня в архитектуре приложения.

Уровень предметной области отвечает за инкапсуляцию сложной бизнес-логики или простой бизнес-логики, которая повторно используется несколькими моделями представления. Этот уровень является необязательным, поскольку не все приложения будут соответствовать этим требованиям. Вы должны использовать его только тогда, когда это необходимо, например, чтобы справиться со сложностью или обеспечить возможность повторного использования.

Уровень домена предоставляет следующие преимущества:

  • Это позволяет избежать дублирования кода.
  • Это улучшает читаемость классов, использующих классы уровня предметной области.
  • Это улучшает тестируемость приложения.
  • Он позволяет избежать больших классов, позволяя разделить обязанности.

Чтобы эти классы были простыми и облегченными, каждый вариант использования должен отвечать только за одну функциональность, и они не должны содержать изменяемые данные. Вместо этого вам следует обрабатывать изменяемые данные в пользовательском интерфейсе или на уровнях данных.

Соглашения об именах в этом руководстве

В этом руководстве варианты использования названы в честь одного действия, за которое они отвечают. Соглашение заключается в следующем:

глагол в настоящем времени + существительное/что (необязательно) + UseCase .

Например: FormatDateUseCase , LogOutUserUseCase , GetLatestNewsWithAuthorsUseCase или MakeLoginRequestUseCase .

Зависимости

В типичной архитектуре приложения классы вариантов использования располагаются между моделями представления на уровне пользовательского интерфейса и репозиториями на уровне данных. Это означает, что классы вариантов использования обычно зависят от классов репозитория и взаимодействуют с уровнем пользовательского интерфейса так же, как репозитории, — используя либо обратные вызовы (для 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
) { /* ... */ }
GetLatestNewsWithAuthorsUseCase зависит от классов репозитория на уровне данных, но он также зависит от FormatDataUseCase, другого класса варианта использования, который также находится на уровне домена.
Рисунок 2. Пример графика зависимостей для варианта использования, который зависит от других вариантов использования.

Варианты использования вызовов в Котлине

В Kotlin вы можете использовать экземпляры класса Case, вызываемые как функции, определив функцию invoke() с модификатором operator . См. следующий пример:

class FormatDateUseCase(userRepository: UserRepository) {

    private val formatter = SimpleDateFormat(
        userRepository.getPreferredDateFormat(),
        userRepository.getPreferredLocale()
    )

    operator fun invoke(date: Date): String {
        return formatter.format(date)
    }
}

В этом примере метод invoke() в FormatDateUseCase позволяет вызывать экземпляры класса, как если бы они были функциями. Метод 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 , которые обрабатывают операции с новостями и данными автора соответственно. Класс Article , предоставляемый NewsRepository , содержит только имя автора, но вы хотите отобразить на экране дополнительную информацию об авторе. Информацию об авторе можно получить из AuthorsRepository .

GetLatestNewsWithAuthorsUseCase зависит от двух разных классов репозитория уровня данных: NewsRepository и 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 ; поэтому, даже несмотря на то, что уровень данных безопасен для основного потока, эта работа не должна блокировать основной поток, поскольку вы не знаете, сколько элементов он обработает. Вот почему вариант использования перемещает работу в фоновый поток с использованием диспетчера по умолчанию.

Другие потребители

Помимо уровня пользовательского интерфейса, уровень домена может повторно использоваться другими классами, такими как службы и класс Application . Более того, если другие платформы, такие как TV или Wear, используют кодовую базу мобильного приложения, их уровень пользовательского интерфейса также может повторно использовать варианты использования, чтобы получить все вышеупомянутые преимущества уровня предметной области.

Ограничение доступа к уровню данных

Еще одно соображение при реализации уровня домена: следует ли вам по-прежнему разрешать прямой доступ к уровню данных из уровня пользовательского интерфейса или принудительно все через уровень домена.

Уровень пользовательского интерфейса не может получить прямой доступ к уровню данных, он должен пройти через уровень домена.
Рис. 4. График зависимостей, показывающий, что уровню пользовательского интерфейса отказано в доступе к уровню данных.

Преимущество создания этого ограничения заключается в том, что оно не позволяет вашему пользовательскому интерфейсу обходить логику уровня домена, например, если вы выполняете ведение журнала аналитики для каждого запроса доступа к уровню данных.

Однако потенциально существенным недостатком является то, что вам придется добавлять варианты использования, даже если это всего лишь простые вызовы функций на уровне данных, что может усложнить работу без небольшой пользы.

Хороший подход — добавлять варианты использования только при необходимости. Если вы обнаружите, что ваш уровень пользовательского интерфейса обращается к данным почти исключительно через сценарии использования, возможно, имеет смысл обращаться к данным только таким способом.

В конечном счете, решение об ограничении доступа к уровню данных зависит от вашей индивидуальной кодовой базы и от того, предпочитаете ли вы строгие правила или более гибкий подход.

Тестирование

Общие рекомендации по тестированию применяются при тестировании уровня предметной области. Для других тестов пользовательского интерфейса разработчики обычно используют поддельные репозитории, и хорошей практикой является использование поддельных репозиториев и при тестировании уровня домена.

Образцы

Следующие примеры Google демонстрируют использование уровня домена. Изучите их, чтобы увидеть это руководство на практике:

{% дословно %} {% дословно %} {% дословно %} {% дословно %}