Как и обещал, начнём с наброса 😃
Что такое чистая архитектура, зачем нужна, плюсы, издержки.
Если вы работали c ytq, расскажите о своём опыте? Что было круто, что было неудобно? Будем разбираться, действительно ли это полезный инструмент, или просто переусложнённый хайп.
Начнём с того, что такое «Чистая архитектура».
Behold!
В общих чертах: весь код поделен на слои.
Центральный слой (домен) — ядро приложения, максимально независим и отвечает за то, чем приложение отличается от других.
Прикладной слой рулит сценариями, которые специфичны конкретно этому приложению.
Адаптеры и порты — это связь со внешним миром: БД, UI, вот это всё.
Особенно советую — первую. Просто офигенная статья!
Когда я начал постить в блоге (о фронтенде) конспекты книг и статьи об архитектуре, мне стали прилетать вопросы типа «А кому и нафига это вообще надо?».
Я отчасти понимаю природу этих вопросов.
Кажется, что архитектура это что-то далёкое от фронтенда: мы же просто формочки шлёпаем да кнопочки двигаем.
А все эти Мартины и Физерсы как-то уж очень сильно переусложняют.
Вот нафига мне выделять «слои» в приложении, если _всё_ моё приложение — это небольшое PWA с парой кнопочек?
Есть аргументы вида «будет проще переехать с React на что-то ещё» — но я не собираюсь переезжать с React, зачем мне тогда адаптеры для него?
Да чтобы просто _нарисовать_ схему приложения по такой архитектуре у меня времени уйдёт больше, чем на то, чтобы написать его 😃
В чём профит?
Я предлагаю начать с того, что архитектура — это прежде всего инструмент.
У любого инструмента есть область применения и ограничения.
Я, пожалуй, не стану покупать шуруповёрт, чтобы вкрутить один саморез.
Но если саморезов 1000, то я уже подумаю: потратить 5 тысяч на шуруповёрт или лечить в будущем артрит кисти за бóльшие деньги 😃
Архитектура, как и шуруповёрт, стоит ресурсов.
Поддержка сложного проекта с лапше-кодом, как и артрит, — тоже стоит ресурсов, и тоже, как правило, больше.
Свой первый вывод я сделал для себя, когда сравнил маленький и простой проект с навороченной архитектурой и большой и сложный проект без какой-либо архитектуры вовсе.
Дело было так: я однажды попал в сложный проект на PHP с кучей легаси и запутанным кодом.
Ни о какой архитектуре там речи, разумеется, не шло. Ребята зафигачили стартап, он полетел, побежали фичи и баги, а потом пришёл я 😃
Тогда я только-только начинал знакомиться с хорошими практиками в разработке софта, книжки там читать начал, всё такое.
Но уже тогда было понятно, что ясного понимания, как работает система — нет, причём ни у кого 😃
Работать было невозможно, потому что добавишь чё-нибудь-куда-нибудь, где-нибудь-что-то-ещё отвалится.
— Так написали бы тестов, чё.
Ага, мы тоже так подумали 🙂
Не писались там тесты, как бы мы ни старались 😃
Код был написан так, что чтобы протестировать какой-то модуль, приходилось мокать вообще всё подряд.
Как и куда направлены зависимости тоже ясно не было. (Циклические зависимости себя не заставили долго ждать 😅)
Теперь контр-пример: прототип приложения на React.
Надо быстро, поддерживать будет, скорее всего, не нужно. А если и нужно — то всё равно переписывать, потому что дизайн будет другой, UX поменяется и т. д.
Страдая от, кхм, ПТСР с прошлого опыта, я накрутил туда архитектуры по всем правилам: вот тебе и домен, вот тебе прикладной слой, адаптеры, всё независимо, найс.
Только прототип никому не понадобился, а проект затух 😃
Вместо того, чтобы проверить гипотезу, приложив минимум усилий, я вбухал кучу ресурсов.
Ладно хоть писал сам, а то стыдно бы потом было смотреть в глаза команде! 😃
И вот мой первый тогдашний вывод:
== Издержки должны быть меньше выгоды ==
Да, вот так очевидно 😃
После того проекта я решил порефлексировать на него.
Что бы произошло, если бы всё-таки прототип пришлось переписать.
- Сколько кода я бы _мог_ переиспользовать?
- Какой код _надо_ было бы переиспользовать?
*Сейчас пойду поработаю, а после расскажу:
- какие выводы получилось сделать после этого,
- как я использую ЧА сейчас,
- какое минимальное количество усилий стоит прикладывать,
- как понять, что пора расширять инструментарий.
Обед! Продолжим 😃
Итак, что бы произошло, если бы всё-таки прототип пришлось переписать.
- Сколько кода я бы _мог_ переиспользовать?
- Какой код _надо_ было бы переиспользовать?
Кто-то уже мог догадаться, что я клоню к домену.
Домен — это самое главное, что есть в приложении. Та функциональность, которая отличает идею одного приложения от другого.
То, что мне точно пришлось бы перенести из прототипа в продукт — именно домен.
Да, вероятно, с изменениями, возможно, что-то пришлось бы добавить. Но именно этот код _пришлось_ бы переносить.
Второй вывод:
=== Стоит начать с домена ===
Сперва можно и не городить оставшиеся слои, не писать адаптеры к библиотекам, всего этого можно на первом этапе не делать.
Без выделенного домена очень сложно вообще понять, что происходит.
Так было в запутанном проекте из первого примера. Вся логика была разбросана тут и там, понять, какие есть сущности и для чего они, было почти невозможно.
Имей мы на руках функции и модули конкретных сущностей, мы бы уже знали, как они себя ведут и что с ними можно делать.
Ядро системы было бы проще для понимания.
— Ок, допустим. Но вот я читаю книжки об архитектуре, там сплошное ООП. А я не хочу в свой проект его тащить.
Понимаю. Могу обрадовать: архитектура и ООП — вещи ортогональные 😃
Ну то есть понятно, что большая часть книг написана с примерами на ОО-языках, но это не значит, что нам нельзя взять идею и использовать только её.
(Почему с ООП проще строить грамотную архитектуру мы поговорим завтра.)
Домен можно вообще писать как хочется. Главное, чтобы код был понятным и независимым.
Я, если пишу не в ОО-стиле, то люблю описывать домен в виде типов и чистых функций, которые оперируют данными этих типов:
Профит в том, что если проект выстрелит и начнёт быстро расти, вам будет проще накрутить мяса вокруг самого важного кода, чем искать этот самый важный код по всей кодовой базе.
Чем проще и прямолинейнее домен, тем очевиднее, что в системе можно вытворять, а что нет.
А чем очевиднее правила, тем легче выстраивать вокруг них потоки данных и использовать дополнительные инструменты.
— Ладно, это всё, конечно, круто, но ты кажется забыл, что мы тут вс же на JS пишем. Какие нафиг типы? 😃
Отсутствие типов тоже не проблема для выделения домена 🙂
Ну то есть да, статичная типизация помогает проектировать, но и без неё можно справиться.
Ну там JSDoc, объекты-стабы для тестов, те же классы в конце концов.
(Хотя признаю, я начал по-настоящему задумываться о проектировании, когда перелез на TypeScript.)
(Без интерфейсов сложно сконцентрироваться на взаимодействии между сущностями.
Труднее выделять публичное API, абстрагироваться от реализаций. У меня есть ощущение, что JS меня как бы подталкивает думать сперва о реализации, а TS — наоборот.)
Я для прототипов тесты, например, не пишу.
Но как только становится понятно, что из прототипа надо делать продукт, гораздо проще покрыть тестами _уже выделенный код_.
В целом считаю, что выделенный домен — это то самое минимальное необходимое количество ресурсов, которое стоит выделить на архитектуру в самом начале проекта.
Всё остальное, мне кажется, стоит добавлять по мере роста сложности.
— Окей, ладно. С доменом разобрались, допустим. Но вот зачем остальные слои? Они нужны?
Короткий ответ: не всегда. Длинный ответ ↓ 😃
Когда я думал, что «используя слой адаптеров, проще съехать с React», я отвечал себе, что я и не собирался съезжать с React.
И это правда, перебраться с него на какой-то другой шаблонизатор сложно. У него богатая экосистема, куча уже написанных компонентов.
Но что, если я заменю “React” на “Redux” 🙂
Кто-то наверняка задумывался о том, чтобы сменить стейт-менеджер.
Кто-то, наверное, даже успешно его менял на какой-нибудь MobX или что-нибудь ещё.
Так вот, заменить стейт-менеджер обычно — затратное мероприятие.
Он обычно затрагивает много кода: хранилище, события всякие, привязка к UI.
Вместе со всем этим кодом надо и тесты переписывать — а это ещё раза в два больше работы.
С адаптером для стейт-менеджера переезд попроще 🙂
Слой адаптеров — это барьер, который говорит, где заканчивается сторонний код и начинается наш.
Адаптеры и порты делят внешний мир от нашего приложения как мембрана клетки отделяет её от окружающей среды.
И все изменения окружающей среды влияют только на мембрану: появилось что-то, что можно съесть — съели, остальное отсеиваем.
Адаптеры как бы ограничивают распространение изменений. Мы пишем такие «переходники», которые делают внешний мир более удобным для нашего приложения.
Из-за этого и API _приложения_ меняется редко. Адаптеры же можно написать (в идеале) для любой сущности, с которой приложение хочет взаимодействовать.
Это, кстати, ещё и ограничивает распространение ошибок 🙂
- Архитектура — это инструмент. У неё есть издержки и выгоды.
- В какой мере инструмент использовать — определяет разница между издержками и выгодами.
- Не знаете, с чего начать — начните с домена.
- Старайтесь привязывать 3-party код адаптерами...
...Но если это очень дорого и бессмысленно (проект точно не доживёт до момента, когда мы захотим поменять React на что-то ещё) просто держите это в уме (а лучше в документации).
Хорошо, вот мы поняли, что нашему проекту на Реакте _нужна_ суровая масштабируемость, и одним выделением доменного слоя мы не обойдёмся. Что делать?