Мой опыт работ c архитектурой FSD
В этой статье я хочу поделиться своим опытом разработки приложений с применением подхода FSD (Feature-Sliced Design). Здесь не будут рассматривать ее детально, так как на этот счет есть достаточно хороших материалов, начиная с официального сайта (изображения в этой записи взяты именно с него), и заканчивая статьями на Хабре.
Лично мне этот подход показался очень удобным. Причем не только применительно к новым проектам, которые изначально можно структурировать нужным образом. Но также и применительно к проектам, которые кто-то до меня уже вел и оставил в весьма запутанном состоянии. Следуя простым правилам, шаг за шагом, можно привести в порядок и сделать удобным для развития любую кодовую базу. Именно об этих простых правилах и будет данная статья. В свое время я их не нашел и выработал уже на собственном опыте. Надеюсь, мой опыт окажется полезным для кого-нибудь еще.
Базовая идея архитектуры
Весь код разбивается на слои:
- app - верхний уровень приложения;
- pages - отдельные страницы или экраны приложения;
- widgets - блоки интерфейса, из которых состоит страница. Например: верхнее меню, корзина с товарами и т.д. В идеале, страница (page) должна быть максимально "тонкой" и просто располагать внутри себя виджеты, каждый из которых работает независимо от других;
- features - бизнес-фича, представляющая какую-то ценность для пользователя. Например добавление и удаление товаров из корзины, расчет итоговой суммы и скидки.
- entities - если упрощенно, то это данные проекта: товар, пользователь, запись в блог и т.д.
- shared - ресурсы, которые используются внутри приложения всеми остальными слоями. Сюда могут выходить какие-нибудь утилиты, интерфейсы, конфиги сторонних сервисов (подключение к БД, Twilio CLI и т.п.)
Каждый элемент замкнут внутрь себя и содержит все необходимое для работы: ui-компоненты, типы и интерфейсы, утилиты и т.п. Наружу экспортируется только то, что должно быть доступно извне, через публичный API.
Здесь важна иерархия. Элементы могут импортировать и использовать внутри себя только элементы, находящиеся в слое более низкого уровня. Так элемент, находящийся в слое features может использовать элементы из слоев entities и shared, но из своего же слоя или более высоких слоев он ничего применять уже не может. Благодаря этому правилу проект приобретает понятную и четкую структуру.
Например, внося правки в виджет "Корзина" мы можем не опасаться, что они как-то затронут виджет "Список товаров" или изменят логику фичи (feature) ответственной за расчет размера скидки. Изменения коснутся только самого виджета и страниц, на которых он располагается. Соответственно, чем на более низкий уровень мы опускаемся, тем более глобальными и опасными становятся правки.
Итак, опишу подход, который помогает мне придавать проектам удобную для работы структуру.
Шаг 1. Слой Shared: сторонние сервисы
Настройка подключения к базе данных, аутентификации, служб для отправки SMS и т.п. Для этих целей у меня создана отдельная папка "shared/services".
Также можно сразу прописать основные типы, связанные с работой REST API и т.п.
Теперь, когда все сторонние сервисы, с которыми будет взаимодействовать наше приложение, аккуратно собраны в одном месте, можно приступать ко второму шагу.
Шаг 2. Определение Entities
Этот шаг также не требует каких-то сложных оценок проекта. Здесь прописываются бизнес-сущности, а также интерфейсы взаимодействия с ними. Цель этапа - максимально абстрагировать работу с entiites для всех элементов, находящихся в более верхних слоях.
Например, бизнес-сущность NFT (в моем примере использовалась Prisma)
- db - здесь прописываются все обращения к базе данных, которые будут использоваться в приложении. Это позволяет как упростить тестирование, так и проводить оптимизацию запросов, если потребуется
- selectors - селекторы, используемые для запросов к БД через Prisma
- types - типы, сгенерированные из селекторов. В принципе, можно объединить selectors и types в одну папку, при желании.
- ui - если есть какие-то общие UI-компоненты. В моем случае это была кнопка, переводящая посетителя на страницу для оформление покупки NFT. Компонент кнопки расположен именно здесь так как используется в нескольких фичах, а обмен кода между фичами запрещен (так как находятся в одном слое features).
- utils - вспомогательные функции
- index.ts - здесь определяется, что из всего вышеперечисленного будет доступно для остального кода.
Шаг 3. Widgets и Features
Эти два слоя содержат в себе уже бизнес-логику. Я объединил эти два слоя в один шаг, так как граница между ними не всегда очевидна. Формально, разграничение должно быть следующим:
Feature: какое-то ценное действие вроде регистрации пользователя или формы редактирования товара.
Widget: выводит компоненты, создавая из них единый изолированный блок, который уже можно размещать на страницах.
На практике здесь много пограничных ситуаций. Поэтому я рекомендую по умолчанию любой элемент рассматривать как Widget и размещать весь связанный с ним код в его папке. Если какая-то часть виджета окажется восстребованной в другом виджете, то она выводиться в отдельную feature - опускается на слой ниже, чтобы стать доступной для всех widgets.
Порядок действий выходит следующий:
- После того, как прописали все связи со сторонними сервисами и бизнес-сущности, начинаем создавать виджет в отдельной папке, помещая в нее абсолютно весь необходимый код, не стараясь выделать его части в отдельные элементы, размещенные вне этой папки.
- В идеальном варианте виджет целиком сможет замкнуться внутри себя, взаимодействовать только с нашими entites и свободно размещаться на любой странице. Весь код, связанный с ним, будет размещен в одном месте и гарантированно не влиять на другие части приложения.
- Если, при добавлении других виджетов выясняется, что ему требуется использовать код нашего виджета, например, форма для ввода данных банковской карты и обработка запроса на списание денежных средств, то эта форма выносится как отдельный элемент в слой feature. Теперь разные виджеты могут использовать ее в своих целях, а виджет, от куда ее извлекли, сохранился таким же изолированным, как и был.
- В случае, когда по какой-то причине, отправка запроса на списанеи денежных средств с карты потребуется в другой feature, мы перенесем его на самый низкий уровень - shared. Получится что-то вроде (за работу с картами отвечает сервис authorizenet):
То есть правило такое: стараемся разместить код на как можно более высоком уровне - то есть в виджетах, и перемещаем его на более низкие слои только в случае необходимости.
Шаг 4. Pages
По сути - самый верхний уровень. Страницы должны быть максимально "тонкими" и отвечать только за загрузку связанных с ними данных(загрузка с сервера информации о товаре, на основе productSlug в url страницы, например), а также порядок размещения виджетов.
Итого
При первом прочтении архитектура FSD вызывала больше вопросов, чем ответов. Но, на практике, оказалась довольно понятной и простой в работе.
Существенная часть кода группируется по папкам общего назначения entities и shared. После чего, оставшиеся его элементы стараемся разместить в изолированных виджетах и, по мере необходимости, опускаем на слои более низких уровней.
Код, составленный таким образом, довольно легко поддерживать. Вы сразу можете определить "опасные" и "безопасные" для правки части, а также понять, на что именно повлияют изменения. Изменяя форму для ввода данных банковской карты, расположенную в слое features, вы можете совершенно не переживать за непредвиденные эффекты в других features и, тем более, коде, расположенным в более низких слоях. Достаточно лишь будет проверить те несколько виджетов, где форма используется. Если утилита размещена в папке какого-то виджета, это значит, что она используется только для этого виджета и нигде более.
Сравните, насколько это удобнее, чем иметь дело с переполненными папками components и lib.
Для своих проектов я теперь всегда стараюсь привести код к данной архитектуре, вне зависимости от размера приложения.