Вы за это заплатите! Цена Чистой Архитектуры. Часть 2

deb6c6ff126820ae6585e7c71d253e8f.png

В прошлой части мы немного коснулись теории и остановились на определении лишних компонентов. В этой части мы продолжим определять лишнее, коснёмся вопросов экстремальной экономии, а также закрепим всё выводами.

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

Итак, продолжим.

На чем мы остановились?

Мы начали с более дорогой структуры:

a4207dc0ff2a93bf6d7ccab16ab069b1.png

И смогли убрать небольшое количество компонентов.

a1c6e461b833b3c5a8df8ef14f58f155.png

На мой взгляд это уже существенный прорыв в пользу масштабируемости проекта. Но стоило ли писать статью ради такой мелочи? Продолжим изыскания способов сделать структуру дешевле. 

За что мы платим?

Разрывы связей

Я когда-нибудь рассказывал про лишние интерфейсы? А что, если лишней является связь с ним?

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

Структура, которую мы будем использовать для следующего примера — это API-Impl. В этой структуре Api-модуль — интерфейс фичи, в котором предоставляется доступ к компонентам для переиспользования. За реализацию фичи и всех её компонентов отвечает Impl-модуль.

Вот варианты предоставления доступа к UseCase, к Repository и к обоим компонентам сразу:

f442abbe88406d202434df917845290b.png

Если структура feature-shared резала общую часть вертикально, то API-Impl нарезает её горизонтально (относительно текущей формы представления схем).

Видно, что для данной структуры снова появляются интерфейсы в Repository, а также дополнительно появляются интерфейсы в UseCase в зависимости от общей части.

Давайте разберёмся, зачем они нужны.

Может показаться, что API-модуль с интерфейсами используется для инверсии зависимостей. Действительно, в редких случаях такая структура позволяет решать архитектурные задачи при помощи инверсии зависимостей, но в рамках взаимодействия разных feature-модулей. В любом случае, считать это самоцелью данной структуры — абсолютно неверно.

Особенность и сильная сторона структуры API-Impl заключается в условии, что Impl-модули не должны зависеть от других Impl-модулей. Это условие помогает в обеспечении принципа ацикличности зависимостей (Acyclic Dependencies Principle — ADP). А также мы избавляемся от проблемы структуры feature-shared, когда изменения в реализации shared-модуля приводили к необходимости пересобирать все зависимые части.

Интерфейсы в Repository и UseCase отсутствуют в тех ситуациях, когда нет необходимости делать их логику общедоступной.

Но это всё предисловие, а разговор должен был идти про какие-то связи.

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

10b8b21eef085c79f0e04f115b0218a3.png

Количество связей и компонентов не меняется, но при этом анализировать код в контексте конкретной фичи становится проще. 

Но также я понимаю, что появление реализации в компонентах может привести к тому, что разработчики будут перепроверять, а не появились ли зависимости Impl-модуля от Impl-модуля, что может нивелировать плюсы такого подхода. Эту проблему можно было бы решить организационно, например, через названия, но пока оставляю это до того будущего, в котором других проблем не осталось.

Опциональные компоненты

1. Шаблоны проектирования

Мы можем поменять шаблон проектирования внешнего архитектурного слоя с Model-View-Presenter (MVP) на Model-View-ViewModel (MVVM) или на Model-View-Intent (MVI):

12a7192c3b6d81e6e8960525d2a4c173.png

Из-за своей особенности шаблону MVP необходим был дополнительный компонент для инверсии зависимостей и обеспечения направления зависимостей обратно потоку выполнения. Говоря проще, Presenter напрямую указывал View, что делать, но не зависел от ui-слоя.

У MVVM и MVI таких особенностей нет, и нужное направление зависимостей обеспечивается по умолчанию.

Будет ли вам проще поддерживать другой шаблон проектирования, зависит от вашего личного опыта, но если говорить про структуру, то она стала меньше.

2. Конвертация

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

2bbd0106dfe9d7d65d2e027d933a7f02.png

