Корни проблемы
Слова 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/
Добавить комментарий