Файловая структура проектов в компании
Рассказ о нашей архитектуре FBCA (Feature-Based Clean Architecture) — как устроена структура модулей, чем отличаются слои, слайсы и сегменты, куда класть код и зачем. Почему важно соблюдать модульность, как устроены зависимости и зачем нужны conventions. Полезно для новичков и опытных разработчиков.
Файловая структура WEB-продуктов YCLIENTS представляет из себя самостоятельно выработанное решение на базе FSD и называется FBCA
(Feature-Based Clean Architecture).
В этой статье ты сможешь узнать, что такое слой / слайс / сегмент, какими они бывают, и в какую же папку нужно положить свой код.
Слои
Верхнеуровнево модуль разделен на слои:
app— инфраструктурный код для инициализации приложенияpages— компоненты страниц (точки входа для роутера). Могут содержать бизнес-логику, но желательна декомпозиция в слои views / use-casesviews— содержит компоненты и уникальную для них бизнес-логику, как самостоятельный сценарий, который может быть переиспользован в модуле — табы, модалки/диалоги, виджеты, обертки, функциональные области и т. д.use-cases— общая переиспользуемая бизнес-логика между несколькими viewsshared— содержит общие элементы оторванные от бизнес-логики
Связь между слоями организована по принципу луковичной структуры — модули на одном слое могут знать о модулях со слоев строго ниже. Сами связи между слоями выражается это в следующем виде: app → pages → views→ use-cases → shared
Слайсы
Каждый слой состоит из слайсов, а они в свою очередь из сегментов. При этом слои App и Shared, в отличие от других слоев, не имеют слайсов и состоят из сегментов напрямую.
Слайсы делят слой по предметной области. Вы можете называть ваши слайсы как угодно, и создавать их сколько угодно. Слайсы группируют тесно связанный по смыслу код. К примеру summary-card, product-default.
Сегменты
Слайсы, а также слои App и Shared, состоят из сегментов, которые группируют код по его назначению.
Всегда имеют плоскую структуру:
configs/— конфигурации для модуляroutes/— статичные массивы маршрутовplugins/— плагины модуляclasses/— реализации классов и (опционально) создание и ре-экспорт их глобальных инстансовconfigs/— конфигурация модуляconstants/— константы (объекты, массивы, примитивы)media/— фото, гифки, видеоstyles/— переменные, миксины, css-классыtypes/— перечислители, типы, интерфейсыcomposables/— композабл функцииcomponents/— UI-компонентыstorages/— работа сLS,SS,CS,IndexedDbstore/— модули стораworkers/— логика создания. регистрации и коммуникации с воркером.api/— работа с API: вызовы fetch и получение DTO бэкенда без их преобразований в модели компонентов.media/— фото, гифки, видео не относящиеся к конкретному компоненту (например, иконка стрелки)styles/— глобальные переменные, миксины и стили (например, reset стили)utils/— недоменные утилиты (функции работы с внешними API, которые не содержат бизнес контекст). Также именно здесь содержится файлtracks.ts, если он нужен, и реализует сбор продуктовой аналитики в рамках слайса
Верхнеуровневый пример файловой структуры
Общее описание слоев
marketplace/
├── app/
│ ├── configs/
│ ├── routes/
│ ├── plugins/
│ ├── router.ts
│ ├── store.ts
│ └── app.vue
├── pages/
│ └── catalog/
│ ├── classes/
│ ├── constants/
│ ├── media/
│ ├── styles/
│ ├── types/
│ ├── composables/
│ ├── utils/
│ ├── components/
│ ├── storages/
│ ├── store/
│ ├── workers/
│ ├── tracks/
│ └── index.ts
├── views/
│ └── catalog-filter/
│ ├── classes/
│ ├── constants/
│ ├── media/
│ ├── styles/
│ ├── types/
│ ├── composables/
│ ├── utils/
│ ├── components/
│ ├── storages/
│ ├── store/
│ ├── workers/
│ ├── tracks/
│ └── index.ts
│ └── product-default/
├── use-cases/
│ └── catalog-search/
│ ├── classes/
│ ├── constants/
│ ├── media/
│ ├── styles/
│ ├── types/
│ ├── composables/
│ ├── utils/
│ ├── storages/
│ ├── store/
│ ├── workers/
│ ├── tracks/
│ └── index.ts
│ └── index.ts
├── shared/
│ ├── api/
│ ├── media/
│ ├── styles/
│ └── utils/
app/ — инфраструктурный код для инициализации приложения
Содержит сегменты app/ слоя и доп. файлы:
configs/routes/plugins/router.tsstore.tsapp.vue
pages/ — компоненты страниц (точки входа для роутера)
Компоненты страниц (точки входа для роутера). Могут содержать бизнес-логику, но желательна декомпозиция в слои views / use-cases. index.ts файл отдает только один компонент (точку входа). Также index.ts может отдавать при необходимости props и emits.
Состоит из слайсов, которые делятся на сегменты:
classes/constants/media/styles/types/composabled/utils/components/storages/store/workers/index.ts— ре-экспорт ui-компонента страницы и типов для работы с ним
views/ — компоненты и уникальная для них бизнес-логику
Содержит компоненты и уникальную для них бизнес-логику, как самостоятельный сценарий, который может быть переиспользован в модуле — табы, модалки/диалоги, виджеты, обертки, функциональные области и т. д. Обязательно имеют ui-компонент. index.ts файл отдает только один компонент и обязательно экспортирует props и emits этой точки входа.
Состоит из слайсов, которые делятся на сегменты:
classes/constants/media/styles/types/composables/utils/components/— ограничение на кол-во компонентов — 10 штstorages/store/workers/index.ts— ре-экспорты для ui-компонента и типов для работы с ним
use-cases/ — общая переиспользуемая бизнес-логика между несколькими views
Общая переиспользуемая бизнес-логика между несколькими views. Содержит только функциональную часть — ui полностью отсутствует. При необходимости поглащает потенциально объединяемые элементы. Состоит из слайсов и index.ts файла, который аккумулирует в себе все структурные единицы из index.ts файлов дочерних модулей.
Состоит из слайсов, которые делятся на сегменты:
classes/constants/media/styles/types/models/composables/utils/storages/store/workers/index.ts— ре-экспорты для фичи
shared/ — общие элементы оторванные от бизнес-логики
Содержит общие элементы оторванные от бизнес-логики. Функционал выносится сюда если используется более чем в 1 фиче и не содержит бизнес логику. Этот флоу работает только в одну сторону, к примеру — при удалении одной из двух фичей, которая использует этот функционал, обратно в фичу он уже не попадает. Считаем, что уже был
прецедент на переиспользование логики.
Состоит из сегментов:
api/media/styles/utils/
Подход к написанию
Технически можно описать всю логику непосредственно в page или views. Однако такой подход может привести к нарушению принципа разделения ответственности и ухудшить масштабируемость. Строго рекомендуется выделять повторно используемые компоненты и логику в отдельные слои для лучшей модульности и поддержки архитектуры.
Вопросы и ответы
Чем слой shared отличается от use-cases? Зачем вообще нужен shared слой?
Разделение на слои use-cases и shared продиктовано необходимостью разграничения кода по его назначению и уровню привязки к бизнес-домену. use-cases служит для объединения и переиспользования доменного функционала, который, несмотря на общность, остаётся привязанным к предметной области. shared же хранит универсальные, инфраструктурные элементы, не связанные с бизнес-логикой, что позволяет сохранять чистоту и модульность архитектуры. К тому же, наличие shared явно показывает нам, что существуют зависимости от внешних библиотек, API, утилит или стилей.
Почему API в shared, если shared не должен содержать бизнес-логики?
Слой API представляет собой изолированный инфраструктурный уровень, который реализует универсальные механизмы связи с сервером и не содержит бизнес-правил или логики, характерных для конкретных доменов. Оно предоставляет лишь общие инструменты, которые могут быть использованы в любых фичах и в любых модулях.
Почему бы не иметь один общий слой «features» вместо views и use-cases?
Разделение на views и use-cases было произведено по нескольким ключевым причинам, связанным с соблюдением принципов DI, сохранением структурности и единообразия архитектуры, а также правильным распределением ответственности между слоями.
Рассмотрим их более детально:
1. Соблюдение принципов DI и инкапсуляции бизнес-логики:
- Мы разрешаем экспортировать из фичи только UI точку входа вместе с её пропсами и эмитами. Это помогает избежать неявного использования внутренних зависимостей и бизнес-логики напрямую, что соответствует принципам DI.
- Бизнес-логика, которая может использоваться несколькими фичами, выносится в отдельный слой (
use-cases). Это обеспечивает строгую инкапсуляцию, так что UI-компоненты (изui) взаимодействуют с бизнес-логикой через чётко определённые интерфейсы.
2. Сохранение структурности и единообразия:
- Если бы бизнес-логику вынесли в отдельную фичу, структура проекта стала бы неоднородной: в одних местах были бы фичи с UI, а в других — без него. Такое расхождение усложнило бы навигацию и понимание архитектуры.
3. Правильное распределение ответственности между слоями:
viewsотвечает за визуальное представление и взаимодействие с пользователем, экспортируя только публичный интерфейс (UI точку входа).use-casesсодержит доменную (бизнес) логику, которая не должна смешиваться с представлением и при этом может использоваться несколькими фичами.sharedпредназначен для универсальных утилит, зависимостей от внешних библиотек, стилей и прочего, что не относится к бизнес-логике, поэтому переносить туда специфичную доменную логику нельзя.
Почему внутри views можно экспортировать только один компонент и типы для работы с ним?
Такой подход побуждает писать самостоятельные и переиспользуемые компоненты. Когда каждая фича имеет единую публичную точку входа с четко определёнными типами, разработчики вынуждены думать о структуре и разделении ответственности, что помогает избежать превращения фичи в «помойку». Это способствует созданию модульных, тестируемых и легко поддерживаемых компонентов, которые можно повторно использовать в разных частях модуля.
Могу ли я весь код описать в page, а во views / use-cases выносить только переиспользуемую логику?
Да, технически можно описать всю логику непосредственно в page. Однако такой подход может привести к нарушению принципа разделения ответственности и ухудшить масштабируемость. Строго рекомендуется выделять повторно используемые компоненты и логику в отдельные слои для лучшей модульности и поддержки архитектуры
Почему я могу брать всё содержимое из use-cases, это же нарушает DI в том кейсе, что я знаю про устройство абстракции?
Если use-cases экспортирует только четко определённые публичные интерфейсы и абстракции, которые инкапсулируют всю внутреннюю бизнес-логику, то использование этих интерфейсов не нарушает DI. То есть, ключевым моментом является то, как организован экспорт функциональности: доступ должен быть возможен только через публичный API, а не через прямой доступ ко всем внутренним деталям.
Почему не описана структура API и верхнеуровневая структура проекта?
Из-за большого объема данных в проектах верхнеуровневая структура и API-слой будут описаны в отдельных задачах. Такой подход позволит постепенно привести приложения к единообразию. Мы решили начать с низкоуровневых модулей, чтобы оставить пространство для экспериментов и доработок. По мере прояснения архитектурных аспектов мы сможем гибко адаптировать и расширять схему.
Обязательно ли иметь все указанные виды сегментов для слоя в page / views / use-cases?
Нет, не обязательно. Эти сегменты являются рекомендациями для разделения ответственности и обеспечения масштабируемости. Если какая-либо функциональность не используется или не нужна для определённого слоя, можно опустить соответствующие сегменты, главное — сохранять единообразие и ясное разделение обязанностей там, где это имеет смысл.
Зачем нам иметь так много разных инфраструктурных сегментов (contstants, storages, types…) не легче ли все объеденить в models?
Если объединить всё в одну папку, получится «свалка», где трудно быстро найти нужное, а также нарушится принцип разделения ответственности. При явном разделении каждая категория кода становится более самостоятельной.
Куда размещать логику perfomance-логов, треков аналитики и прочих вспомогательных сервисов?
Доменные классы/функции, требуемые внутри конкретного доменного компонента/функции, размещаются сегментах семантически: если требуется класс — размещаем его в classes, если функция — в utils, если функция с реактивностью и/или методами жизненного цикла компонентов фрейвморка — composables. При этом непосредственные запросы fetch и классы/функции для работы со вспомогательными сервисами должны располагаться в API-слое отдельно.