Главное, чтобы Entity при этом оставалась чистой и не содержала никаких упоминаний о модели (DTO), которую нам присылает бэкенд.

Экстремальная экономия

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

Лишние проксирования

1. Проксирующие DataSource-ы

Сначала рассмотрим, для чего нужен был DataSource. Как компонент источника данных, он был необходим для выноса логики по настройке источника. Так мы отгораживаем Repository от платформенной или сторонних зависимостей.

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

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

В Android мы используем Retrofit, с помощью которого можно сократить количество настроек до минимума, а при хорошем подходе можно сократить этот минимум до одного интерфейса API с настройками в аннотациях для конкретных ручек. В итоге компонент DataSource для удалённых источников данных будет служить нам только для проксирования методов API. Почему бы нам не избавиться от него, как от лишнего компонента в случае работы с удалёнными источниками?

4843eaf60a0e6226418501350648fea9.png

Но для локальных источников этот компонент будет всё-таки нужен. Например, в тех случаях, когда мы захотим получать данные удалённо и сохранять их в Preferences, необходим будет Context для настройки этих Preferences:

db5cc9701660bd26e80e4f10977824eb.png

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

  1. Проксирующие UseCase-ы

При должном проектировании наличие бизнес-логики для тонких клиентов — явление редкое. Это даёт нам повод рассуждать об опциональности UseCase-ов, которые очень часто выступают проксирующими классами.

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

e2471fc4c9c0872ea8503f9155682223.png

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

И куда нас это привело? Снова к Чистой Архитектуре!

3c2cbd4c2f74c7271e28f5a2a4cae4bf.png

Все, кто хочет обеспечить на проекте ЧА, стараются приводить структуру своего проекта в соответствие схеме из книги. В мобильной разработке это выглядело примерно так:

Схема из статьи «Заблуждения Чистой Архитектуры»

Но результат экономии отличается своей линейностью:

Линейное направление зависимостей

Линейное направление зависимостей

И выглядит теперь так:

e365ffb6943370d92313f5ad0c97a2d1.png

Возможно, как это бывает в статьях, хающих ЧА, кто-то на этом месте ожидал услышать выводы о том, что мы ушли от ЧА, чтобы обеспечить масштабируемость.

Нет! Мы пришли к ЧА, чтобы обеспечить масштабируемость!

55fbf82064bc099a50ef7414996494ce.png

Полагаю, что с того момента, как был поставлен вопрос «Что делает ЧА дорогой?», мало кто из читателей задался вопросом — »‎А как все компоненты, на которых мы сэкономили, относятся к ЧА?».

Кроме UseCase, для мобильной разработки ни один из убранных компонентов не был обусловлен ЧА. Да и он в книге был упомянут таким же опциональным, когда Дядя Боб говорил, что этот набор слоёв не является обязательным. Однако я не считаю отсутствие UseCase равносильным отсутствию слоя. Как только бизнес-логика появится, появится и соответствующий компонент. А значит слой есть, просто он временно бедствующий ждёт своего часа.

До этого момента я слышал только 2 мнения:

  • ЧА — это плохо потому, что дорого.

  • ЧА — это хорошо, хоть и дорого.

Но моё мнение таково, что всё это время она не была дорогой. Не разбираясь в сути, мы приписывали на её счет необоснованные компоненты. ЧА должна была бороться со стоимостью, и она действительно может с ней бороться.

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

Дядя Боб не был мобильным разработчиком.

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

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

3d0045b20d5c577d9302dc660b528b76.png

Любые изменения в API бэкенда могут привести к тому, что нам придётся менять все слои, чтобы эти изменения поддержать. Что нам это напоминает? SDP: «устойчивым компонентом является тот, изменяя который вы не сможете избежать изменения в остальных компонентах». Получается бэкенд является главным направлением устойчивости для тонких клиентов.

