В конце декабря 2021-го Android обновил рекомендации по архитектуре мобильных приложений. Публикуем перевод гайда в пяти частях:
Доменный слой (вы находитесь здесь)
Доменный слой не является обязательным и располагается между слоями UI и данных.
![Роль доменного слоя в архитектуре приложения Роль доменного слоя в архитектуре приложения](https://habrastorage.org/getpro/habr/upload_files/cdd/553/886/cdd5538860c7a7c8b9697f0b02ba8025.png)
Доменный слой отвечает за инкапсуляцию сложной или простой бизнес-логики, которую переиспользуют несколько ViewModel. Этот слой необязателен: не всем приложениям он требуется. Его следует внедрять только при необходимости: например, при работе со сложной логикой или чтобы переиспользовать логику.
Преимущества доменного слоя:
Помогает избежать дублирования кода.
Улучшает читаемость кода в классах, которые используют классы из доменного слоя.
Облегчает тестирование приложения.
Позволяет избежать раздутых классов благодаря разделению ответственностей.
Чтобы классы оставались облегчёнными, каждый класс UseCase должен отвечать только за одну функциональность и не должен содержать изменяемых данных. С изменяемыми данными лучше работать в слоях UI или данных.
Правила именования в гайде
В этом гайде классы UseCase именуются тем действием, за которое они отвечают. Правило выглядит следующим образом:
Глагол в настоящем времени + существительное/объект (не обязательно) + UseCase
Например: FormatDateUseCase
, LogOutUserUseCase
, GetLatestNewsWithAuthorsUseCase
или MakeLoginRequestUseCase
.
Зависимости
В архитектуре классического приложения UseCase-классы размещают между моделями ViewModel из слоя UI и классами Repository из слоя данных. Следовательно, UseCase-классы обычно зависят от Repository-классов.
Вместе они обмениваются данными со слоем UI так же, как это делают классы Repository — с помощью функций callback (в Java) или корутин (в Kotlin). Подробнее об этом можно почитать в гайде «Слой данных».
Например, у вас в приложении может быть UseCase-класс, который берёт данные из новостного репозитория и репозитория авторов, а затем их объединяет:
class GetLatestNewsWithAuthorsUseCase(
private val newsRepository: NewsRepository,
private val authorsRepository: AuthorsRepository
) { /* ... / }
Так как классы UseCase содержат переиспользуемую логику, их могут использовать и другие классы UseCase. Вполне нормально, если у вас в доменном слое есть несколько уровней классов UseCase.
UseCase из следующего примера может воспользоваться классом FormatDateUseCase
, если несколько классов из слоя UI используют данные о часовом поясе для отображения верного сообщения на экране:
class GetLatestNewsWithAuthorsUseCase(
private val newsRepository: NewsRepository,
private val authorsRepository: AuthorsRepository,
private val formatDateUseCase: FormatDateUseCase
) { / ... / }
![Пример графика зависимостей для класса UseCase, который зависит от других классов UseCase Пример графика зависимостей для класса UseCase, который зависит от других классов UseCase](https://habrastorage.org/getpro/habr/upload_files/e4b/43d/99b/e4b43d99bb6d10c24d44d526e0eb042d.png)
Вызов классов UseCase в Kotlin
В Kotlin экземпляр класса UseCase можно сделать вызываемым как функцию, определив функцию 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()
с разными типами параметров. В таком случае класс UseCase из предыдущего примера можно было бы вызвать следующим образом:
class MyViewModel(formatDateUseCase: FormatDateUseCase) : ViewModel() {
init {
val today = Calendar.getInstance()
val todaysDate = formatDateUseCase(today)
/ ... /
}
}
Подробнее об операторе invoke()
можно почитать в документации по Kotlin.
Жизненный цикл
У классов UseCase нет собственных жизненных циклов: они ограничены жизненным циклом классов, которые их используют. То есть UseCase можно вызывать из классов в слое UI, из служб или даже из класса Application
. Так как классы UseCase не должны содержать изменяемые данные, следует создавать новый экземпляр UseCase класса каждый раз, когда вы передаете его в качестве зависимости.
Потоки
Классы UseCase из доменного слоя должны быть main-safe: вызывать их из главного потока должно быть безопасно. Если UseCase-классы выполняют продолжительные операции, которые блокируют поток, они должны отвечать за перенос логики в соответствующий поток. Однако сначала убедитесь, что эти операции нельзя перенести в другие слои иерархии.
Как правило, сложные вычисления выполняются в слое данных, чтобы их можно было переиспользовать или кэшировать. К примеру, требовательную к ресурсам операцию для длинного списка лучше перенести в слой данных, чем в доменный слой, если результат нужно будет кэшировать, а затем переиспользовать на нескольких экранах приложения.
В примере ниже показан класс UseCase, который работает в фоновом потоке:
class MyUseCase(
private val defaultDispatcher: CoroutineDispatcher = Dispatchers.Default
) {
suspend operator fun invoke(...) = withContext(defaultDispatcher) {
// Long-running blocking operations happen on a background thread.
}
}
Типичные для доменного слоя задачи
Простая переиспользуемая бизнес-логика
Повторяющуюся в слое UI бизнес-логику следует инкапсулировать в UseCase класс: будет легче вносить изменения везде, где она используется. Кроме того, так её можно тестировать изолированно.
Вспомним пример FormatDateUseCase
, описанный ранее. Если бизнес требования к формату даты изменятся в будущем, весь код, который потребуется изменить, будет сконцентрирован в одном месте.
Важно: в некоторых случаях логику, находящуюся в классах UseCase, можно сделать частью статических методов в классах Util. Однако такой подход использовать не рекомендуется: классы Util зачастую тяжело найти и тяжело разобраться в их функциональности. Более того, у нескольких классов UseCase может быть одна и та же функциональность (переключение потоков и обработка ошибок в базовых классах), от которой при масштабировании выиграют растущие команды.
Слияние репозиториев
В новостном приложении могут быть классы NewsRepository и AuthorsRepository, которые работают с данными о новостях и авторах соответственно. Класс Article, к которому открывает доступ NewsRepository, содержит только имя автора. Представим, что появится необходимость показать на экране больше информации об авторе. Её можно получить из AuthorsRepository.
![Схема зависимостей класса UseCase, который объединяет данные из нескольких классов Repository Схема зависимостей класса UseCase, который объединяет данные из нескольких классов Repository](https://habrastorage.org/getpro/habr/upload_files/921/8ed/292/9218ed292499209812c9ab7e12cf88a2.png)
Так как требуемая логика задействует несколько классов Repository и может стать довольно сложной, нужно создать класс 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
}
}
Логика задействует все элементы списка новостей. Несмотря на то, что слой данных является main-safe, нужно позаботиться, чтобы этот процесс не блокировал главный поток, ведь мы не знаем, сколько элементов ему придётся обработать. Поэтому класс UseCase переносит работу в фоновый поток при помощи диспатчера по умолчанию.
Важно: Room library позволяет запрашивать, какие взаимосвязи существуют между различными сущностями в базе данных. Если база данных — source of truth, можно создать запрос, который всю эту работу выполнит за вас. В таком случае лучше создать Repository класс (например, NewsWithAuthorsRepository) вместо класса UseCase.
Переиспользование
Помимо слоя UI, доменный слой могут переиспользовать другие классы: например, классы Service или класс Application. Кроме того, если другие платформы, такие как TV или Wear используют общую кодовую базу с мобильным приложением, их слой UI также может переиспользовать классы UseCase и получить все преимущества доменного слоя.
Тестирование
Общие рекомендации по тестированию применимы к тестированию доменного слоя. В случае с UI-тестами разработчики, как правило, используют фиктивные классы Repository. Их же рекомендуется использовать и для тестирования доменного слоя.
Читайте далее
jesaiah4
Не очень понял где вы храните модели которые шарятся между UI REQ , И QUEUE system ?
Или вы имеете три одинаковых копии модели ?