Всем привет, меня зовут Сергей Сибара, я фронтенд-разработчик в ИТ-холдинге Т1. Так как при использовании Feature-Sliced Design (FSD) возникает много вопросов и разные люди понимают её по-разному, я решил написать статью-справочник, раскрывающий некоторые подробности методологии. В этой статье я продолжаю использовать те же принципы и часть терминологии, что и в предыдущей.
Здесь я, в основном, описываю структурирование по рекомендациям методологии. А в следующей статье, напротив, рассмотрю, как можно улучшить структуру проекта, намеренно нарушая рекомендации FSD. Заранее предупрежу, что правила методологии носят рекомендательный, а не обязательный характер. Их назначение — задать направление структурирования, а дальше принимать решения нужно в зависимости от конкретного проекта и ситуации в нём. Строгое же следование рекомендациям может привести к бо̒льшим проблем, чем их нарушение.
Если заметите ошибки — пишите в комментариях!
Содержание
-
Слой shared
1.1 Как избежать свалки в shared/lib?
1.2 Направление зависимостей в слое shared
1.3 Может ли store (слайс Redux store в случае Redux Toolkit) находиться в слое shared?
1.4 В слое shared стоит размещать только то, что можно переиспользовать в других проектах?
-
Сегменты
2.1 Ошибки группирования файлов в сегментах
2.2 Может ли логика интерфейса, константы, хуки и типы находиться в сегменте ui?
2.3 Когда для сегмента создавать папку, а когда файл?
2.4 Можно ли создавать в сегментах публичный API (файлы index.ts)?
-
Слайсы и связь между ними
3.1 Может ли слайс в entities содержать несколько бизнес-сущностей?
3.3 Всегда ли нужно создавать публичный API (файлы index.ts в слайсах)?
3.4 Могут ли вложенные слайсы и сегменты быть на одном уровне?
-
Слои
4.2 Где разместить функциональность запросов к эндпоинтам и DTO-типы?
4.3 Где размещать мапперы в DTO и во View, переводы, схемы валидации, layout-ы страниц?
1. Слой shared
1.1. Как избежать свалки в shared/lib?
Иногда в shared/lib создают папки вроде hooks, consts, types и т. д., что является неправильным подходом. В документации описано, что это ведёт к группированию файлов по их типу, а не по решаемым ими задачам, а это противоречит принципам FSD. Это создаёт сложности, поскольку каждый файл предназначен для решения несвязанных задач, и их, скорее всего, не получится объединить в подсистему.
Более правильным группированием будет создание в shared/lib папок, таких как:
? validation
? modals
? notifications
? localization
? urlUtils
Например, shared/lib/notifications содержит утилиты, хуки, компоненты интерфейса, константы и типы, реализующие механизм уведомлений, используемый в различных частях приложения. Может показаться, что компоненты этой подсистемы следует располагать отдельно в shared/ui, но в таком случае сильно связанные файлы будет разделены. Уточню, что здесь речь не о том, в какой папке размещать механизм уведомлений, он взят в качестве примера. Речь о том, что файлы подобных подсистем зачастую лучше держать рядом.
Может получиться так, что в shared/lib скопится много инфраструктурных подсистем и различных наборов утилит, дополняющих API JavaScript, браузера и сторонних библиотек, и хранить их все в одной папке станет неудобно. FSD не даёт ответа, что делать в таком случае.
1.2. Направление зависимостей в слое shared
Хаотичные связи в этом слое, как и в других слоях, приведут к запутанной структуре. Поэтому желательно контролировать связи между сегментами и их направление. Например, чтобы сегмент ui импортировал файлы из сегмента lib, но не наоборот.
1.3. Может ли store (слайс Redux store в случае Redux Toolkit) находиться в слое shared?
Да, может.
Если хранилища предназначены для работы с бизнес-сущностями, то им не место в слое shared. Однако, если они предназначены исключительно для работы с инфраструктурной логикой, или являются абстрактными и могут работать с произвольными типами без зависимости от бизнес-логики, то такие хранилища вполне можно разместить в слое shared.
1.4. В слое shared стоит размещать только то, что можно переиспользовать в других проектах?
Если не ошибаюсь, то в прошлом в методологии так и рекомендовалось. Сейчас же такого ограничения нет: в shared разрешается размещать API, маршрутизацию, авторизацию, переводы, темизацию и прочую инфраструктурную функциональность. Перечисленная функциональность (за исключением API) не относится к специфике приложения/бизнеса. Она нужна для работы приложения и для удобства работы с приложением, но не для решения каких-либо задач пользователя или бизнеса.
2. Сегменты
2.1. Ошибки группирования файлов в сегментах
Неправильно создавать сегменты вроде hooks, types, consts. Об этом написано в документации. В предыдущей статья я уже писал, что назначение стандартных сегментов в бизнес-слоях, за исключением сегментов lib и config, такое же, как и у паттернов MVC, MVP, MVVM, Flux и т. д.: группирование кода приложения по техническому назначению. В отличие от подобных паттернов, в FSD предлагается группировать такой код ещё и на уровне файловой структуры проекта.
2.2. Может ли логика интерфейса, константы, хуки, типы находиться в сегменте ui?
Может. Неправильно ограничивать сегмент ui только компонентами.
В бизнес-слоях FSD он является аналогом слоя представления в таких паттернах, как MVC, MVP, MVVM, Flux и т. д. В нём могут содержаться хуки, типы (обычно это props, но могут быть типы для других функций и объектов), константы, различная логика интерфейса и утилиты, относящиеся именно к сегменту ui и используемые только в нём. Если же вынести эти файлы в другие сегменты, то получится, что сильно связанный код станет разделён, а это противоречит принципам Low Coupling и High Cohesion.
2.3. Когда для сегмента создавать папку, а когда файл?
Согласно методологии FSD, если в слайсе каждый сегмент содержит не более одного файла, то дополнительная вложенность в виде папок для группирования сегментов не требуется. Однако, как только хотя бы в одном сегменте появляется два или более файлов, для него следует создать отдельную папку. При этом можно сочетать сегменты-файлы и сегменты-папки в рамках одного слайса.
Рекомендация создавать папку-сегмент, если в сегменте более одного файла, может быть неудобным в случае слайсов с небольшим количеством файлов. А кто-то предпочитает сразу делать сегменты папками, аргументируя тем, что потом слайс может разрастись и придётся переносить файлы сегментов в папки. Но если это заранее неизвестно, то зачем усложнять без необходимости? О том, что не нужно усложнять структуру папок без необходимости, довольно хорошо написано в документации к другой методологии.
2.4. Можно ли создавать публичный API (файлы index.ts) в сегментах?
Это допустимо, но в подавляющем большинстве случаев избыточно. Файлы index.ts предназначены для предоставления доступа только к публичным файлам слайса.
Но думаю, что в редких ситуациях с большим количеством файлов в сегментах слайса всё-таки может быть польза от создания файлов index.ts для экспорта в другие сегменты этого же слайса. В таком случае использование публичного API может помочь избежать путаницы в связях между файлами разных сегментов и ограничить импорт приватных файлов в соседние сегменты.
3. Слайсы и связь между ними
3.1. Может ли слайс в entities содержать несколько бизнес-сущностей?
Да, некоторые бизнес-сущности могут быть сильно связаны друг с другом или быть вложенными. Правильнее размещать такие сущности в одном слайсе, чем пытаться искусственно разделить их.
3.2. Стоит ли создавать для каждой сущности слайс с одним и тем же именем в каждом из слоёв: entities, features, widgets, pages?
Если у вас группы слайсов, то, возможно, сто̒ит, но обычно — нет. Если для каждой сущности (например, product) в каждом или почти в каждом слое создаётся папка с именем этой сущности, например:
entities/products/
features/products/
widgets/products/
pages/products/
то, возможно, вы неправильно поняли, по какому принципу в FSD создаются слайсы.
Только слайс из слоя entities привязан к одной или нескольким связанным бизнес-сущностям. А слайсы в слоях features, widgets и pages обычно не привязаны к определённой бизнес-сущности. Какая-нибудь фича может быть общей для группы сущностей или вообще для всех. Какой-нибудь виджет может работать одновременно с несколькими сущностями. А, например, на странице Dashboard может использоваться десяток сущностей.
Зачастую стоит выделять слайсы, относящиеся к определённой функциональности, а не сущности. Например, слайс в слое features, реализующий механизм фильтрации и импортирующий файлы из нескольких слайсов слоя entities.
Если же вы не переиспользуете фичи и виджеты определённой сущности в нескольких слайсах на уровнях выше, и не планируете это делать, то обычно лучше поместить их в папку pages, как это рекомендуется в подходе pages-first.
3.3. Всегда ли нужно создавать публичный API (файлы index.ts в слайсах)?
Нет, это зависит от особенностей проекта, требований к разделению на чанки и наличия проблем с циклическими зависимостями.
Публичные API полезны в больших проектах, так как они уменьшают количество точек взаимодействия между разными подсистемами. Легче разобраться в связях между подсистемами, чем между отдельными файлами.
Не стоит использовать импортирование из публичного API внутри самого слайса, поскольку это может привести к возникновению циклических зависимостей.
3.4. Могут ли вложенные слайсы и сегменты быть на одном уровне?
Согласно принципам FSD — нет: слайсы и сегменты принадлежат к разным уровням организационной иерархии данной методологии и не должны смешиваться.
3.5. Что делать, когда сущности связаны?
Возможны разные ситуации и варианты решения:
Их можно рассматривать как один слайс (без вложенных слайсов) и не делить на отдельные слайсы. То есть во всех слоях считать их одной сущностью, состоящей из нескольких.
Несколько связанных слайсов можно сгруппировать в один большой слайс с вложенными слайсами.
Использовать кросс-импорты в рамках одного слоя с использованием публичного API и @x-нотации.
Варианты с нарушением FSD в зависимости от ситуации, но об этом в следующей статье.
3.6. @x-нотация для публичного API
В FSD с недавнего времени стало допустимо использовать «правильные кросс-импорты» через дополнительные публичные API. Для этого предлагается использовать @x-нотацию и только в слое Entities. Причины её появления описаны тут.
Кому-то такая нотация может показаться неудобной. Это лишь одна из рекомендаций методологии, а не призыв к действию. Можно отступить от неё и использовать другой подход.
4. Слои
4.1. Стоит ли вводить дополнительные промежуточные слои, которых нет в FSD, чтобы избежать кросс-импортов?
Не рекомендуется вводить дополнительные слои. В FSD и без того довольно много слоёв, и даже стандартных рекомендуется использовать как можно меньше. Рассмотрите альтернативы: объединение связанных слайсов, передача через контекст или props, добавление нескольких кросс-импортов, и т. д.
4.2. Где разместить функциональность запросов к эндпоинтам и DTO-типы?
Ответ зависит от конкретного проекта. В одних случаях оптимальным вариантом будет размещение в слое shared, в других — в бизнес-слоях. При этом инфраструктурный код API размещают в shared/api.
Насколько я понял, в FSD обычно принято использовать следующие варианты:
в shared/api/ — если API генерируется или если в нём много связей между разными бизнес-сущностями;
в entities/{имя_слайса}/api/ — при использовании FSD ниже версии 2.1 для проектов, где редки связи между API разных сущностей;
В сегменте api в том же бизнес-слайсе, где вызывается API-запрос — когда проект ориентирован на pages-first из FSD 2.1 и редки связи между API разных сущностей.
В документации подробно описаны:
4.3. Где размещать мапперы в DTO и во View, переводы, схемы валидации, layout-ы страниц?
Многое уже описано в документации по типам. Про расположение схем валидации написано в пункте «Схемы валидации типов и Zod». Про расположение layout-ов в разных ситуациях написано здесь.
Мапперы из DTO вo View
Официальная рекомендация: размещать рядом с DTO. Я с ней не согласен. Для одного DTO может потребоваться несколько мапперов для разных элементов интерфейса, страниц. Если же в проекте весь API находится в слое shared, то в нём окажутся ещё и типы бизнес-блоков интерфейса. В будущем при неаккуратном удалении ненужных компонентов интерфейса они могут там и остаться. На мой взгляд, мапперы правильнее размещать в слайсе, где находится компонент страницы, для отображения в котором делается преобразование.
Насчёт того, в каком сегменте располагать, я считаю, из стандартных сегментов наиболее подходит сегмент model.
Мапперы из View в DTO
Видел, что в официальном чате обсуждался вариант располагать ближе к формам — в сегменте ui, а также обсуждался вариант располагать в сегменте model. На мой взгляд, второй вариант предпочтительнее, да и в таком случае обе разновидности мапперов будут рядом. Работа с данными, в том числе их преобразование, не является ответственностью слоя представления (слоя View в паттернах MV* или его аналога в FSD — сегмента ui).
Маршрутизация
В последние годы я работал только с React Router, поэтому рассматриваю относительно него.
Маршруты, использующие компоненты-страницы, нужны только в слое app для их инициализации в приложении, поэтому там им и место. Либо, если в слайсах слоя pages несколько страниц, то маршруты можно располагать в соответствующих слайсах и импортировать оттуда в слой app.
А вот константы URL-путей могут понадобиться в любом бизнес-слое, поэтому их зачастую помещают в слой shared, например в shared/routes.
Константы переводов
Глобальные переводы обычно хранят в слое shared — например, в shared/i18n. Если же переводы располагаются в бизнес-слайсах, то для такого случая чаще всего в официальном чате я встречал вариант размещения переводов в сегменте i18n в том же слайсе, что и компонент интерфейса.
Заключение
В статье я собрал основные моменты, вызывавшие вопросы и разногласия в проектах с FSD, в которых мне довелось работать.
Конечно же, были ситуации, когда у команды возникал вопрос, к чему относится компонент: к features или entities. Я не стал его рассматривать, потому что нет универсального понятия, «чем считается» тот или иной элемент интерфейса в FSD. К тому же с выходом FSD 2.1 понятие «фича» изменилось.
В следующей статье я рассмотрю ещё несколько рекомендаций методологии, но уже более спорных.
dominus_augustus
Если методология требует ответа на столько вопросов, может методология говно, не?
js2me
Думаю если взять другие методологии, то будет тоже очень много вопросов, на которые требуется ответить.
В целом аналогично можно сказать и про разные архитектуры. Но это же не будет значит, что все архитектуры говно ?
dominus_augustus
Например? Статьями и видео по фсд забит весь хабр и ютуб, все они об одном и том же "как мы сову на глобус натянули". Фундаментальная проблема фсд связана с тем, что эта медология файловой структуры, которую пытаются выдать за архитектуру. Цель любой архитектуры лежит в плоскости расширяемости и переиспользуемости. Как эту проблему решает фсд, не ясно, импорт файла из папки А вместо папки Б не должен влиять на работу системы. Да, удобно расложить все по папочкам это всегда плюс, а не минус. Но если я не знаю куда положить тот или иной файл, в конечно итоге должно быть все равно. Кажется архитектура это про сущности (сторы, сервисы, оркестраторы и т.д.) и их взаимодействие. Но это конечно ИМХО, может фсд единственный верный способ написать приложение, а фсд + редакс это серебряная пуля
js2me
Например? Берём недавнюю статью про FEOD (https://habr.com/p/972410/) и там точно также возникает много вопросов о том что где и как должно лежать. И это мы говорим про хранение файлов и структуру папок в контексте архитектурных методологий / принципов
Берём, например, DDD. По ней, не знаю как у вас, но у меня возникает очень много вопросов в контексте организации кодовой базы.
Поэтому, я считаю, что у всего будут вопросы, главное на них просто отвечать, придерживаясь документации по тем принципам, которые используешь.
Другое дело, когда мейнтейнеры FSD сами не знают где и что должно лежать. Вспомнил, как пару лет назад, я спрашивал где мне расположить компонент Layout, который использовал виджеты, и который использовался на страницах, ну и на такой вопрос, не нарушая принципов FSD нельзя ответить
dominus_augustus
По DDD у меня наоборот вопросов по сути и нет, книгу не читал, только урывками какие-то аспекты, но в целом они прозрачны + добавить туда чистую архитектуру дяди Боба, понятное дело без упоротости в доскональном следовании догмам.
Но в принципе в моем видении приложение выглядит так, верхнеуровнево. Доменная область, в ней лежат сервисы доменной область, сущности (которые entity), агрегаты, все эти вещи образуют собой какой-то кусок бизнес логики, можно гордо назвать это моделью. Вот из этих моделей и состоит наша доменная область.
Доменная область не зависит ни от чего, то есть она изолирована, живет в своем мире и ничего не знает о внешнем слое.
Следующий слой, слой приложения. Тут у нас живут сервисы приложения, ui сторы, вью модели. Сервисы бывают разные, но не вижу смысла их как-то обозначать конкретно, их задача по сути шаринг логики между вью моделями и другими сервисами. Задача сторов такая же, глобально это нужно для шаринга, все остальное, что привязано к конкретному компоненту или части экрана можно держать во вью модели. Ну понятное дело чтобы не перегружать классы что-то выносится в отдельные сервисы/сторы и тд.
Отдельный слой, это слой, который связывает наше приложение с доменной областью. По сути это наборы окрестраторов, которые зависят от интерфейсов и выполняют операции над моделями. Но тут нужно важную ремарку сделать, возможно если следовать чистой архитектуре и ддд все это идеально ложится на бэкенд часть, но с учетом специфики фронтенд разработки, не все удобно. Например в идеале слой приложения должен взаимодействовать с моделями только через юз кейзы, у которых должны быть адаптеры входа для принятия имплементаций интерфейсов и данных для обработки и адаптеры выхода для трансформации данных в формат для приложения. Но фактически это неудобно, я, например, сделал послабление и позволил менять модели не через юз кейзы, а напрямую, когда не требуется оркестрация и операция изменения атомарна для модели. Но когда нам нужно повлиять на несколько моделей, то заводится юз кейз/оркестратор и операция проходит через него. Ну то есть глобально я бы выделил наверное 3 слоя, домен, приложение и пограничный слой между ними для общения. Если говорить о каких-то сущностях, я бы поделил их для слоя приложения на 3 большие группы:
1. Сторы, хранилища данных нужных для ui и не являющихся частью доменной модели. Тупой пример текущий активный элемент
2. Сервисы, все то что инкапсулирует в себе определенную логику обработки каких-то бизнесовых задач. Ну например менеджер, управляющий какой-нибудь инвалидацией определенных данных
3. Вью модели, эта штука по сути просто логика, которую мы обычно пишет в компонентах, только вынесенная в классы.
Конечно там есть куча сущностей, обеспечивающих работу этого комбайна, инфраструктура по сути, di, роутинг, апи клиенты, фабрики, кэши и т.д.
По слою домена, это сервисы, агрегаты, сущности или в рамках агрегатов или отдельно.
Но насколько знаю, то что я называю слоем приложения, это слой UI, а то что я назыаю пограничным слоем, это как раз таки слой приложения, но это дефиниции, не думаю, что это на что-то влияет. Соответсвенно такая связь позволяет нам гибко оперировать сущностями, а также самое крутое на мой взгляд, переиспользовать бизнес логику.
cmyser
Каждая новая книга по DDD начинается со слов автора
Единственное место где реально решена эта проблема это $mol
Там используется система FNQ ( уникальное имя класса = месту в файловом дереве )
Например класс my_super_app будет лежать в my/super/app.ts
А если не будет, то код не запустится, получим ошибку компиляции, вот где реальное решение проблемы