Как бы сильно мы не делали вид, что это не так, как бы много интерфейсов не нагородили, мы всё равно обязаны будем поддерживать все эти изменения. Например, если для одной из ключевых Entity добавилось обязательное поле, отложенное решение по нему можно принять на всех слоях без потери работоспособности приложения. Можно просто не добавлять поле, пока вы не захотите отобразить его. А когда захотите, его придётся поддержать на всех слоях. Можно делать это постепенно, но начиная от data-слоя, а не от domain.

Другой пример: если обязательное поле исчезнет из Entity и старый API перестанут поддерживать, нам придётся в срочном порядке применять эти изменения на всех слоях.

В этих примерах лишние интерфейсы никак не повлияют на наши решения, а попытка направлять зависимости не по направлению устойчивости является противоестественной и ведёт к удорожанию архитектуры.

Единый продукт

41752f1dfb1c40e4b0e165fb3f5a6eab.png

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

c4cd360d0cde665337c2f610afcc5c15.png

И расширим границы до представления единого продукта:

dc69203b9f172980a932add8b13d4217.png

Схема абстрактная, а наличие БД и внешних зависимостей для бэкенда на ней опущено для лучшей концентрации на взаимодействии с клиентами, но они всё еще есть в наших сердцах в рамках структуры бэкенда. Также Entities для клиента и бэкенда редко совпадают настолько, чтобы на схеме они стали общими.

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

Вывод

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

Важно понимать, что мы не стремимся насильно уменьшить количество компонентов под эгидой экономии, перекладывая логику одного слоя на другие. Компонент UseCase-а стал опциональным, но не исключён из структуры в тех ситуациях, где он действительно нужен. Равно как для структуры API-Impl интерфейсы в Repository и UseCase остаются опциональными и необходимы в ситуации, когда код должен быть общедоступным.

А в заключение хочется отметить несколько важных для понимания моментов:

  1. Слои не так важны, как компоненты, из которых они сложены.

Наличие слоёв, перечисленных автором, не является необходимым для обеспечения чистоты архитектуры. Важно уметь обращаться с теми компонентами, из которых состоит ваша архитектура. Из них вы выстраиваете необходимые для вас слои. Если компонент сам по себе бесполезен и нужен для формального существования слоя, то это вредит масштабируемости.

  1. Зависимости зависят от устойчивости.

Мы должны выстраивать зависимости по направлению устойчивости. Иначе возникнут дополнительные расходы на поддержание изменений в самых устойчивых компонентах. Одними интерфейсами не отгородиться от изменений, которые пойдут по цепочке от самых устойчивых компонентов.

  1. Устойчивость не равна редкой изменяемости.

Не тот компонент устойчив, что редко меняется, а тот, меняя который придётся поддержать изменения на всех слоях. Эта мысль взята из книги ЧА. Но считаю важным лишний раз её озвучить.

  1. Интерфейсы ситуативны.

Повторюсь, что в своей книге Мартин упоминал необходимость интерфейсов в двух случаях:

  • Чтобы отгородиться от платформы или иных внешних зависимостей.

  • Чтобы реализовать инверсию зависимостей и обеспечить их направление в сторону устойчивости.

Поэтому не стоит создавать интерфейсы на каждый компонент и оправдывать их ЧА. Необходимо понимать, зачем вы используете тот или иной интерфейс.

  1. Чистота — это свойство.

Любая архитектура может быть чистой, если придерживаться принципов. Если вы обеспечили структуру проекта в соответствии со схемой, которую привёл Мартин, это ещё не значит, что вы достигли тех целей, которые он ставил перед собой.

У всех нас свои проекты и свои платформы. Архитектурные цели у нас у нас общие, но пути их достижения могут отличаться.

  1. Чистая Архитектура дешевле, чем её представляют.

Не обременяйте себя стереотипными взглядами на ЧА. Вполне возможно, что негативный опыт других разработчиков берёт начало не в ЧА, а в неправильном представлении о ней.

dfdd30c14d062c58f4fb835a9fd7b4d6.png

P.S. Управляйте зависимостями, оставайтесь независимыми.

© Habrahabr.ru