Корни проблемы

Слова usecase и interactor попали в обиход Android-разработчиков из книги про "чистую" архитектуру. Книгу эту почти никто не читал внимательно, плюс изложенные там свойства "чистой" архитектуры сформулированы неточно (многие до сих пор уверены, что "чистая" архитектура — это про то, как на слои абстракций логику делить). Чтобы в них разобраться, нужно прочитать еще пару книг из 90-х, мало кто этим занимается.
Из-за этого я часто вижу в проектах, как разработчики пытаются самостоятельно осмыслить предложенные дядюшкой Бобом правила написания кода и сделать из принципов SOLID конфетку. Этот процесс натыкается на неопытность разработчиков и непонимание того, что такое архитектура в принципе. В итоге код со временем становится менее расширяемым и более связанным.
Призываю читателя не использовать эти абстракции в принципе. Всегда на собеседованиях спрашиваю, есть ли в проекте юзкейсы. Если есть, это в 99% значит, что проект будет комком грязи. НО — если уж эти абстракции везде используются и про них спрашивают на собеседованиях, я хочу предложить своё видение того, как можно их использовать.


Что такое архитектура

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

Часто совершаемые ошибки

Юзкейсы обычно описывают какую-то мелкую операцию. Под интеракторами подразумевают класс, который "пользуется" несколькими юзкейсами. Этот подход может привести только к фиаско.
Его проблема в том, что он требует огромной дисциплинированности от разработчиков, т.к., чаще всего никто не смотрит на то, написан ли уже юзкейс с нужной тебе логикой и разработчик просто создает свой. Плюс, при таком подходе ничто не мешает добавить в качестве зависимости репозиторий и положить его рядом с интеракторами/юзкейсами. Ничто не мешает какие-то юзкейсы скрыть в интеракторе, а какой-то запровайдить рядом с интерактором.
Таким образом логика реализации бизнес-требований не переиспользуется, а размазывается по приложению. В случае возникновения бага найти место в коде, где возникла проблема, становится очень проблематично и изменение в дата слое может неявно затронуть несколько классов (скептик может возразить "Да ладно, так никто не пишет, нужно совсем тупыми быть"; так вот, уважаемый скептик, ТЫ НЕ ПОВЕРИШЬ, сколько раз я видел подобное даже в командах, состоящих из очень талантливых разработчиков).
Предлагаю от такого кода защититься архитектурным подходом и пересмотром определений.

Как описывать юзкейсы

Юзкейс должен описываться как абстрактный класс, в который провайдятся интерфейсы зависимостей. Таким образом, мы реализуем принцип инверсии зависимостей ("модули зависят от абстракций"), и получаем класс, в котором мы можем заменить логику просто заменив имплементацию зависимости (привет, паттерн Стратегия!).
У юзкейса может быть только один публичный метод — operator fun invoke. Таким образом форсится, что за юзкейсом скрывается единственная операция.
Интерактор в таком случае становится конкретной имплементацией абстрактного юзкейса. Это очень важно — интерактор, это не какой-то класс-контроллер, который прячет за собой юзкейсы, а именно имплементация юзкейса. Интерактор, который прячет за собой юзкейсы — абсолютно ненужная абстракция, которая только мешает разбираться в коде и этот код поддерживать.

Talks is cheap, show me the code (© один финский нацист)

Допустим, есть бизнес-требование запрашивать ленту новостей для авторизованных и неавторизованных пользователей с разных API-endpoint'ов.

Пример того, как это описать:

abstract class GetFeedUseCase constructor(
    private val feedRepository: FeedRepository, // интерфейс
    private val mapper: FeedMapper, // тоже интерфейс
) {
    operator fun invoke(): FeedData {
        return feedRepository.fetchFeed()
            .run(mapper::mapFeed)
    }
}

// юзкейс запроса данных для авторизованного пользователя
class AuthUserGetFeedInteractor constructor(
    @AuthRepo feedRepository: FeedRepository, // здесь в имплементации используем бек с авторизационными хедерами
    mapper: FeedMapper,
) : GetFeedUseCase(feedRepository, mapper)

// юзкейс запроса данных для неавторизованного пользователя
class NonAuthUserGetFeedInteractor constructor(
    @NonAuthRepo feedRepository: FeedRepository, // здесь в имплементации дергаем бек без авторизационных хедеров
    mapper: FeedMapper,
) : GetFeedUseCase(feedRepository, mapper)

В данном случае логику запроса данных для авторизованного и для неавторизованного пользователей можно описать, просто запровайдив соответствующие имплементации репозитория и маппера (например, на уровне DI).
В GetFeedUseCase можно добавить логику с кэшированием, отправкой аналитики, да всё что угодно — логика для обоих типов пользователя (описанная в абстрактном классе) будет работать одинаково для всех кейсов (pun intended).
Таким образом у нас появляется единственная абстракция описания единицы логики — юзкейс. Она всегда прячет за собой принятие решений и работу с данными, тем самым облегчается работа с кодом.
Написание тестов на подобным образом описанную логику становится делом тривиальным. Плюс, в тестах можно использовать стабы вместо моков, что всегда плюс.

Как сделать так, чтобы подобные юзкейсы стали "кирпичиками" логики и организовать взаимодействие с родительскими слоями останется упражнением для читателя (всё зависит от принятой на проекте архитектуры и используемых библиотек для описания асинхронных операций, это выходит за рамки статьи).

Спасибо за время, потраченное на прочтение! Открыт для комментариев.

Всем желаю удовольствия от кодинга.

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