Доменный слой – это необязательный слой, который находится между слоем пользовательского интерфейса и слоем данных.
Уровень домена отвечает за инкапсуляцию сложной бизнес-логики или простой бизнес-логики, которая повторно используется несколькими моделями представления. Этот уровень необязателен, поскольку не у всех приложений будут такие требования. Используйте его только при необходимости, например, чтобы справиться со сложностью или повысить возможность повторного использования.
Уровень домена дает следующие преимущества:
- Это позволяет избежать дублирования кода.
- Это улучшает читаемость в классах, использующих классы уровня домена.
- Это упрощает тестирование приложения.
- Это позволяет избежать больших классов, разделяя обязанности.
Чтобы эти классы были простыми и легкими, каждый вариант использования должен отвечать только за одну функциональность и не должен содержать изменяемые данные. Вместо этого вы должны обрабатывать изменяемые данные в пользовательском интерфейсе или слоях данных.
Правила именования в этом руководстве
В этом руководстве варианты использования названы в честь единственного действия, за которое они отвечают. Ниже приведены примеры.
глагол в настоящем времени + существительное/что (необязательно) + UseCase.
Примеры: FormatDateUseCase, LogOutUserUseCase, GetLatestNewsWithAuthorsUseCase, MakeLoginRequestUseCase.
Связанные запросы
В типичной архитектуре приложения классы вариантов использования вписываются между ViewModels из слоя пользовательского интерфейса и репозиториями из слоя данных. Это означает, что классы вариантов использования обычно зависят от классов репозиториев и взаимодействуют со слоем пользовательского интерфейса так же, как и репозитории – с помощью обратных вызовов (для 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 вы можете сделать экземпляры классов вариантов использования вызываемыми как функции, определив функцию 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, чтобы абстрагировать логику от 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, используют общую базу кода с мобильным приложением, их уровень пользовательского интерфейса также может повторно использовать варианты использования, чтобы получить все вышеупомянутые преимущества уровня домена.
Ограничение доступа к слою данных
Ещё один вопрос, который следует учитывать при реализации доменного слоя, заключается в том, следует ли по-прежнему разрешать прямой доступ к слою данных из слоя пользовательского интерфейса или принудительно пропускать всё через доменный слой.
Преимущество такого ограничения заключается в том, что оно не позволяет пользовательскому интерфейсу обходить логику уровня домена, например, если вы выполняете ведение журнала аналитики для каждого запроса доступа к слою данных.
Однако существенный недостаток заключается в том, что вам приходится добавлять варианты использования, даже если это простые вызовы функций к уровню данных, что может усложнить работу без особой пользы.
Рекомендуем добавлять варианты использования только по мере необходимости. Если вы обнаружите, что ваш слой пользовательского интерфейса обращается к данным почти исключительно через варианты использования, возможно, имеет смысл обращаться к данным только таким образом.
В конечном счете решение ограничить доступ к слою данных зависит от вашей базы кода и от того, предпочитаете ли вы строгие правила или более гибкий подход.
Тестирование
При тестировании уровня домена применяются общие рекомендации по тестированию. Для других тестов пользовательского интерфейса разработчики обычно используют поддельные репозитории, и это хорошая практика – использовать поддельные репозитории и при тестировании уровня домена.
Образцы
В примерах ниже показано, как использовать доменный уровень. Ознакомьтесь с ними, чтобы увидеть, как эти рекомендации применяются на практике:
Рекомендуем
- Примечание. Текст ссылки показывается, когда JavaScript отключен.
- Слой данных
- Создание состояния в интерфейсе