Введение

Если вы уже определенное время занимаетесь разработкой Android, вы, вероятно, слышали о UseCases. Их часто представляют как Святой Грааль Clean architecture. UseCases призваны отделить бизнес-логику от Presentation и Data слоев, сделав ваш код более модульным, переиспользуемым и тестируемым. Но вот в чем загвоздка: UseCases не всегда являются серебряной пулей. На самом деле, слепое их применение может привести к раздутому коду и ненужной сложности, чего как раз и пытается избежать Clean Architecture. В этой статье мы развенчаем миф о UseCases и обсудим, когда они необходимы, а когда - просто пустая трата времени. Если вы разработчик Android и задаетесь вопросом, приносите ли вы больше вреда, чем пользы, следуя этому шаблону, эта статья для вас.

Спорная правда о UseCases

Теоретически UseCases имеют смысл в Clean Architecture: они инкапсулируют бизнес-логику и гарантируют, что слои вашего приложения остаются разъединенными. Однако реальность в повседневной разработке приложений гораздо более тонкая.

Слишком много разработчиков относятся к UseCases как к требованию, что приводит к избыточному и ненужному коду. Результат? Вместо того чтобы сделать свои приложения более удобными для поддержки и масштабируемыми, они создают раздутые кодовые базы, заполненные слоями абстракции, которые не служат реальной цели.

Когда следует избегать UseCases: будь проще

Давайте начнем с того, когда следует избегать UseCases. Самая большая ошибка, которую допускают многие разработчики, - это использование UseCases для каждого небольшого действия в приложении, даже если не задействована значительная бизнес-логика.

Пример: Получение данных из Repository

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

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

class MyViewModel(private val repository: TaskRepository) : ViewModel() {
    val tasks = liveData {
        emit(repository.getTasks())
    }
}

Почему здесь избегают UseCase? Потому что он не добавляет ценности. Логика уже проста, и введение UseCase только раздует код без какой-либо очевидной выгоды. Clean Architecture выступает за простоту и ясность, а здесь UseCase сделает обратное.

Когда необходим UseCase: обработка сложной бизнес-логики

Теперь давайте поговорим о том, когда UseCase действительно необходим. UseCases стоит использовать, когда вам нужно инкапсулировать сложную бизнес-логику, которую нужно отделить от Presentation и Data слоев.

Пример: бронирование рейса в приложении для путешествий

Рассмотрим сценарий, в котором пользователь бронирует рейс в туристическом приложении. Этот процесс включает несколько шагов:

Проверка введенных пользователем данных (даты, пункты назначения).

  • Проверка доступности рейса

  • Бронирование рейса

  • Обработка платежа

  • Отправка подтверждения бронирования

Здесь вы имеете дело с несколькими UseCases между различными службами - доступность рейса, бронирование и оплата - каждое из которых может выдать ошибку.

UseCase в этом сценарии имеет смысл. Инкапсулируя процесс бронирования в UseCase, вы можете:

  • Обеспечить соблюдение всех бизнес-правил

  • Повторно использовать логику в разных частях приложения (например, бронирование через разные UI)

  • Упростить тестирование логики бронирования независимо от UI и уровней данных

class BookFlightUseCase(
    private val flightRepository: FlightRepository,
    private val paymentProcessor: PaymentProcessor
) {
    suspend operator fun invoke(flightId: String, userDetails: User, paymentInfo: PaymentInfo): BookingResult {
        if (!validateInput(userDetails)) throw InvalidInputException()
        val availability = flightRepository.checkAvailability(flightId)
        if (!availability) throw FlightNotAvailableException()

        val reservation = flightRepository.reserveFlight(flightId, userDetails)
        val paymentResult = paymentProcessor.processPayment(paymentInfo)

        return BookingResult.Success(reservation, paymentResult)
    }
}

В этом случае UseCase улучшает организацию кода, тестируемость и повторное использование. Без UseCase эта логика, скорее всего, окажется в ViewModel или Activity, что усложнит ее поддержку, тестирование и повторное использование.

Настоящее предназначение UseCases: избегание раздувания, а не его создание

Цель Clean Architecture - не создавать больше слоев абстракции ради самого процесса, а поддерживать вашу кодовую базу чистой, организованной и простой в обслуживании. Введение UseCases там, где это не нужно, нарушает этот принцип.

Вот где ошибаются многие разработчики: они следуют шаблонам, таким как UseCases, потому что им говорят, что это часть Clean Architecture, не понимая полностью, зачем они это используют. В результате их код становится загроможденным UseCases, которые не служат никакой реальной цели, превращая Clean Architecture в то, чего она должна предотвращать — раздутый и сложный в обслуживании код.

