Описываем UseCase’ы правильно

от автора

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

Слова 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).

Таким образом у нас появляется единственная абстракция описания единицы логики — юзкейс. Она всегда прячет за собой принятие решений и работу с данными, тем самым облегчается работа с кодом.

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

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

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

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


ссылка на оригинал статьи https://habr.com/ru/articles/857698/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *