
Чистая архитектура чрезвычайно полезна, особенно в больших проектах. Однако если применять ее неправильно, то пользы не будет.
Поэтому рассмотрим лучшие практики и красные флаги каждого слоя. При этом постараемся понять преимущества и недостатки каждого подхода и выявить красные флаги, которые настораживают, но все равно игнорируются.
Что входит в зону ответственности UseCase?
UseCase (сценарий/вариант использования, юзкейс) отвечает за инкапсуляцию бизнес-логики для одной многократно используемой задачи, которую должна выполнять система.
Разобьем это определение на части и рассмотрим детально каждую из них.
1. Бизнес-логика фокусируется на том, что нужно достичь команде разработчиков, когда они описывают задачу. Показателем того, что юзкейс следует бизнес-логике, будет возможность использовать его на другой платформе, например iOS.
Допустим, пользователю надо совершить платеж. Для выполнения этой задачи необходимо следовать такой бизнес-логике:
- начать транзакцию;
- отправить платеж;
- завершить транзакцию;
- при необходимости обработать все ошибки.
class SendPayment(private val repo: PaymentRepo) {
suspend operator fun invoke(
amount: Double,
checkId: String,
): Boolean {
val transactionId = repo.startTransaction(params.checkId)
repo.sendPayment(
amount = params.amount,
checkId = params.checkId,
transactionId = transactionId
)
return repo.finalizeTransaction(transactionId)
}
}
2. В юзкейсе должна заключаться только одна задача — в основном одна публичная функция.
Почему? Юзкейс, который отвечает только за одну задачу, будет многоразовым, тестируемым и заставит разработчика выбрать конкретное, а не обобщенное имя.
Юзкейсы, которые не фокусируются на одной задаче, обычно оказываются оберткой репозитория. Кроме того, их трудно поместить в определенный пакет; более того, разработчику приходится читать все юзкейсы, чтобы иметь представление обо всех решаемых задачах.
// Не используйте обобщенные юзкейсы для разных задач.
// Функциональность трудно обнаружить, если разработчик
// не читал юзкейс, что очень сложно в условиях большой кодовой базы.
class GalleryUseCase @Inject constructor(
/*...*/
) {
fun saveImage(file: File)
fun downloadFileWithSave(/*...*/)
fun downloadImage(/*...*/): Image
fun getChatImageUrl(messageID: String)
}
// У каждого юзкейса должна быть только одна ответственность
class SaveImageUseCase @Inject constructor(
/*...*/
) {
operator fun invoke(file: File): Single<Boolean>
// Перегрузка подходит для тех же зон ответственности юзкейсов,
// но с другим набором параметров.
operator fun invoke(path: String): Single<Boolean>
}
class GetChatImageUrlByMessageIdUseCase() {
operator fun invoke(messageID: String): Url {...}
}
Примечание: перегрузка — вещь условная, это нужно будет обсудить с командой.
Другим решением является создание юзкейса для каждого параметра, например GetUser, GetUserByUserId, GetUserByUsername и т. д.
Но думаю, что перегрузка имеет больше смысла, потому что очень сложно давать имена при наличии 4-5 параметров.
Именование
Именование класса UseCase простое: глагол в настоящем времени + существительное/что (опционально) + UseCase.
Примеры: FormatDateUseCase, GetChatUserProfileUseCase, RemoveDetektRulesUseCase и т. д.
Имена функций могут быть как с использованием оператора invoke, так и обычными именами функций.
class SendPaymentUseCase(private val repo: PaymentRepo) {
// использование функции оператора
suspend operator fun invoke(): Boolean {}
// обычные имена
suspend fun send(): Boolean {}
}
// --------------Использование--------------------
class HomeViewModel(): ... {
fun startPayment(...) {
sendPaymentUseCase() // использование invoke
sendPaymentUseCase.send() использование обычных функций
}
}
На мой взгляд, применение оператора invoke лучше, поскольку:
- заставляет разработчика выбрать имя для конкретного юзкейса;
- облегчает выбор дополнительного имени для функции;
- обеспечивает простоту использования;
- позволяет легко добавлять перегрузки для одной и той же ответственности, тогда как добавление разных функций с разными зонами ответственности выглядит странно, на что обычно указывают во время обзоров кода.
Безопасность потоков
Юзкейсы должны быть безопасными для основного потока, то есть любая тяжелая операция обрабатывается в отдельном потоке, а юзкейс вызывается из основного потока.
// Добавление больших списков и операций сортировки может утяжелить работу,
// поэтому должно выполняться на другом потоке.
class AUseCase @Inject constructor() {
suspend operator fun invoke(): List<String> {
val list = mutableListOf<String>()
repeat(1000) {
list.add("Something $it")
}
return list.sorted()
}
}
// Делайте так:
class AUseCase @Inject constructor(
// или диспетчер по умолчанию
@IoDispatcher private val dispatcher: CoroutineDispatcher,
) {
suspend operator fun invoke(): List<String> = withContext(dispatcher) {
val list = mutableListOf<String>()
repeat(1000) {
list.add("Something $it")
}
list.sorted()
}
}
// Не переключайте контекст, когда вы не выполняете тяжелую операцию или просто вызываете репозиторий,
// поскольку функции репозитория должны быть безопасными для основного потока.
class AUseCase @Inject constructor(
private val repository: ChannelsRepository,
// или диспетчер по умолчанию
@IoDispatcher private val dispatcher: CoroutineDispatcher,
) {
suspend operator fun invoke(): List<String> = withContext(dispatcher) {
repository.getSomething()
}
}
Красные флаги при использовании юзкейсов
1. Наличие любого класса, не относящегося к домену, в качестве входного, выходного или в теле юзкейса, например ui-модели, data-модели, связанные с Android импорты или мапперы ui-домена/data-домена.
Почему? Это нарушает ответственность юзкейса и делает его менее пригодным для повторного использования.
Пример 1. Сопоставление данных и домена — это ответственность репозитория, а сопоставление ui-домена — это ответственность ViewModel/презентатора.
Пример 2. Возвращение ui-модели, специфичной для экрана, сделает юзкейс пригодным только для этого экрана.
Пример 3. Принятие решения о фактическом сообщении об ошибке в юзкейсе. Это ответственность представления. Юзкейс должен возвращать только ошибку, а не отображаемое сообщение.
// Не используйте классы/импорты, имеющие отношение к Android
class AddToContactsUseCase @Inject constructor(
@ApplicationContext private val context: Context,
) {
operator fun invoke(
name: String?,
phoneNumber: String?,
) {
context.addToContacts(
name = name,
phoneNumber = phoneNumber,
)
}
2. Наличие более одной публичной функции, если это не перегрузка для одной и той же ответственности.
Почему? Это затрудняет обнаружение юзкейса, заставляет разработчика читать множество юзкейсов в поисках необходимого.
3. Наличие не общих бизнес-правил, определенных в юзкейсе, обычно означающее определенную логику для экрана.
Почему? Это делает юзкейс непригодным для повторного использования для любого другого экрана/юзкейса.
4. Содержание в юзкейсе изменяемых данных. Чтобы избежать этого, надо обрабатывать изменяемые данные в слоях пользовательского интерфейса или слоях данных. Наличие изменяемых данных в юзкейсе может привести к связанному поведению, особенно при использовании более чем на одном экране.
// Не делайте так:
class PerformeSomethingUseCase @Inject constructor() {
val list = mutableListOf<String>()
suspend operator fun invoke(): List<String> {
repeat(1000) {
list.add("Something $it")
}
return list.sorted()
}
}
5. Обобщенное наименование юзкейса, например LivestreamUseCase, UserUseCase, GalleryUseCase и т. д.
Почему? Это нарушает принцип единственной ответственности и заставляет разработчика читать множество юзкейсов, чтобы найти необходимый.
Общие вопросы
1) Следует ли использовать абстракцию в юзкейсах?
Во многих статьях и руководствах приводятся примеры реализации юзкейсов с абстракцией. Вот один из таких примеров:
interface GetSomethingUseCase {
suspend operator fun invoke(): List<String>
}
class GetSomethingUseCaseImpl(
private val repository: ChannelsRepository,
) : GetSomethingUseCase {
override suspend operator fun invoke(): List<String> = repository.getSomething()
}
// Затем привяжите эту реализацию к интерфейсу с помощью инъекции зависимостей
Абстракция дает различные преимущества, например возможность предоставить несколько реализаций и включить возможность мокинга для модульного тестирования.
Однако юзкейсы, как правило, имеют единственную реализацию и не требуют мокинга, поскольку они просто диктуют доменные правила. При тестировании ViewModels предпочтительнее тестировать реальную логику приложения, а не имитировать ее.
На мой взгляд, следует избегать абстракций при использовании юзкейсов, если только не требуется несколько реализаций.
2) Что делать с бесполезными юзкейсами?
Иногда в итоге получается множество юзкейсов, которые только оборачивают функцию репозитория.
class GetSomethingUseCase @Inject constructor(
private val repository: ChannelsRepository,
) {
suspend operator fun invoke(): List<String> = repository.getSomething()
}
Возникает “правомерный” вопрос: не лучше ли вместо этого использовать функцию репозитория в ViewModel?
Что ж, Google согласен с вами в решении этого вопроса. Вот плюсы (+) и минусы (-) постоянного доступа к слою данных с помощью юзкейсов.
- Многие юзкейсы добавляют сложности без особой выгоды (жирный -).
- Постоянное использование юзкейсов защитит код от будущих изменений (+). Например, если отправка платежа потребует еще одного шага, придется создать новый юзкейс, а затем рефакторить каждую ViewModel, которая использовала эту функцию в репозитории, чтобы использовать вместо нее юзкейс.
- Постоянное использование юзкейсов будет действовать как документация и поможет лучше понять, какие задачи выполняет проект (+). Например, новый разработчик может понять, чем занимается проект, просто прочитав названия юзкейсов.
- Постоянное использование юзкейсов поможет обеспечить последовательность действий и снизить усталость от принятия решений (+).
Решение зависит от вас и вашей команды и может варьироваться в зависимости от размера проекта.
3) Допустимы ли юзкейсы в другом юзкейсе?
Это не только допустимо, но и рекомендуется. Разбиение задач на более мелкие и управляемые юзкейсы повышает модульность и возможность повторного использования.
Читайте также:
- Освоение функциональных возможностей Kotlin
- Создание собственной версии UseCase в 2023 году: гибкий и функциональный подход
- 10 советов по созданию чистого кода для мобильной разработки на Kotlin в 2024 году
Читайте нас в Telegram, VK и Дзен
Перевод статьи ilyas ipek: UseCase Red Flags and Best Practices in Clean Architecture





