Архив недели @_sergeysova
Понедельник
Всем доброго утречка. Я Сергей Сова (@_sergeysova). Я лид фронтовой команды Питерского @REDMADROBOT. Меня беспокоит архитектура современного фронтенда на реакте и ему подобных. Изредка появляюсь с докладами и подкастами.
Расскажу о методологии FeatureSlices и Effector.
Давайте начнем с вопроса. Что такое архитектура фронтенда? Это как директории называются? Правила взаимного импорта? Какой стек используется?
Почему меня вообще беспокоит архитектура и структура проекта? Начну с того, что "хорошая" структура вносит ясность. Где искать конкретный код. Куда положить новый. Как писать код командой не внося изменения одновременно в один файл.
С архитектурой сложнее, ведь она про отношение сущностей вашего проекта. Как выстроить зависимости между сущностями и модулями так, чтобы добавление нового кода не требовало внесения изменений в существующий код.
Если со структурой все примерно понятно — описал конвенции в README, накинул правила в eslint и dependency-cruiser, а CI поможет команде соблюдать правила.
С архитектурой такой финт уже не прокатит. Пока не существует линтеров, позволяющих отслеживать связи сущностей
И если погрузиться чуть глубже, то скорее всего такой линтер и не получится создать. Архитектура это какой-то страшный баланс между конвенциями и здравым смыслом.
Мне это напоминает стопку сбалансированных камней. Только архитектор может удерживать проект в устойчивом положении. А разработчики как муравьи на поверхности камней, могут лишь перемещать отдельные камушки так, чтобы при взгляде сверху появилась какая-то структура
Тред (Сергей Сова)
Когда проект начинается, сразу раскладывать по какой-то сложной структуре вообще не хочется. Кажется, что это слишком занудно и на начальном этапе никакой пользы не несет. И это правда. Но есть и другая правда.
Проект растет и растет количество кода, вместе с директориями и файлами. Если не вводить четкой структуры проекта на раннем этапе разработки, развитие проекта превращается в сплошную боль. Хотя никто не запрещает класть код куда придется. Добавлять код легко и это главное.
Проблема в изменении существующего кода, ведь найти все возникшие дубликаты и использование функций/компонентов будет крайне сложно. Возникает так называемый WriteOnly код. Можно только добавлять новый, но не исправлять старый.
Но постой-ка Сова, напишете вы, Чистая архитектура Мартина советует выстроить проект таким образом, чтобы любая новая функциональность требовала только добавления кода, без изменения существующего.
Но давайте вспомним что мы живем в реальном мире, хоть минимальные, но изменения придется внести в кодовую базу. Периодически бизнес требует обновления фич, появляется старый код. Его нужно удалять.
Здесь архитектура и структура проекта играет ключевую роль. Архитектура позволяет выстроить взаимодействие высокоуровневых блоков приложение.
Структура проекта позволяет легко искать существующий код, а также добавлять новый в очевидные места.
Разумеется придется изучить архитектуру и структуру выбранную в проекте, чтобы понимание стало интуитивным. А если вспомнить, что мы проводим большую часть времени читая код, исправляем существующий, а остальное время пишем новый, то профит от обучения становится очевидным
Тред (Сергей Сова)
Вторник
А что на счет microfrontends? Это ли не идеальная архитектура?
Вот только стоит учесть, что microfrontends также как nrwl это не архитектура. А лишь способ организовать часть архитектуры. Где-то упростить, но обязательно усложнить систему целиком.
Есть случаи, когда это оправдано. Но далеко не всегда.
@jsunderhood Без привязки к конкретному проекту и условиям его разработки говорить об идеальном решении не корректно.
Все архитектуры плохие. Нет универсальных хороших решений. Да, есть типичные проекты, архитектура которых весьма однотипна. Но в остальных случаях ни о какой универсальности речи быть не может
twitter.com/kolyuchii/stat… twitter.com/kolyuchii/stat…
По большому счёту, архитектура не про конкретные инструменты. Она о том как разделить код приложения и легче его поддерживать.
Кто-то при старте приложения проектирует так, чтобы любую часть можно было легко заменить. Думать, что это маловероятно несколько опрометчиво
Если проект призван жить долго, то он уже стал «кораблем Тесея» — рано или поздно все его составные части будут заменены.
В большинстве тредов я буду говорить об архитектуре в пределах одного проекта-приложения.
Всякие сложные решения вроде монорепо с микрофронтендами не тема этой недели. Может быть обсудим это после, в моем личном аккаунте.
Но при этом всём, вполне возможно описать универсальную структуру проекта.
Универсальность здесь будет не в названиях директорий и формате файлов. А в заложенных принципах, по которым строится проект.
Понял принципы — освоился со структурой — погрузился в архитектуру
Среда
За мои несколько лет попыток сделать универсальную архитектуру, я случайно обнаружил структуру, которая позволяет обобщить подходы к проектированию и облегчить проработку архитектуры.
Называется FeatureSlices
Тред (Сергей Сова)
FeatureSlices существует на стыке множества разных подходов, там и .NET, и hexagonal, и вообще много опыта в разных сферах.
Сейчас, это методология, без жестких рамок. Основная цель — упростить добавление нового кода и изменение существующего.
Первое, что я стремлюсь сделать в любом проекте, это определить опасный и безопасный код. Опасный код — это такие строки в вашем проекте, изменение которых, может легко сломать зависящие от него модули. Безопасный код напротив, влияет лишь на модуль в котором он описан.
К сожалению, невозможно писать лишь безопасный код. Ведь тогда, от этого кода никто не зависит, а значит его никто не использует. А код который не используется — бесполезен.
К счастью, многолетняя история программирования подарила нам волшебную концепцию — инкапсуляцию. Мы можем скрыть детали реализации за красивым интерфейсом. И тут интерфейсом может быть всё что угодно: методы класса, интерфейсы джавы, экспорты в javascript и rust модулях и т.д.
Задача инкапсуляции — скрыть все детали под красивым названием, может даже несколькими. Ровно так мы и поступаем, когда заворачиваем несколько JSX-элементов в компонент с “говорящим” названием.
И если продолжить аналогию, то легко понять, что код внутри компонента безопасен, его можно менять не переживая, что где-то сломается код зависящий от этого компонента. Разумеется, это работает пока не изменяется интерфейс компонента — названия и типы его пропс.
Задача архитектуры разделить весь проект на высокоуровневые блоки-компоненты, обезопасив код внутри каждого.
Немного поразмыслив, я понял, что любой реюзабельный код опасен. Чем чаще его используют, тем сложнее вносить изменения в такой код. Но при этом, в проектах можно четко провести границу между опасным и безопасным кодом — просто положив код в разные директории со своей семантикой.
Если я понимаю с каким типом кода я работаю, я знаю, что я могу с ним сделать.
При некотором исследовании разных подходов, оказалось, что переиспользовать код страниц не имеет смысла. Ведь одна страница это один конкретный путь роутера.
При этом UI-компоненты, парсеры, конвертеры и прочий библиотечный код можно вполне закономерно вынести в директорию библиотечного кода, покрыть тестами, написать документацию. Чем не полноценный NPM-пакет.
Тред (Сергей Сова)
Четверг
В идеале, бизнес-логика должна быть отделена от остальных частей приложения. Может быть даже вынесена в отдельный большой модуль, который можно рефакторить не боясь разломать все приложение.
Рефакторинг — изменение кода, не изменяющее поведение и интерфейсы модулей. Всякие вьюхи, контроллеры навигации и прочие модули системы должны зависеть от бизнес-логики, а не наоборот.
Во время проектирования я предпочитаю думать о вьюхах и контроллерах как о заменяемых модулях. В какой-то момент в будущем мне понадобится заменить React на другую библиотеку рендеринга, роутер тоже будет заменен, также как и модуль доступа к backend.
Даже если мне никогда не понадобится заменять React, такой подход позволяет четко разделить обязанности и описывать модули с минимальной связанностью.
Но в своих проектах я далеко не всегда выделяю бизнес-логику в отдельный пакет. Все потому, что придется придумывать способ инжектить логику в реакт, настраивать сервисы.
Архитектура должна развиваться вместе с проектом и требованиями к нему. А значит, пока выделение бизнес-логики в отдельный пакет не понадобилось, делать этого я не буду.
Но соломку конечно же подстелю — разделю вью и модель так, чтобы в будущем выделение бизнес-логики в отдельный пакет не становилось невозможной задачей, без изменения вью.
Итак, определенно у нас есть код страниц, модель которых никак не переиспользуется. Это всякие контроллеры форм, редиректы и прочее, в общем это логика управления страницей.
В моем случае, разделение логики и вью это размещение кода в двух разных файлах-модулях.
Например: pages/login/{index.tsx, model.ts}
При необходимости сама бизнес-логика может быть легко выделена из модели в отдельный модуль, при этом в модели страницы останется только связка конкретных форм и абстракной бизнес-логики.
А еще есть код, который разделяется некоторыми страницами, то есть используется сразу в нескольких местах. Например, данные о текущей сессии, права доступа, имя пользователя, и прочее. Куда же класть модель управления этими данными? Но это точно не библиотечный код.
Тред (Сергей Сова)
Вернемся к разделяемой логике. Куда класть модель управления данными используемыми сразу в нескольких страницах?
В методологии FeatureSlices это называется feature. Одна фича это переиспользуемый общий код относящийся к одной сущности. Этой сущностью может быть как бизнес-сущность(пользователь, блог-пост или рекламная кампания), а также техническая сущность (но лучше без таких).
В случае аутентификации и авторизации, бизнес-сущностью является текущий пользователь, так называемый viewer.
В фиче viewer может лежать все что нужно для работы с текущим пользователем: проверка прав доступа, компонент для этого, разделяемые данные этого пользователя, модели фонового обновления данных, завершение текущей сессии пользователя и т.п.
Но при этом, процессы входа(создания сессии) не являются фичей, так как не разделяются и не переиспользуются на разных страницах.
Ведь способов входа может быть достаточно много: по номеру телефона, email + password, email + link, вход после восстановления пароля, вход после регистрации. И в каждом из способов есть свой особенный флоу и код его реализующий.
Пятница
FeatureSlices не является догмой или строгими правилами по которым нужно проектировать и разделять код. Это рекомендация, цель которой является упрощение разработки приложений.
Если в Вашем приложении имеются общие компоненты или код в процессах аутентификации, вполне можно вынести суб-процессы, отдельные компоненты, валидаторы и другое разнообразие кода в техническую фичу authentication.
Важный момент — документация. Стоит создать README.md для каждой фичи и накидать туда описание фичи, цели использования, мотивация для создания, доступные элементы, обслуживаемые процессы.
Главное не забывать просматривать документацию и сверять с актуальным состоянием, может нужно отрефакторить, чтобы соответствовать целям фичи. А может быть нужно обновить документацию.
Тред (Сергей Сова)
Какие компоненты класть в UI, а что оставить в фиче?
Сразу стоит понять, что компоненты во фронтенд проекте могут лежать в любой директории.
Разделение проекта на директории необходимо, чтобы понимать назначение лежащих внутри файлов, как их менять и что может быть потенциально опасно для модификации. Например, в lib вполне может лежать реализация onClickOutside с хуком React, или же модальные окна с особой логикой.
Важно, чтобы в такой библиотечный код не протекали особенности бизнеса. Ведь если такое случится модифицировать и переиспользовать такой станет очень сложно и чревато ошибками. То же самое относится к директории ui.
UI это внутренняя библиотека строительных блоков проекта. Я уверен, что в любом проекте можно выделить внушительную пачку компонентов без четкой области применения.
Такие компоненты называются контекстонезависимыми. Ведь для использования компонента не нужно учитывать контекст использования и окружающую логику.
Пример: выпадающее меню составлено из выпадающего блока, списка и кнопки с иконкой. При этом, четкой области применения нет ни у самого меню, ни у элементов из которых оно состоит. Все это потенциально может быть использовано в любой части приложения.
Чего нельзя с уверенностью сказать про компонент профиля текущего пользователя. Используя тот компонент, можно построить только верхнеуровневые компоненты для авторизованного пользователя. Для гостя этот компонент более, чем бесполезен, вот так и возникает контекст.
Ставьте лайкосы твитам, а то непонятно, интересно это или я просто не во время пишу твиты и их не видят. Какой там прайм-тайм в IT-твиттере?
У твиттера отвратная аналитика по твитам. Даже у гитхаба и simplecast сильно лучше.
Все компоненты без контекста я укладываю в UI, всякие библиотечные реализации в lib, но только если там есть дополнительный код для обслуживания.
А вот компоненты с четким контекстом располагаются по фичам. Причем необходимо четко определить область применения компонентов, а для этого достаточно, чтобы компонент решал лишь одну общую задачу.
Тред (Сергей Сова)
Суббота
Каким боком @effectorjs помогает выстроить архитектуру приложений? Очень просто: он является языком описания бизнес-логики. А еще и помогает связать логику с представлением.
К чему мы привыкли? Встраиваем управление состоянием внутрь React. И после этого продолжаем называть REACT библиотекой, хотя он уже давно ФРЕЙМВОРК.
Мне не нравится такое положение. Из-за возраста React нам приходится очень долго ждать ConcurrentMode, терпеть издевательства StrictMode и наглости memo.
Чего я хочу от библиотеки рендеринга? Простого разделения ответственности, изящной интеграции СТМ, не сложного SSR, зависимости от состояния.
Да. Я хочу, чтобы вьюха зависела от бизнес-логики и подчинялась её законам. Чтобы не приходилось вымучивать интеграцию, и не пытаться адаптировать БЛ под Реакт. Бесит!
Помните нас «ублажали», что Реакт это «функция от состояния»? Мол, передали пропсы сверху в корневой компонент и получили всегда одинаковое состояние?
Ага. Только если не используем хуки и классовые компоненты. Очевидно, что нужно иметь способ управлять состоянием приложения снаружи.
Взять даже пресловутый SSR. Посмотрите на костыли NextJs, для того, чтобы запустить релевантные функции загрузки данных.
Просто вдумайтесь: чтобы сделать SSR, пришлось построить огромный ФРЕЙМВОРК поверх реакта и новую экосистему! Экосистему, Карл!
Чем дольше я пишу на реакте и чем больше создаю веб-приложений, тем больше убеждаюсь, что вьюха должна строиться вокруг бизнес-логики. Написал модель — прицепил вьюху.
То, что делает Redux, RxJs и MobX так это встраивание БЛ внутрь жизненного цикла слоя представления. А это сразу же накладывает кучу тупых ограничений:
БЛ-процесс запускается из вьюхи, а значит на сервере ждет проблема в виде необходимости вытащить этот запуск из вьюхи, либо рендерить несколько раз, чтобы запустилось точно всё.
А в браузере нужно дождаться рендера компонента, чтобы запустить логику. Вместо того, чтобы запустить логику сразу, а рендерить уже компоненты по актуальным данным
Нельзя просто так запустить логику вне компонента. Ведь СТМ лежит в контексте и извлечь наружу весьма проблемно. Придется создать служебный компонент и иже с ним...
...логику использования этого служебного компонента(привет reatom), потерю семантичности jsx разметки. Притом, что у нас и так слишком много всяких служебных Provider, которые не про вьюху, а про какие-то данные
Если нужно, чтобы БЛ работала по своему особенному жизненному циклу не связанному с ЖЦ реакт компонента, то извините, придумывайте костыли.
Я крайне не понимаю людей, которые рекомендуют не брать СТМ и писать на чистом реакте. Это же билет в один конец — станция метро «Наследие».
Очень легко писать WriteOnly код. А как потом разбираться в куче memo и неизвестных причин рендера/ремаунта, которые запускают логику повторно?
Тред (Сергей Сова)
Пару лет назад, я понял, что хочу себе СТМ, который позволит из вьюхи отправлять оповещения о произошедших событиях: клики, ввод, маунт и прочее.
Чтобы БЛ жила отдельно своим процессом и могла самостоятельно реагировать на события и решать, нужно ли запустить логику какую-то или можно проигнорировать.
Вьюха стала бы «функцией от состояния». БЛ обновляла бы состояние когда ей потребуется, именно те кусочки стейта, которые реально обновили данные.
А прикиньте, как круто избавиться от необходимости в селекторах и мемоизации?
А что если состояние приложения не уложить в дерево без поломки очевидности кода?
Как избавиться от гонок обновлений в ромбовидных зависимостях?
Может есть способ не тащить 100Кб СТМ без привязки к классам, но получить все эти возможности?
Тред (Сергей Сова)
Давайте попробуем старую практику как мир: лайк этому посту от вас, факт про эффектор, его историю и концепции от меня. Посмотрим насколько меня хватит!
Изначально effector был миддлварой для ридакса и разрабатывался для высоконагруженного клиента реалтайм чата, вроде Телеграм.
Пока флоу не умер, имелась его первоклассная поддержка. Сейчас типы затачиваются под TypeScript.
Flow и Reason остались для поддержки обратной совместимости.
Команда effector очень щепетильно относится к своим пользователям. Не выкатывая мажорных версий с кучей несовместимых изменений.
Все новые фичи добавляются в минорных версиях. Мажорные версии только удаляют устаревшие методы. Мажор выход раз в год. Вот скоро будет 21 версия
Русскоязычный чат в Телеграм ждет ваших вопросов и предложений. Удивительно, но у эффектора есть пользователи по всему миру, хоть и не так много.
t.me/effector_ru
Эффектор занимается обработкой данных, а не жонглированием декораторами, стримами, пропсами и мемоизацией. Всё в его API нацелено на данные
Во времена первых версий требовался инструмент, позволяющий управлять данными в сложных приложениях без опасности раздуть монолитный центральный стор, с явным control flow, нормальной типизацией и емким API
Сторы для приложения должны быть лёгкими, насколько это возможно — не должна пугать мысль о том, что нужно добавить ещё один стор для конкретных нужд
Сторы должны свободно совмещаться — идея в том, что данные, которые потребуются приложению, можно распределить статически, заранее показав как данные будут преобразоваться во время работы приложения
Принцип работы должен by design исключать необходимость в reselect, оповещая об изменениях только тех, кому они необходимы.
Это позволяет не задумываться о том, что у тебя будет триггериться всё приложение если ты захочешь вынести стейт модалки из реакта. По совместительству это означает что приложения избавлены от проблем с перфомансом, возникшим у react-redux при переходе на контекст
Возможность, место, и способ вынести любую требуемую бизнес-логику из view, максимально упрощая компоненты
Независимость от спорных концепций — никаких декораторов, никаких зависимостей от реакта/rxjs либо необходимости юзать классы или прокси
Ничего из этого не требуется для управления состоянием приложения и поэтому апи библиотеки использует только функции и простые js объекты
Предсказуемость API — небольшое число базовых принципов переиспользуются в различных кейсах, снижая нагрузку на юзера и повышая узнаваемость. зная как работает .watch в эвентах, можно догадаться, что делает функция .watch у стора.
Те же базовые принципы позволяют построить вокруг эффектора дополнительные библиотеки методов, не снижая очевидности кода. Например, patronum. Это явный показатель хорошей расширяемости
Приложение строится из комбинации базовых элементов и возможности строить новые.
Нет никакого смысла стремиться выдать всё за стрим, за редьюсер или за обсервабл, в приложении требуются они все, и библиотека предлагает решение чтобы управлять структурой данных, а не скрывать её
Отличие эффектора от контекста, и разница с внутриреактовыми сторами сводятся к одному: зонам ответственности.
Концепция реакта подразумевает, что фронтенд-разработчики — это части гигантского организма, в котором всё предопределяется наверху, а разработчики могут максимум лишь реагировать (reacts) на то, что им свалилось сверху на этот раз, не имея ни малейшего шанса повлиять на это
Вся концепция эффектора предполагает, что мы сами решаем, что и когда мы отображаем, и каким образом обрабатываем — при этом решаем сразу, заранее, избегая ситуативных субоптимальных решений.
Метод sample взял свое название из микроэлектроники. Гуглить «сэмплирование сигнала». Назначение: взять данные из стора по событию и переслать.
Эффектор предполагает, что все редюссеры чистые. Почитать про чистоту effector.now.sh/docs/glossary
Для асинхронных вычислений имеется отдельный контейнер — Эффект. Называется так, потому что его нужно использовать еще и для локализации сайд-эффектов.
20 и 21 версия вовсе не означает, что эффектор прошел столько мажорных обновлений. На самом деле, два года разработка шла до 0.18. А затем 19.0 без изменений вообще github.com/zerobias/effec…
Типы тестируются не привычным для всех способом — с помощью снапшотов сохраняется реакция системы типов на код. Таким образом можно легко проверить новую версию тс или эффектора github.com/zerobias/effec…
Уже несколько лет автор разрабатывает Effector full-time на пожертвования и помощь сообщества. Призываю закинуть $5-10 в месяц для развития такого мощного инструмента. patreon.com/zero_bias
Воскресенье
В качестве основ для концепций эффектора использовался Лисп. Но больше как теория, а не фактические решения. …ias-papers.s3-eu-west-1.amazonaws.com/interlisp-vm.p…
Я заметил тенденцию в Телеграм сообществе. Все неудачные решения тяжело реализовать на Effector. При этом хорошие продуманные кейсы, достаточно просто реализуются. Считаю, что Effector стимулирует продумывать предметную область
Это определенно очень круто! Столько желающих. Но это сильно больше, чем мое воображение и память способны выдать за ограниченное время! Огромное спасибо всем, за лайки и вопросы.
Я отвечу на все оставшиеся вопросы со своего аккаунта. А также постараюсь добить тред фактами.
Начнем новый тред про архитектуру?
Подписывайтесь @_sergeysova
Тред (Сергей Сова)
Всем спасибо за эту неделю!
Вести коллективный аккаунт это круто. Видеть, что люди согласны с высказываемыми мыслями или же делятся своей точкой зрения, приводя весьма интересные аргументы.
Очень приятно видеть лайки от людей, которых я уважаю. Не знаю, что именно они выражают этим, но надеюсь согласие.
Надеюсь на интересные и жаркие дискуссии в будущем. Расскажите, как вы проектируете свои React-приложения?