Как и обещал, начнём с наброса 😃
Что такое чистая архитектура, зачем нужна, плюсы, издержки.
Если вы работали c ytq, расскажите о своём опыте? Что было круто, что было неудобно? Будем разбираться, действительно ли это полезный инструмент, или просто переусложнённый хайп.
Начнём с того, что такое «Чистая архитектура».
Behold!

В общих чертах: весь код поделен на слои.
Центральный слой (домен) — ядро приложения, максимально независим и отвечает за то, чем приложение отличается от других.
Прикладной слой рулит сценариями, которые специфичны конкретно этому приложению.
Адаптеры и порты — это связь со внешним миром: БД, UI, вот это всё.
Если хочется узнать побольше, то вот пара ссылок:
- herbertograca.com/2017/11/16/exp…
- habr.com/ru/post/269589/
- bespoyasov.ru/blog/clean-arc…
Особенно советую — первую. Просто офигенная статья!
Когда я начал постить в блоге (о фронтенде) конспекты книг и статьи об архитектуре, мне стали прилетать вопросы типа «А кому и нафига это вообще надо?».
Я отчасти понимаю природу этих вопросов.
Кажется, что архитектура это что-то далёкое от фронтенда: мы же просто формочки шлёпаем да кнопочки двигаем.
А все эти Мартины и Физерсы как-то уж очень сильно переусложняют.
Вот нафига мне выделять «слои» в приложении, если всё моё приложение — это небольшое PWA с парой кнопочек?
Есть аргументы вида «будет проще переехать с React на что-то ещё» — но я не собираюсь переезжать с React, зачем мне тогда адаптеры для него?
Да чтобы просто нарисовать схему приложения по такой архитектуре у меня времени уйдёт больше, чем на то, чтобы написать его 😃
В чём профит?
Я предлагаю начать с того, что архитектура — это прежде всего инструмент.
У любого инструмента есть область применения и ограничения.
Я, пожалуй, не стану покупать шуруповёрт, чтобы вкрутить один саморез.
Но если саморезов 1000, то я уже подумаю: потратить 5 тысяч на шуруповёрт или лечить в будущем артрит кисти за бóльшие деньги 😃
Архитектура, как и шуруповёрт, стоит ресурсов.
Поддержка сложного проекта с лапше-кодом, как и артрит, — тоже стоит ресурсов, и тоже, как правило, больше.
Свой первый вывод я сделал для себя, когда сравнил маленький и простой проект с навороченной архитектурой и большой и сложный проект без какой-либо архитектуры вовсе.
Дело было так: я однажды попал в сложный проект на PHP с кучей легаси и запутанным кодом.
Ни о какой архитектуре там речи, разумеется, не шло. Ребята зафигачили стартап, он полетел, побежали фичи и баги, а потом пришёл я 😃
Тогда я только-только начинал знакомиться с хорошими практиками в разработке софта, книжки там читать начал, всё такое.
Но уже тогда было понятно, что ясного понимания, как работает система — нет, причём ни у кого 😃
Работать было невозможно, потому что добавишь чё-нибудь-куда-нибудь, где-нибудь-что-то-ещё отвалится.
— Так написали бы тестов, чё.
Ага, мы тоже так подумали 🙂
Не писались там тесты, как бы мы ни старались 😃
Код был написан так, что чтобы протестировать какой-то модуль, приходилось мокать вообще всё подряд.
(Говоря умными словами, код был сильно зацеплен: ru.wikipedia.org/wiki/Зацеплени…)
Поддерживать этот комбайн было трудно. И не только потому, что тестировать было неудобно — было трудно даже понять, что и какой модуль должен делать 😃
Сейчас я бы сказал, что код доменного слоя был размазан ровным слоем (будум-тсс) по всему приложению.
Всё было намешано в кучу.
Держать в голове даже один модуль было трудно: модуль мог и за шаблонизацию отвечать, и за преобразование данных.
(Умными словами, нарушал SRP: ota-solid.vercel.app/srp)
Как и куда направлены зависимости тоже ясно не было. (Циклические зависимости себя не заставили долго ждать 😅)
Теперь контр-пример: прототип приложения на React.
Надо быстро, поддерживать будет, скорее всего, не нужно. А если и нужно — то всё равно переписывать, потому что дизайн будет другой, UX поменяется и т. д.
Страдая от, кхм, ПТСР с прошлого опыта, я накрутил туда архитектуры по всем правилам: вот тебе и домен, вот тебе прикладной слой, адаптеры, всё независимо, найс.
Только прототип никому не понадобился, а проект затух 😃
Вместо того, чтобы проверить гипотезу, приложив минимум усилий, я вбухал кучу ресурсов.
Ладно хоть писал сам, а то стыдно бы потом было смотреть в глаза команде! 😃
И вот мой первый тогдашний вывод:
== Издержки должны быть меньше выгоды ==
Да, вот так очевидно 😃
После того проекта я решил порефлексировать на него.
Что бы произошло, если бы всё-таки прототип пришлось переписать.
- Сколько кода я бы мог переиспользовать?
- Какой код надо было бы переиспользовать?
*Сейчас пойду поработаю, а после расскажу:
- какие выводы получилось сделать после этого,
- как я использую ЧА сейчас,
- какое минимальное количество усилий стоит прикладывать,
- как понять, что пора расширять инструментарий.
Обед! Продолжим 😃
Итак, что бы произошло, если бы всё-таки прототип пришлось переписать.
- Сколько кода я бы мог переиспользовать?
- Какой код надо было бы переиспользовать?
Кто-то уже мог догадаться, что я клоню к домену.
Домен — это самое главное, что есть в приложении. Та функциональность, которая отличает идею одного приложения от другого.
То, что мне точно пришлось бы перенести из прототипа в продукт — именно домен.
Да, вероятно, с изменениями, возможно, что-то пришлось бы добавить. Но именно этот код пришлось бы переносить.
Второй вывод:
=== Стоит начать с домена ===
Сперва можно и не городить оставшиеся слои, не писать адаптеры к библиотекам, всего этого можно на первом этапе не делать.
Но выделить домен — стоит обязательно.
Я это называю, кхм, «прагматичной архитектурой» 😃
Это как правило 20/80, только про дизайн систем.
ru.wikipedia.org/wiki/Закон_Пар…
Без выделенного домена очень сложно вообще понять, что происходит.
Так было в запутанном проекте из первого примера. Вся логика была разбросана тут и там, понять, какие есть сущности и для чего они, было почти невозможно.
Имей мы на руках функции и модули конкретных сущностей, мы бы уже знали, как они себя ведут и что с ними можно делать.
Ядро системы было бы проще для понимания.
— Ок, допустим. Но вот я читаю книжки об архитектуре, там сплошное ООП. А я не хочу в свой проект его тащить.
Понимаю. Могу обрадовать: архитектура и ООП — вещи ортогональные 😃
Ну то есть понятно, что большая часть книг написана с примерами на ОО-языках, но это не значит, что нам нельзя взять идею и использовать только её.
(Почему с ООП проще строить грамотную архитектуру мы поговорим завтра.)
Домен можно вообще писать как хочется. Главное, чтобы код был понятным и независимым.
Я, если пишу не в ОО-стиле, то люблю описывать домен в виде типов и чистых функций, которые оперируют данными этих типов:
github.com/bespoyasov/www…
Профит в том, что если проект выстрелит и начнёт быстро расти, вам будет проще накрутить мяса вокруг самого важного кода, чем искать этот самый важный код по всей кодовой базе.
Чем проще и прямолинейнее домен, тем очевиднее, что в системе можно вытворять, а что нет.
А чем очевиднее правила, тем легче выстраивать вокруг них потоки данных и использовать дополнительные инструменты.
— Ладно, это всё, конечно, круто, но ты кажется забыл, что мы тут вс же на JS пишем. Какие нафиг типы? 😃
Отсутствие типов тоже не проблема для выделения домена 🙂
Ну то есть да, статичная типизация помогает проектировать, но и без неё можно справиться.
Ну там JSDoc, объекты-стабы для тестов, те же классы в конце концов.
(Хотя признаю, я начал по-настоящему задумываться о проектировании, когда перелез на TypeScript.)
(Без интерфейсов сложно сконцентрироваться на взаимодействии между сущностями.
Труднее выделять публичное API, абстрагироваться от реализаций. У меня есть ощущение, что JS меня как бы подталкивает думать сперва о реализации, а TS — наоборот.)
Я для прототипов тесты, например, не пишу.
Но как только становится понятно, что из прототипа надо делать продукт, гораздо проще покрыть тестами уже выделенный код.
В целом считаю, что выделенный домен — это то самое минимальное необходимое количество ресурсов, которое стоит выделить на архитектуру в самом начале проекта.
Всё остальное, мне кажется, стоит добавлять по мере роста сложности.
— Окей, ладно. С доменом разобрались, допустим. Но вот зачем остальные слои? Они нужны?
Короткий ответ: не всегда. Длинный ответ ↓ 😃
Когда я думал, что «используя слой адаптеров, проще съехать с React», я отвечал себе, что я и не собирался съезжать с React.
И это правда, перебраться с него на какой-то другой шаблонизатор сложно. У него богатая экосистема, куча уже написанных компонентов.
Но что, если я заменю “React” на “Redux” 🙂
Кто-то наверняка задумывался о том, чтобы сменить стейт-менеджер.
Кто-то, наверное, даже успешно его менял на какой-нибудь MobX или что-нибудь ещё.
Так вот, заменить стейт-менеджер обычно — затратное мероприятие.
Он обычно затрагивает много кода: хранилище, события всякие, привязка к UI.
Вместе со всем этим кодом надо и тесты переписывать — а это ещё раза в два больше работы.
С адаптером для стейт-менеджера переезд попроще 🙂
Слой адаптеров — это барьер, который говорит, где заканчивается сторонний код и начинается наш.
Адаптеры и порты делят внешний мир от нашего приложения как мембрана клетки отделяет её от окружающей среды.
И все изменения окружающей среды влияют только на мембрану: появилось что-то, что можно съесть — съели, остальное отсеиваем.
Адаптеры как бы ограничивают распространение изменений. Мы пишем такие «переходники», которые делают внешний мир более удобным для нашего приложения.
Из-за этого и API приложения меняется редко. Адаптеры же можно написать (в идеале) для любой сущности, с которой приложение хочет взаимодействовать.
Это, кстати, ещё и ограничивает распространение ошибок 🙂
Об этом писал Ганмер в «Паттернах отказоустойчивых приложений»:
bespoyasov.ru/blog/patterns-…
(Офигенная книжка, очень советую.)
Кроме ошибок, это ещё и помогает рефакторить код.
Разделение по слоям — идеальный «шов», как называет его Физерс в «Эффективной работе с легаси»:
bespoyasov.ru/blog/working-e…
(Тоже советую 😃)
Итак, к этому моменту:
- Архитектура — это инструмент. У неё есть издержки и выгоды.
- В какой мере инструмент использовать — определяет разница между издержками и выгодами.
- Не знаете, с чего начать — начните с домена.
- Старайтесь привязывать 3-party код адаптерами...
...Но если это очень дорого и бессмысленно (проект точно не доживёт до момента, когда мы захотим поменять React на что-то ещё) просто держите это в уме (а лучше в документации).
Хорошо, вот мы поняли, что нашему проекту на Реакте нужна суровая масштабируемость, и одним выделением доменного слоя мы не обойдёмся. Что делать?
Теперь немного о собственно проектировании. Допустим, мы знаем, что нашему проекту нужна суровая масштабируемость. Что делать? Первым делом стоит взять ручку, бумажку и пойти «программировать ногами» 😃 twitter.com/lizuschiykotik…
Писать код ещё рано. Проектиурем!
twitter.com/jsunderhood/st…
Да! ^_^ Недавно меня уже спрашивали в Твитере, что-куда-и-как можно вынести. Я ответил на примере приложения с котиками 😼 twitter.com/ch_ronik/statu…
Прорабатываем взаимодействие модулей:
twitter.com/jsunderhood/st…
Пишем код домена.
Начать лучше именно с него, потому что всё приложение мы будет строить под его нужды.
Можно писать как угодно: если достаточно типов пары сущностей и одной-двух функций — замечательно. Это будет доменом.
При необходимости дополняем.
Прикладной слой и порты с адаптерами.
Я, кстати, видел даже неплохие стартовые шаблоны для того же Реакта:
- github.com/eduardomoroni/…
- github.com/bailabs/react_…
Держим при написании в голове разницу между выгодами и издержками.
Если вы чувствуете, что оверижинирите, опишите в документации описание и не плодите лишний код.
Если вы чувствуете, что вот это место, как слишком сильно сцеплено с 3-party кодом — добавляйте адаптеры.
Архитектура — это не только возможность масштабироваться, но ещё и увеличенный порог входа. Возможно, будет нужен онбординг.
(Я, например, когда маленьким был, долго не мог вдуплить, что такое DI и как он работает.)
Стараемся не плодить абстракций сверх меры.
Слои нам нужны, чтобы не смешивать зоны ответственности модулей.
И вот мой первый тогдашний вывод: == Издержки должны быть меньше выгоды == Да, вот так очевидно 😃
Плохо умею в треды, продолжение вот тут, простите 😅
twitter.com/jsunderhood/st…