Вывод: UseCases — это инструменты, а не правила

Главный вывод: UseCases — это инструменты, а не правила. То, что Clean Architecture предполагает их, не означает, что их следует применять везде. Чрезмерное использование UseCases, особенно в ситуациях без сложной бизнес-логики, приводит к ненужной абстракции и раздутому коду.

Принимая решение о том, внедрять ли UseCases, спросите себя:

  • Есть ли бизнес-логика, которую нужно отделить от уровня представления?

  • Будет ли эта логика повторно использоваться в другом месте приложения?

  • Нужно ли проводить независимое тестирование этой логики?

Если ответ на эти вопросы — нет, пропустите UseCases и сделайте свой код простым. Если ответ — да, то UseCases поможет вам получить более чистую и более поддерживаемую кодовую базу.

Используйте UseCases с умом, и вы будете на правильном пути к написанию чистого и эффективного кода.

Комментарии (12)


  1. KarmanovichDev
    24.09.2024 08:55

    Спасибо за статью!

    Полностью согласен! Если UseCase является прокси объектом к другому механизму(репо например) или содержит минимальное кол-ло логики, то выделение UseCase приводит лишь к дополнительным тестам, а учитывая, что репо это внепроцессная зависимость, то по-хорошему и к дополнительным тестовым дублёрам(моки, фейки и т. п.).


  1. MagDen
    24.09.2024 08:55

    Не люблю юзкейсы - конструктор класса возвращает совсем иной объект. Глаза режет)

    А добавьте туда высокие требования к ясности названий как классов, так и получаемых переменных. Тут и без юзкейсов зачастую беда с неймингами. А тут смотришь на их применение в коде и в голове крутится - "Что ты такое?"


    1. kavaynya
      24.09.2024 08:55
      +2

      Что-то вы путаете. Как ни крути, но конструктор класс по прежнему возвращает instance этого класса. А вот уже сам instance можно вызвать после этого с другими параметрами.


      1. MagDen
        24.09.2024 08:55

        Смотри, попутал на ночь глядя)

        С конструктором все ок, но все равно выглядит странно. Не могу привыкнуть к ним. К тому же, те примеры, что я видел на практике были не совсем удачные, в отличии от того что тут приведен


        1. kavaynya
          24.09.2024 08:55

          Согласен, что выглядит странно, особенно если так написать )):
          SomeUseCase(someRepository)(someId)

          Но можно же и явно вызвать метод invoke

          Боюсь с этим мало что можно сделать, современные тенденции, они такие )


          1. clint_eastwood Автор
            24.09.2024 08:55

            в контексте юзкейсов думаю это оправданно. такой подход подталкивает пооектировать их только с одним публичным методом. и странно конда имя useCase совпадает именем метода.

            плюс в реальном мире врят ли увидишь приведенную запись. так как скорее всего будет какойто di


            1. kavaynya
              24.09.2024 08:55

              странно конда имя useCase совпадает именем метода.

              Что имеется в виду? Не понятно...


              1. Donnie_D
                24.09.2024 08:55

                Что bookFlightUseCase.bookFlight(...) выглядел бы не очень.


                1. clint_eastwood Автор
                  24.09.2024 08:55

                  именно это и имелось ввиду


                1. kavaynya
                  24.09.2024 08:55

                  А чем метод invoke не нравится? Он же и так есть по умолчанию, как оператор, но вызывать явно можно.


          1. Gizcer
            24.09.2024 08:55

            Как сложно =) На самом деле если вы используете иньекцию зависимостей, то в вашу VM придёт переменная someUseCase: SomeUseCase. А потом вы ее вызовите как будто бы обычный метод someUseCase(someID). И выглядит совсем не страшно) Ну и invoke если уж совсем явно хотим.


  1. alyxe
    24.09.2024 08:55
    +1

    Использование use case везде — это вынужденная мера для сохранения единого стиля, консистентности и архитектуры. Если никто не запретит использовать repository напрямую, то на одном экране будут использованы use case, на другом repository во view model. А на третьем, однажды, вообще решат, что view model — ни к чему, можно запросы дергать из UI кода. Архитектура — есть архитектура. Рассуждения о том, что где-то лишнее приводит лишь к халиварам.

    Если вопрос в том, что для простого приложения, по типу todo list, не нужно использовать use case, то соглашусь. MVP Todo list не предполагает логики кроме получения списка и добавления новой записи. Но позже, вероятно, может потребоваться расширение. В таком случае, для выстраивания архитектуры может быть необходимость какой-то код перенести в use case. А для того, чтобы код был идентичен, придется везде мигрировать на use case.