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