Файловая структура проектов в компании

Рассказ о нашей архитектуре FBCA (Feature-Based Clean Architecture) — как устроена структура модулей, чем отличаются слои, слайсы и сегменты, куда класть код и зачем. Почему важно соблюдать модульность, как устроены зависимости и зачем нужны conventions. Полезно для новичков и опытных разработчиков.

Файловая структура проектов в компании

Файловая структура WEB-продуктов YCLIENTS представляет из себя самостоятельно выработанное решение на базе FSD и называется FBCA
(Feature-Based Clean Architecture).

В этой статье ты сможешь узнать, что такое слой / слайс / сегмент, какими они бывают, и в какую же папку нужно положить свой код.

Слои

Верхнеуровнево модуль разделен на слои:

  • app — инфраструктурный код для инициализации приложения
  • pages — компоненты страниц (точки входа для роутера). Могут содержать бизнес-логику, но желательна декомпозиция в слои views / use-cases
  • views — содержит компоненты и уникальную для них бизнес-логику, как самостоятельный сценарий, который может быть переиспользован в модуле — табы, модалки/диалоги, виджеты, обертки, функциональные области и т. д.
  • use-cases — общая переиспользуемая бизнес-логика между несколькими views
  • shared — содержит общие элементы оторванные от бизнес-логики

Связь между слоями организована по принципу луковичной структуры — модули на одном слое могут знать о модулях со слоев строго ниже. Сами связи между слоями выражается это в следующем виде: app → pages → views→ use-cases → shared

Слайсы

Каждый слой состоит из слайсов, а они в свою очередь из сегментов. При этом слои App и Shared, в отличие от других слоев, не имеют слайсов и состоят из сегментов напрямую.

Слайсы делят слой по предметной области. Вы можете называть ваши слайсы как угодно, и создавать их сколько угодно. Слайсы группируют тесно связанный по смыслу код. К примеру summary-cardproduct-default.

Сегменты

Слайсы, а также слои App и Shared, состоят из сегментов, которые группируют код по его назначению.

Всегда имеют плоскую структуру:

  • configs/ — конфигурации для модуля
  • routes/ — статичные массивы маршрутов
  • plugins/ — плагины модуля
  • classes/ — реализации классов и (опционально) создание и ре-экспорт их глобальных инстансов
  • configs/ — конфигурация модуля
  • constants/ — константы (объекты, массивы, примитивы)
  • media/ — фото, гифки, видео
  • styles/ — переменные, миксины, css-классы
  • types/ — перечислители, типы, интерфейсы
  • composables/ — композабл функции
  • components/ — UI-компоненты
  • storages/ — работа с LS, SS, CS, IndexedDb
  • store/ — модули стора
  • 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.ts
  • store.ts
  • app.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-слое отдельно.

Подписаться на новые выпуски блога

Не пропустите последние обновления.
i.ivanov@yandex.ru
Подписаться