From 7be201c1eb33b33d782ca9bf8062133dbfe192ae Mon Sep 17 00:00:00 2001 From: "S.Gromov" Date: Mon, 11 May 2026 22:16:42 +0300 Subject: [PATCH] =?UTF-8?q?docs:=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20=D1=80=D0=B0=D0=B7=D0=B4=D0=B5=D0=BB=20=D0=B0?= =?UTF-8?q?=D1=80=D1=85=D0=B8=D1=82=D0=B5=D0=BA=D1=82=D1=83=D1=80=D1=8B=20?= =?UTF-8?q?SLM?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - добавлен отдельный раздел архитектуры в сайдбаре - подключены спецификация SLM и React-примеры - удалён дубль архитектуры из базовых правил --- .vitepress/config.ts | 34 ++- docs/docs/slm-design/VERSION | 2 + docs/docs/slm-design/architecture/index.md | 114 ++++++++ docs/docs/slm-design/architecture/layers.md | 254 ++++++++++++++++++ docs/docs/slm-design/architecture/modules.md | 213 +++++++++++++++ docs/docs/slm-design/architecture/monorepo.md | 235 ++++++++++++++++ docs/docs/slm-design/architecture/segments.md | 181 +++++++++++++ .../examples/react/composition-provider.md | 249 +++++++++++++++++ .../examples/react/factory-composition.md | 52 ++++ .../docs/slm-design/examples/react/factory.md | 114 ++++++++ 10 files changed, 1438 insertions(+), 10 deletions(-) create mode 100644 docs/docs/slm-design/VERSION create mode 100644 docs/docs/slm-design/architecture/index.md create mode 100644 docs/docs/slm-design/architecture/layers.md create mode 100644 docs/docs/slm-design/architecture/modules.md create mode 100644 docs/docs/slm-design/architecture/monorepo.md create mode 100644 docs/docs/slm-design/architecture/segments.md create mode 100644 docs/docs/slm-design/examples/react/composition-provider.md create mode 100644 docs/docs/slm-design/examples/react/factory-composition.md create mode 100644 docs/docs/slm-design/examples/react/factory.md diff --git a/.vitepress/config.ts b/.vitepress/config.ts index 2f11fbb..fbef1bb 100644 --- a/.vitepress/config.ts +++ b/.vitepress/config.ts @@ -9,21 +9,35 @@ const sidebar = [ text: 'Подсказки', link: '/docs/workflow', }, + { + text: 'Архитектура', + items: [ + { + text: 'Спецификация', + items: [ + { text: 'Обзор', link: '/docs/slm-design/architecture/' }, + { text: 'Слои', link: '/docs/slm-design/architecture/layers' }, + { text: 'Модули', link: '/docs/slm-design/architecture/modules' }, + { text: 'Сегменты', link: '/docs/slm-design/architecture/segments' }, + { text: 'Монорепозитории', link: '/docs/slm-design/architecture/monorepo' }, + ], + }, + { + text: 'Примеры', + collapsed: true, + items: [ + { text: 'Создание фабрики', link: '/docs/slm-design/examples/react/factory' }, + { text: 'Композиция фабрик', link: '/docs/slm-design/examples/react/factory-composition' }, + { text: 'Композиция через Provider', link: '/docs/slm-design/examples/react/composition-provider' }, + ], + }, + ], + }, { text: 'Базовые правила', items: [ { text: 'Технологии и библиотеки', link: '/docs/basics/tech-stack' }, { text: 'Именование', link: '/docs/basics/naming' }, - { - text: 'Архитектура', - collapsed: true, - items: [ - { text: 'Обзор', link: '/docs/basics/architecture/' }, - { text: 'Слои', link: '/docs/basics/architecture/layers' }, - { text: 'Модули', link: '/docs/basics/architecture/modules' }, - { text: 'Сегменты', link: '/docs/basics/architecture/segments' }, - ], - }, { text: 'Стиль кода', link: '/docs/basics/code-style' }, { text: 'Документирование', link: '/docs/basics/documentation' }, { text: 'Типизация', link: '/docs/basics/typing' }, diff --git a/docs/docs/slm-design/VERSION b/docs/docs/slm-design/VERSION new file mode 100644 index 0000000..d0b7f5d --- /dev/null +++ b/docs/docs/slm-design/VERSION @@ -0,0 +1,2 @@ +v0.1.5 +2026-05-11T18:29:26.821Z diff --git a/docs/docs/slm-design/architecture/index.md b/docs/docs/slm-design/architecture/index.md new file mode 100644 index 0000000..51a7f16 --- /dev/null +++ b/docs/docs/slm-design/architecture/index.md @@ -0,0 +1,114 @@ +--- +title: SLM Design +description: "Scoped Layered Module Design — модульная архитектура фронтенд-приложений. Код организован по слоям ответственности, а модуль содержит всё, что ему нужно: компоненты, хуки, сторы, типы, стили." +--- + +# SLM Design +Scoped Layered Module Design — модульная архитектура фронтенд-приложений. Код организован по слоям ответственности, а модуль содержит всё, что ему нужно: компоненты, хуки, сторы, типы, стили. + +## Разделы спецификации + +Спецификация SLM Design состоит из нескольких связанных разделов. Этот обзор даёт общий контекст, а детальные правила описаны дальше: + +- [Слои](./layers.md) — уровни организации `src/`, направление зависимостей и зона ответственности каждого слоя. +- [Модули](./modules.md) — границы ответственности, публичный API, типы модулей и отличие модуля от компонента. +- [Сегменты](./segments.md) — внутренние папки модуля (`ui/`, `parts/`, `hooks/`, `types/` и другие) и правила размещения файлов. +- [Монорепозитории](./monorepo.md) — применение SLM в `apps/` и `packages/`, правила выноса общих слоёв и ограничения для business. + +Рекомендуемый порядок чтения: обзор → слои → модули → сегменты → монорепозитории. + +## Преимущества + +### Вертикальная организация домена + +Бизнес-домен не разбивается по техническим слоям — сценарии, сущности, типы и UI живут в одном модуле. Это сокращает время навигации и упрощает сопровождение: все изменения домена локализованы. + +### Dependency Injection без фреймворков + +Cross-domain зависимости в бизнес-слое реализуются через фабрики — модуль декларирует что ему нужно, а точка использования предоставляет зависимости. Домены изолированы без DI-контейнеров, провайдеров и шин событий. + +### Разделение ответственности без перегрузки слоёв + +Сервисы приложения (`infra/`), UI-кит (`ui/`) и общие ресурсы (`shared/`) — три разных слоя с разной природой. Ни один слой не превращается в свалку разнородного кода. + +### Горизонтальная инкапсуляция + +Вложенные модули (`parts/`) и направление зависимостей позволяют нескольким разработчикам работать над одной областью приложения параллельно, не затрагивая код друг друга. + +### Колокация по умолчанию + +Код начинает жизнь рядом с местом использования и поднимается в общие слои только при реальной потребности. Глобальные слои не засоряются преждевременными абстракциями. + +### Явное разделение каркаса и контента + +Каркас группы маршрутов (`layouts/`) и контент конкретной страницы (`screens/`) — независимые слои с собственной ответственностью. + +### Масштабирование через группировку + +При росте проекта слои не теряют структуру — модули группируются по естественным признакам: бизнес-домены по субдоменам, страницы по разделам, UI-компоненты по уровню абстракции (примитивы и композиции). + +### Адаптация к монорепозиториям + +SLM применяется внутри каждого приложения, а `packages/*` используются только для общего кода из слоёв `ui`, `infra` и `shared`. Бизнес-домены остаются внутри приложений, чтобы не размывать продуктовые границы. + +## Происхождение + +SLM Design вырос на основе: + +- **Feature-Sliced Design** — слоистая структура, публичный API модуля, направление зависимостей +- **Vertical Slice Architecture** — модуль как вертикальный срез, содержащий всё необходимое +- **Screaming Architecture** — структура проекта «кричит» о назначении: открыл `business/auth` — видишь авторизацию +- **Colocation Principle** — код живёт рядом с местом использования + +## Пример структуры проекта + +```text +src/ +├── app/ +│ +├── layouts/ +│ ├── main/ +│ └── dashboard/ +│ +├── screens/ +│ ├── home/ +│ ├── products/ +│ ├── product-detail/ +│ └── about/ +│ +├── widgets/ +│ ├── page-heading/ +│ ├── hero-section/ +│ └── promo-banner/ +│ +├── business/ +│ ├── auth/ +│ ├── catalog/ +│ ├── orders/ +│ └── chat/ +│ +├── infra/ +│ ├── theme/ +│ ├── i18n/ +│ ├── backend-api/ +│ └── logger/ +│ +├── ui/ +│ ├── button/ +│ ├── input/ +│ ├── modal/ +│ ├── toast/ +│ └── dropdown/ +│ +└── shared/ + ├── lib/ + ├── types/ + └── styles/ +``` + +## Принципы + +- **Домен — единое целое.** Всё, что относится к домену, живёт в одном модуле. +- **Колокация.** Код рождается рядом с местом использования и поднимается только при необходимости. +- **Зависимости однонаправлены.** Импорты только сверху вниз, только через публичный API. +- **Архитектура — каркас, не клетка.** Правила фиксируют направление зависимостей и структуру модуля, остальное определяет команда. diff --git a/docs/docs/slm-design/architecture/layers.md b/docs/docs/slm-design/architecture/layers.md new file mode 100644 index 0000000..335c978 --- /dev/null +++ b/docs/docs/slm-design/architecture/layers.md @@ -0,0 +1,254 @@ +--- +title: Слои +description: "Раздел описывает слои SLM: что такое слой, какие бывают, как между ними направлены зависимости и какие правила действуют на каждом." +--- + +# Слои + +Раздел описывает слои SLM: что такое слой, какие бывают, как между ними направлены зависимости и какие правила действуют на каждом. + +## Определение + +**Слой — уровень организации кода внутри `src/`. Каждый слой отвечает за свою область (каркас страницы, бизнес-логика, UI-кит) и задаёт правила для кода внутри: направление импортов, именование, допустимые связи между модулями.** + +## Группы слоёв + +Слои делятся на три группы: + +| Группа | Слои | Описание | +|--------|------|----------| +| Композиция | `app`, `layouts`, `screens`, `widgets` | Собирают интерфейс из готовых блоков: маршруты, каркасы, страницы | +| Ядро | `business`, `infra`, `ui` | Реализация продукта: бизнес-домены, техсервисы, UI-кит | +| Фундамент | `shared` | Общие ресурсы: утилиты, хелперы, стили, конфиги | + +## Направление зависимостей + +Любой импорт между модулями — только через публичный API. + +``` +app → [ layouts | screens ] → widgets → business → infra → ui → shared +``` + +- `layouts` и `screens` — параллельные слои, не импортируют друг друга +- Модули одного слоя в группе «Композиция» изолированы друг от друга +- Модули одного слоя `infra` и `ui` могут импортировать друг друга через публичный API +- Модули `business` — cross-domain зависимости по коду через фабрику, `import type` напрямую +- Импорт типов (`import type`) в «Ядре» разрешён в обоих направлениях + + +## Слой App + +Точка входа приложения. Отвечает за запуск, роутинг и композицию маршрутов из layout и screen. + +В отличие от остальных слоёв, `app/` не содержит модулей SLM. Здесь живут только инфраструктурные файлы, которые не могут быть никаким другим слоем: файлы фреймворка роутинга, точка запуска и код инициализации. + +### Требования + +- Не содержит модулей SLM — только файлы фреймворка, роутинг, инициализация +- Содержит: файлы маршрутов, bootstrap, обработку ошибок верхнего уровня (404, error boundary), подключение глобальных стилей и ассетов +- Провайдеры и гарды — только подключает готовые из нижних слоёв, не реализует +- Не содержит бизнес-логику, UI-компоненты, хуки, сторы, сервисы +- Никем не импортируется + +## Слой Layouts + +Каркас страницы: общие элементы, одинаковые для группы маршрутов (header, footer, sidebar). + +```text +src/layouts/ +├── main/ +├── dashboard/ +└── auth/ +``` + +### Требования + +- Содержит только модули +- Не содержит бизнес-логику +- Контекстно-зависимые блоки принимает через пропсы от `app`, не импортирует напрямую + +## Слой Screens + +Контент конкретной страницы: собирает её из модулей нижних слоёв. + +```text +src/screens/ +├── home/ +├── products/ +├── product-detail/ +├── about/ +└── contacts/ +``` + +Когда количество страниц затрудняет навигацию — вводится группировка по разделам. Группа — папка для организации, не модуль (без `index.ts`). + +```text +src/screens/ +├── shop/ +│ ├── home/ +│ ├── products/ +│ ├── product-detail/ +│ └── cart/ +├── account/ +│ ├── profile/ +│ ├── settings/ +│ └── order-history/ +└── info/ + ├── about/ + ├── contacts/ + └── faq/ +``` + +### Требования + +- Содержит только модули +- Не содержит бизнес-логику +- Локальные одноразовые секции живут внутри screen-модуля, не выносятся в `widgets`/`business` + +## Слой Widgets + +Составной блок интерфейса, который компонует модули ядра, но не принадлежит конкретному бизнес-домену. Widget появляется когда блок используется в нескольких screens или layouts. + +Если блок принадлежит домену — он живёт в `business/{area}/`, даже если переиспользуется. Если блок нужен только в одном месте — это `screens/{name}/parts/` или `layouts/{name}/parts/`, а не widget. + +```text +src/widgets/ +├── page-heading/ +├── hero-section/ +├── onboarding-checklist/ +├── promo-banner/ +└── error-boundary/ +``` + +### Требования + +- Не принадлежит конкретному бизнес-домену. Если блок доменный — он живёт в `business/` +- Используется в нескольких screens или layouts + +## Слой Business + +Бизнес-домены приложения: auth, catalog, orders, checkout, chat. Каждый домен — отдельный модуль со своими типами, логикой, UI и сервисами. + +Слой входит в группу «Ядро». Импортирует `infra/`, `ui/`, `shared/`. Каждый бизнес-модуль создаёт публичный runtime API через фабрику в корне. Cross-domain зависимости: runtime — через аргументы фабрики, типы — напрямую через `import type`. + +Business объединяет то, что в FSD разделено на `features` и `entities`: пользовательские сценарии и бизнес-сущности живут вместе, внутри одного домена. Внутри домена сегменты разделяют ответственность: `types/` — доменная модель, `hooks/` и `services/` — сценарии и логика, `mappers/` — трансформация данных, `parts/` — составные блоки. + +```text +src/business/ +├── auth/ +├── catalog/ +├── orders/ +├── checkout/ +└── chat/ +``` + +Когда количество доменов затрудняет навигацию — вводится группировка по субдоменам. Группа — папка для организации, не модуль (без `index.ts`). + +```text +src/business/ +├── commerce/ +│ ├── catalog/ +│ ├── cart/ +│ ├── orders/ +│ └── checkout/ +└── communication/ + ├── chat/ + └── notifications/ +``` + +### Требования + +- Один модуль = один бизнес-домен +- Циклические зависимости между доменами запрещены +- Публичный runtime API — через фабрику в корне модуля (`{name}.factory.ts`). `index.ts` экспортирует только фабрику и type-only экспорты +- Импорт runtime-кода между доменами — через фабрику. `import type` — напрямую +- Доменные типы (`User`, `Product`) живут здесь, не в `shared/` + +## Слой infra + +Техсервисы приложения: theme, i18n, API-адаптеры, logger, realtime. Каждый сервис — отдельный модуль. + +Слой входит в группу «Ядро». Импортирует `infra/`, `ui/`, `shared/`. + +Отличие от `shared/`: infra — инфраструктура приложения (сервисы, темы, адаптеры к API), `shared/` — общие ресурсы (утилиты, хелперы, стили, конфиги). + +```text +src/infra/ +├── theme/ +├── i18n/ +├── backend-api/ +├── maps-api/ +├── logger/ +├── feature-flags/ +└── realtime/ +``` + +### Требования + +- Один модуль = один техсервис +- Импортирует `infra/`, `ui/`, `shared/` + +## Слой UI + +UI-кит без бизнес-логики: button, carousel, toast, modal. + +Слой входит в группу «Ядро». Импортирует `ui/` и `shared/`. + +Компоненты строятся друг на друге: `button` использует `icon`, `carousel` использует `button`. + +```text +src/ui/ +├── button/ +├── input/ +├── icon/ +├── carousel/ +├── modal/ +├── toast/ +├── dropdown/ +├── tabs/ +└── tooltip/ +``` + +Когда количество компонентов затрудняет навигацию — вводится группировка на примитивы и композиции. Примитивы (`button`, `icon`, `input`) не импортируют композиции. Композиции (`carousel`, `modal`, `dropdown`) строятся на примитивах. + +```text +src/ui/ +├── primitives/ +│ ├── button/ +│ ├── input/ +│ ├── icon/ +│ └── badge/ +└── composites/ + ├── carousel/ + ├── modal/ + ├── dropdown/ + ├── tabs/ + └── tooltip/ +``` + +### Требования + +- Не содержит бизнес-логику +- Импортирует только `ui/` и `shared/` + +## Слой Shared + +Общие ресурсы: утилиты, хелперы, стили, конфиги. Не знает о бизнес-домене. + +Слой входит в группу «Фундамент» — ни о ком не знает, никого не импортирует. + +Отличие от `infra/`: infra — инфраструктура приложения (сервисы, темы, адаптеры к API), `shared/` — общие ресурсы (утилиты, хелперы, стили, конфиги). + +Отличие от `ui/`: UI-компоненты (button, carousel, modal) живут в слое `ui/`, а не здесь. + +```text +src/shared/ +├── lib/ +├── types/ +├── styles/ +└── sprites/ +``` + +### Требования + +- Не имеет runtime-состояния diff --git a/docs/docs/slm-design/architecture/modules.md b/docs/docs/slm-design/architecture/modules.md new file mode 100644 index 0000000..2b318c4 --- /dev/null +++ b/docs/docs/slm-design/architecture/modules.md @@ -0,0 +1,213 @@ +--- +title: Модули +description: "Раздел описывает модуль как границу ответственности в SLM: что считается модулем, что такое компонент внутри модуля и как модуль взаимодействует с остальным кодом." +--- + +# Модули + +Раздел описывает модуль как границу ответственности в SLM: что считается модулем, что такое компонент внутри модуля и как модуль взаимодействует с остальным кодом. + +## Определение + +**Модуль — минимальная архитектурная единица SLM. Он живёт на одном из слоёв, владеет конкретной областью ответственности и предоставляет наружу только публичный API.** + +Модуль может содержать всё, что нужно этой области: компоненты, вложенные модули, хуки, сторы, сервисы, типы, стили, конфиги и утилиты. Набор сегментов не фиксирован — модуль включает только то, что реально нужно. + +Модуль не обязан быть UI-блоком. Это может быть страница, виджет, бизнес-домен, инфраструктурный сервис или UI-kit сущность. + +Главная граница модуля — не папка, а ответственность. + +## Компонент + +**Компонент — презентационная единица модуля, которая находится только в `ui/` своего родительского модуля и отвечает за отображение части интерфейса.** + +Компонент не является архитектурной единицей: он не владеет сценарием, зависимостями, данными или внутренней структурой. Он работает только внутри границы родительского модуля. + +> Компонент отображает. Модуль организует. + +Компонент не может: + +- Импортировать код проекта за пределами родительского модуля. +- Владеть архитектурными зависимостями. +- Содержать любые компоненты. +- Содержать любые модули. +- Делать внешние запросы. +- Самостоятельно получать данные. +- Выбирать источник данных. +- Композировать данные. +- Вызывать сценарные хуки. +- Оркестрировать сценарий. +- Композировать модули. +- Решать, как устроен процесс. +- Содержать бизнес-логику. +- Содержать сценарную логику. + +Если компоненту требуется что-то из этого списка, он перестаёт быть компонентом и должен быть оформлен как модуль. + +```text +auth/ +├── ui/ +│ └── logout-button/ +│ ├── logout-button.tsx +│ ├── styles/ +│ │ └── logout-button.module.css +│ ├── types/ +│ │ └── logout-button-props.type.ts +│ └── index.ts +└── index.ts +``` + +## Что считается модулем + +Модулем считается папка, которая представляет самостоятельную область ответственности и имеет публичную границу. + +Примеры модулей: + +- `screens/home/` — модуль страницы. +- `widgets/page-heading/` — модуль виджета. +- `business/auth/` — модуль бизнес-домена. +- `infra/theme/` — модуль инфраструктурного сервиса. +- `ui/button/` — модуль UI-kit сущности. +- `screens/home/parts/hero-section/` — вложенный модуль страницы. + +Не считаются модулями: + +- `ui/`, `parts/`, `hooks/`, `types/`, `styles/`, `config/` — это сегменты. +- `screens/shop/`, `business/commerce/` — это группы, если в них нет `index.ts`. +- `screens/home/ui/user-card/` — это компонент, если он находится в `ui/` и соблюдает ограничения компонента. + +## Типы модулей + +Тип модуля определяет обязательный корневой файл и стартовую структуру. + +### UI-модуль + +Модуль строится вокруг основного UI-компонента и обязан иметь основной `.tsx` файл в корне: + +```text +header/ +├── header.tsx +└── index.ts +``` + +`ui/` внутри такого модуля используется только для компонентов, которые помогают корневому `.tsx` файлу. + +### Бизнес-модуль + +Бизнес-модуль — модуль, который строится вокруг публичного runtime API. + +Бизнес-модуль обязан иметь фабрику в корне: + +```text +auth/ +├── auth.factory.ts +├── index.ts +└── types/ +``` + +Фабрика возвращает публичный runtime API модуля. + +### Инфраструктурный модуль + +Инфраструктурный модуль — модуль, который строится вокруг технического сервиса или интеграции. + +Инфраструктурный модуль не обязан иметь фиксированный корневой файл. Его структура определяется природой сервиса. + +```text +theme/ +├── index.ts +├── config/ +├── hooks/ +├── styles/ +└── ui/ +``` + +```text +backend-api/ +├── backend-api.client.ts +├── config/ +├── types/ +└── index.ts +``` + +## Структура + +Модуль состоит из сегментов. Ни один сегмент не обязателен — модуль включает только те части, которые нужны его ответственности. + +```text +{module-name}/ +├── {module-name}.factory.ts # фабрика (для business-модулей) +├── {module-name}.tsx # корневой файл модуля (опционален) +├── ui/ # компоненты модуля +├── parts/ # вложенные модули +├── hooks/ # хуки +├── stores/ # сторы состояния +├── services/ # внешние источники данных +├── mappers/ # трансформация данных между форматами +├── types/ # типы +├── styles/ # стили +├── lib/ # утилиты модуля +├── config/ # константы и конфигурация +└── index.ts # публичный API +``` + +Подробное описание сегментов — в разделе [Сегменты](./segments.md). + +## Публичный API + +Внешний код импортирует модуль только через публичный API. + +```ts +// Хорошо +import { customerFactory } from '@/business/customer' +import type { Customer } from '@/business/customer' +``` + +```ts +// Плохо +import { validateToken } from '@/business/auth/lib/tokens' +``` + +`index.ts` модуля не обязан экспортировать всё содержимое. Он экспортирует только то, что действительно нужно снаружи. + +Внутренние сегменты модуля остаются деталями реализации. + +Business-модуль экспортирует из `index.ts` только фабрику и type-only экспорты. Хуки, компоненты, сервисы, мапперы и утилиты напрямую из `index.ts` не экспортируются — они доступны через API, который возвращает фабрика. + +```ts +// business/customer/index.ts +export { customerFactory } from './customer.factory' + +export type { Customer } from './types/customer.type' +export type { CustomerApi } from './types/customer-api.type' +export type { CustomerDeps } from './types/customer-deps.type' +export type { CustomerFactory } from './types/customer-factory.type' +``` + +## Фабрика + +Business-модуль всегда экспортирует фабрику. Фабрика лежит в корне модуля (`{name}.factory.ts`), типизируется через `{Name}Factory` и возвращает публичный runtime API модуля. + +Всё, что нужно внешнему коду в runtime, должно быть частью API, который возвращает фабрика. + +Модуль без cross-domain зависимостей экспортирует фабрику без аргументов. Модуль с зависимостями — фабрику, принимающую зависимости доменными именами. Типы всегда экспортируются напрямую через `export type` — `import type` не является runtime-зависимостью. + +Компоновка фабрик происходит на уровне модуля-потребителя: screen, layout, widget или любой другой модуль группы «Композиция». + +### Примеры + +Пример реализации фабрики в React см. в [Создание фабрики](../examples/react/factory.md). + +Пример композиции фабрик в React screen-модуле см. в [Композиция фабрик](../examples/react/factory-composition.md). + +## Жизненный цикл + +Модуль рождается на самом низком уровне использования и поднимается выше только при реальной потребности. + +- Нужен на одной странице → `screens/{name}/parts/` +- Появился в 2+ местах → поднимается по природе: + - абстрактный UI → `ui/` + - блок с данными/логикой → `widgets/` + - представление бизнес-домена → `business/{area}/parts/` + +Подъём — обычный рефакторинг в рамках задачи, а не отдельная активность. diff --git a/docs/docs/slm-design/architecture/monorepo.md b/docs/docs/slm-design/architecture/monorepo.md new file mode 100644 index 0000000..eb1f5d2 --- /dev/null +++ b/docs/docs/slm-design/architecture/monorepo.md @@ -0,0 +1,235 @@ +--- +title: Монорепозитории +description: "Раздел описывает, как применять SLM Design, когда фронтенд-проекты находятся в одном монорепозитории. В нём показано, что остаётся внутри приложений, что можно выносить в `packages/` и какие ограничения действуют для общих пакетов." +--- + +# Монорепозитории + +Раздел описывает, как применять SLM Design, когда фронтенд-проекты находятся в одном монорепозитории. В нём показано, что остаётся внутри приложений, что можно выносить в `packages/` и какие ограничения действуют для общих пакетов. + +## Определение + +**Монорепозиторий — внешний уровень организации нескольких фронтенд-приложений и общих пакетов. SLM применяется внутри каждого приложения, а frontend-пакеты, относящиеся к SLM, содержат переиспользуемый код, вынесенный из слоёв `ui`, `infra` и `shared`.** + +## Базовая структура + +Каждое приложение внутри `apps/` сохраняет собственную SLM-структуру в `src/`. + +```text +repo/ +├── apps/ +│ ├── web/ +│ │ └── src/ +│ │ ├── app/ +│ │ ├── layouts/ +│ │ ├── screens/ +│ │ ├── widgets/ +│ │ ├── business/ +│ │ ├── infra/ +│ │ ├── ui/ +│ │ └── shared/ +│ └── admin/ +│ └── src/ +│ └── ... +└── packages/ + ├── ui/ + │ ├── button/ # самостоятельный пакет UI-модуля + │ ├── input/ # самостоятельный пакет UI-модуля + │ └── modal/ # самостоятельный пакет UI-модуля + ├── infra/ + │ ├── theme/ # самостоятельный пакет infra-модуля + │ ├── backend-api/ # самостоятельный пакет infra-модуля + │ └── logger/ # самостоятельный пакет infra-модуля + └── shared/ # единый shared-пакет + ├── package.json + └── src/ + ├── lib/ # переиспользуемые утилиты + ├── helpers/ # переиспользуемые helpers + └── index.ts +``` + +`apps/{app}/src` — граница SLM-приложения. `packages/*` находятся выше SLM и не добавляют новые архитектурные слои. + +## Группировка frontend-пакетов + +Frontend-пакеты, вынесенные из SLM-приложений, рекомендуется группировать по источнику кода: `ui`, `infra`, `shared`. + +```text +packages/ui/* # пакеты UI-модулей +packages/infra/* # пакеты infra-модулей +packages/shared # единый shared-пакет +``` + +Эта группировка повторяет названия SLM-слоёв для навигации, но сама не является слоистой архитектурой внутри `packages/`. Монорепозиторий может содержать другие пакеты: tooling, конфиги, SDK, схемы, e2e и другие технические пакеты вне SLM. + +## Пакет и модуль + +Пакет не равен SLM-модулю: модуль — архитектурная единица внутри слоя приложения, package — единица монорепозитория для переиспользования, владения, сборки и публикации. + +В `packages/ui/*` размещаются пакеты самостоятельных UI-модулей. В `packages/infra/*` размещаются пакеты самостоятельных инфраструктурных модулей. `packages/shared` устроен иначе: это единый пакет для переиспользуемых утилит, helpers и другого фундаментального кода без привязки к конкретному приложению. + +```text +packages/ui/button/ +packages/ui/modal/ +packages/infra/theme/ +packages/infra/backend-api/ +packages/shared/ +``` + +## Что остаётся в приложении + +Слои `app`, `layouts`, `screens`, `widgets` и `business` остаются внутри конкретного приложения. + +```text +apps/web/src/app/ +apps/web/src/layouts/ +apps/web/src/screens/ +apps/web/src/widgets/ +apps/web/src/business/ +``` + +`app`, `layouts` и `screens` привязаны к роутингу, каркасу и страницам конкретного приложения. `widgets` не выносятся в пакеты, потому что это слой композиции интерфейса приложения. + +`business` не выносится в `packages/*`. Домены остаются рядом со сценариями приложения, чтобы не превращать монорепозиторий в общий бизнес-слой. + +## Что можно выносить + +В пакеты выносится только код из `ui`, `infra` и `shared`, который потенциально будет использоваться в двух и более фронтенд-приложениях монорепозитория. + +| Группа | Что выносить | Пример | +|--------|--------------|--------| +| `packages/ui/*` | Самостоятельные UI-модули без бизнес-логики | `packages/ui/button` | +| `packages/infra/*` | Самостоятельные технические сервисы | `packages/infra/backend-api` | +| `packages/shared` | Общие утилиты, helpers и фундаментальный код | `packages/shared` | + +Пакет можно создавать сразу, если модуль имеет общую природу и ожидается его переиспользование между приложениями. App-specific код остаётся внутри приложения. + +## UI-пакеты + +В `packages/ui/*` размещаются переиспользуемые UI-модули. + +```text +packages/ui/button/ +├── package.json +└── src/ + ├── button.tsx + ├── styles/ + ├── types/ + └── index.ts +``` + +UI-пакет не содержит бизнес-логику, обращения к API, сценарные хуки приложения и композицию страниц. + +## Infra-пакеты + +В `packages/infra/*` размещаются переиспользуемые инфраструктурные модули. + +```text +packages/infra/backend-api/ +├── package.json +└── src/ + ├── clients/ + ├── config/ + ├── types/ + └── index.ts +``` + +Привязанные к конкретному приложению сервисы остаются в `apps/{app}/src/infra`. Например, локализация со словарями конкретного продукта остаётся в приложении; общим пакетом может быть только переиспользуемый i18n-движок. + +## Shared-пакет + +`packages/shared` является единым пакетом. + +```text +packages/shared/ +├── package.json +└── src/ + ├── lib/ + ├── helpers/ + └── index.ts +``` + +В `packages/shared` сразу выносится общий фундаментальный код: чистые функции, helpers, утилиты, независимые константы и другой код без знания о продукте. + +Проектные стили, типы приложения, продуктовые конфиги и ресурсы, завязанные на одно приложение, в общий `shared` не выносятся. + +## Имена пакетов и импорты + +Путь импорта задаётся `name` в `package.json`, а не расположением директории. + +```json +{ + "name": "@repo/theme" +} +``` + +```text +packages/infra/theme/package.json +``` + +```ts +import { ThemeProvider } from '@repo/theme' +``` + +Пакеты должны импортироваться только через публичный API. Deep imports внутрь пакета запрещены. + +```ts +// Хорошо +import { Button } from '@repo/button' + +// Плохо +import { Button } from '@repo/button/src/button' +``` + +## Зависимости + +На уровне монорепозитория приложения зависят от пакетов, а пакеты не зависят от приложений. + +```text +apps → packages +packages -/→ apps +``` + +Внутри приложения продолжает действовать обычное направление зависимостей SLM. + +```text +app → [ layouts | screens ] → widgets → business → infra → ui → shared +``` + +Пакеты не должны нарушать природу своей группы: `packages/ui/*` не импортирует `packages/infra/*`, `packages/shared` не импортирует другие группы, а `packages/infra/*` не знает о приложениях. + +## Когда не выносить + +Не выносите код в пакет, если он не может быть использован в двух и более фронтенд-приложениях, зависит от роутинга или страниц, содержит бизнес-логику, отражает продуктовую композицию конкретного интерфейса или не имеет стабильного публичного API. + +Фактическое использование в одном приложении не запрещает пакет, если модуль имеет общую природу и потенциально нужен нескольким приложениям. + +```text +# Плохо +apps/web/src/screens/home/parts/promo-section/ +packages/ui/promo-section/ +``` + +Если блок нужен только одной странице или отражает продуктовую композицию конкретного приложения, он остаётся локальным `parts/`-модулем. + +## Конфигурационные пакеты + +Конфигурационные пакеты не относятся к SLM-архитектуре. + +Если в монорепозитории есть общие настройки TypeScript, ESLint, сборки или форматирования, они относятся к tooling-инфраструктуре репозитория. Такие пакеты могут находиться в `packages/`, но их структура зависит от выбранного инструментария и не участвует в правилах слоёв внутри `src/`. + +## Правила + +- SLM применяется внутри каждого `apps/{app}/src`. +- Frontend-пакеты, вынесенные из SLM-приложений, группируются в `packages/ui`, `packages/infra`, `packages/shared`. +- Группы `packages/ui`, `packages/infra`, `packages/shared` не являются SLM-слоями. +- В `packages/ui/*` размещаются пакеты самостоятельных UI-модулей. +- В `packages/infra/*` размещаются пакеты самостоятельных инфраструктурных модулей. +- `packages/shared` является единым пакетом для переиспользуемых утилит и helpers. +- Модуль можно размещать в пакете, если он потенциально будет использоваться в двух и более фронтенд-приложениях. +- `business`, `app`, `layouts`, `screens`, `widgets` не выносятся в пакеты. +- Проектные стили, типы приложения и продуктовые конфиги не выносятся в `packages/shared`. +- Пакеты не импортируют приложения. +- Межпакетные импорты идут только через публичный API. +- Deep imports внутрь пакетов запрещены. +- Локальная колокация важнее преждевременного выноса в `packages/*`. diff --git a/docs/docs/slm-design/architecture/segments.md b/docs/docs/slm-design/architecture/segments.md new file mode 100644 index 0000000..ba11b77 --- /dev/null +++ b/docs/docs/slm-design/architecture/segments.md @@ -0,0 +1,181 @@ +--- +title: Сегменты +description: "Раздел описывает сегменты SLM: что такое сегмент, какие бывают и что в каждом из них лежит." +--- + +# Сегменты + +Раздел описывает сегменты SLM: что такое сегмент, какие бывают и что в каждом из них лежит. + +## Определение + +**Сегмент — папка внутри модуля, которая группирует файлы по назначению. Набор сегментов не фиксирован — модуль включает только те, которые ему нужны. Команда сама определяет какие сегменты используются в проекте — архитектура даёт рекомендацию.** + +## Обзор + +| Сегмент | Содержимое | +|---------|------------| +| `ui/` | Презентационные компоненты родительского модуля | +| `parts/` | Вложенные модули со своими сегментами | +| `hooks/` | React-хуки | +| `stores/` | Сторы состояния | +| `services/` | Работа с внешними источниками данных | +| `mappers/` | Трансформация данных между форматами | +| `types/` | TypeScript-типы и интерфейсы | +| `styles/` | Стили | +| `lib/` | Утилиты и хелперы модуля | +| `config/` | Константы и конфигурация | + +## Сегмент ui/ + +Презентационные компоненты родительского модуля. `ui/` содержит только компоненты, которые отвечают за отображение части интерфейса и не выходят за границы своего модуля. + +Компонент в `ui/`: + +- Находится в собственной папке. +- Может содержать только `{name}.tsx`, `index.ts`, `styles/`, `types/`. +- Не содержит любые компоненты. +- Не содержит любые модули. +- Не импортирует код проекта за пределами родительского модуля. +- Не делает внешние запросы. +- Не вызывает сценарные хуки. +- Не получает данные самостоятельно, не выбирает источник данных и не композирует данные. +- Не содержит бизнес-логику или сценарную логику. + +Если UI-сущности нужно что-то за пределами этих ограничений, она должна быть оформлена как модуль. Полная граница описана в разделе [Компонент](./modules.md#компонент). + +Корневой файл модуля в `ui/` не размещается. Он лежит в корне модуля: `{module-name}.tsx`. + +```text +user/ +├── ui/ +│ ├── user-avatar/ +│ │ ├── user-avatar.tsx +│ │ ├── styles/ +│ │ │ └── user-avatar.module.css +│ │ ├── types/ +│ │ │ └── user-avatar-props.type.ts +│ │ └── index.ts +│ └── user-status/ +│ ├── user-status.tsx +│ └── index.ts +├── types/ +├── hooks/ +├── user.tsx +└── index.ts +``` + +Если UI-сущности нужна внутренняя декомпозиция, сценарная логика, получение данных или собственные архитектурные зависимости — это уже не компонент в `ui/`, а модуль в `parts/`. + +## Сегмент parts/ + +Вложенные модули со своими сегментами. `parts/` содержит только модули: каждый элемент `parts/` — папка полноценного модуля с собственным публичным API. Отдельные `.tsx`, стили, хуки или произвольные файлы в `parts/` не размещаются. + +```text +home/ +├── parts/ +│ ├── hero-section/ +│ │ ├── hero-section.tsx +│ │ ├── styles/ +│ │ ├── parts/ +│ │ │ └── top-banner/ +│ │ │ ├── top-banner.tsx +│ │ │ └── index.ts +│ │ └── index.ts +│ └── features-section/ +│ ├── features-section.tsx +│ ├── hooks/ +│ └── index.ts +├── home.screen.tsx +└── index.ts +``` + +Отличие от `ui/`: элемент `parts/` — модульная папка со своими сегментами. Элемент `ui/` — компонент родительского модуля без собственной архитектурной ответственности. + +Вложенность `parts/` инкапсулирует область разработки горизонтально: каждый разработчик работает в своём `parts/`-модуле, не затрагивая чужие. Это снижает конфликты при параллельной разработке. + +Если вложенный модуль обрастает своими `parts/` — это сигнал, что он достаточно самостоятельный для подъёма на уровень выше. + +## Сегмент hooks/ + +React-хуки модуля. Инкапсулируют логику, состояние, подписки, побочные эффекты. + +```text +hooks/ +├── use-auth.hook.ts +├── use-session.hook.ts +└── use-permissions.hook.ts +``` + +## Сегмент stores/ + +Сторы состояния модуля. Конкретная реализация зависит от выбранного стейт-менеджера (Zustand, MobX, Redux и т.д.). + +```text +stores/ +├── auth.store.ts +└── session.store.ts +``` + +## Сегмент services/ + +Работа с внешними источниками данных: API-вызовы, запросы, подписки. + +```text +services/ +├── auth.service.ts +└── token.service.ts +``` + +## Сегмент mappers/ + +Функции трансформации данных из одного формата в другой: DTO в доменный тип, доменный тип в DTO, доменный тип в ViewModel. + +```text +mappers/ +├── map-user.ts +├── map-product.ts +└── map-order-to-dto.ts +``` + +## Сегмент types/ + +TypeScript-типы и интерфейсы модуля. Доменные типы, DTO, пропсы компонентов. + +```text +types/ +├── user.type.ts +└── session.type.ts +``` + +## Сегмент styles/ + +Стили модуля. Формат зависит от выбранного подхода (CSS Modules, SCSS, CSS-in-JS и т.д.). + +```text +styles/ +├── auth.module.css +└── login-form.module.css +``` + +## Сегмент lib/ + +Утилиты и хелперы, специфичные для модуля. Чистые функции без побочных эффектов. + +```text +lib/ +├── validate-email.ts +└── format-phone.ts +``` + +Отличие от `shared/lib/`: здесь лежат утилиты, нужные только этому модулю. Общие утилиты — в `shared/lib/`. + +## Сегмент config/ + +Константы и конфигурация модуля: маршруты, лимиты, дефолтные значения. + +```text +config/ +├── routes.ts +└── constants.ts +``` diff --git a/docs/docs/slm-design/examples/react/composition-provider.md b/docs/docs/slm-design/examples/react/composition-provider.md new file mode 100644 index 0000000..4f5593b --- /dev/null +++ b/docs/docs/slm-design/examples/react/composition-provider.md @@ -0,0 +1,249 @@ +--- +title: Композиция через Provider +description: "Раздел показывает, как screen-модуль может получить готовую композицию бизнес-доменов через React Context, не вызывая фабрики внутри себя." +--- + +# Композиция через Provider + +Раздел показывает, как screen-модуль может получить готовую композицию бизнес-доменов через React Context, не вызывая фабрики внутри себя. + +## Идея + +Screen получает готовый API бизнес-доменов через React Context. Граф фабрик собирается снаружи, например в роутере, а внутренние `parts/` достают нужные домены через хук без пропс-дриллинга. + +## Принципы + +1. **Принадлежность.** Provider, Context и хук принадлежат конкретному screen-модулю и лежат в его сегментах. +2. **Внутренний тип.** Тип композиции не экспортируется наружу — это закрывает доступ из бизнес-модулей. +3. **Внутренний хук.** Хук доступа не экспортируется — доступен только внутри screen и его `parts/`. +4. **Публичный Provider.** Только Provider экспортируется через `index.ts`, чтобы роутер мог обернуть screen. +5. **Сборка снаружи.** Граф фабрик собирается в роутере или другом композиторе, screen фабрики не вызывает. +6. **Запрет для бизнеса.** Бизнес-модули не используют провайдеры композиции. Cross-domain зависимости передаются только через аргументы фабрики. + +## Структура модуля + +```text +screens/main/ +├── main.screen.tsx +├── providers/ +│ └── main-composition.provider.tsx +├── hooks/ +│ └── use-main-composition.hook.ts +├── types/ +│ └── main-composition.type.ts +├── parts/ +│ └── featured-products/ +│ ├── featured-products.tsx +│ └── index.ts +└── index.ts +``` + +Сегмент `providers/` — расширение стандартного набора SLM. Спецификация это разрешает: команда сама определяет, какие сегменты используются. + +## Распределение по сегментам + +| Файл | Сегмент | Назначение | +|------|---------|------------| +| `main-composition.type.ts` | `types/` | TypeScript-тип композиции | +| `main-composition.provider.tsx` | `providers/` | Context и Provider-компонент | +| `use-main-composition.hook.ts` | `hooks/` | React-хук доступа | +| `main.screen.tsx` | корень | Корневой компонент screen-модуля | +| `featured-products/` | `parts/` | Вложенный модуль со своим публичным API | + +## Тип композиции + +Файл: `screens/main/types/main-composition.type.ts`. + +Тип описывает, какие бизнес-домены доступны на этой странице. Он не экспортируется через `index.ts`, чтобы другие модули не зависели от внутренней формы композиции screen. + +```ts +import type { CatalogApi } from '@/business/catalog' +import type { CartApi } from '@/business/cart' + +export type MainComposition = { + catalog: CatalogApi + cart: CartApi +} +``` + +## Context и Provider + +Файл: `screens/main/providers/main-composition.provider.tsx`. + +Context — внутренняя деталь Provider, наружу он не экспортируется. Значение `null` по умолчанию нужно, чтобы хук мог проверить отсутствие Provider в дереве. + +Provider-компонент экспортируется через `index.ts`. Роутер передаёт в `value` уже собранный граф фабрик со стабильной ссылкой. + +```tsx +import { createContext, type ReactNode } from 'react' +import type { MainComposition } from '../types/main-composition.type' + +export const MainCompositionContext = createContext(null) + +type Props = { + value: MainComposition + children: ReactNode +} + +export const MainCompositionProvider = ({ value, children }: Props) => ( + + {children} + +) +``` + +## Хук доступа + +Файл: `screens/main/hooks/use-main-composition.hook.ts`. + +Хук остаётся внутренним и не экспортируется через `index.ts` модуля. Он доступен только внутри screen и его `parts/`. + +Если хук используется вне Provider, он бросает ошибку. Это даёт раннюю диагностику неправильной композиции дерева. + +```ts +import { useContext } from 'react' +import { MainCompositionContext } from '../providers/main-composition.provider' + +export const useMainComposition = () => { + const ctx = useContext(MainCompositionContext) + if (!ctx) { + throw new Error('useMainComposition must be used within MainCompositionProvider') + } + return ctx +} +``` + +## Сборка графа в роутере + +Файл: `app/router.tsx`. + +Роутер или другой композитор собирает граф фабрик в точке использования screen. Каждый домен получает свои зависимости через аргументы фабрики. + +Фабрики вызываются вне React-компонента, если не зависят от runtime-параметров. Так API доменов не пересоздаётся на каждый рендер route-компонента. + +```tsx +import { MainScreen, MainCompositionProvider } from '@/screens/main' +import { catalogFactory } from '@/business/catalog' +import { cartFactory } from '@/business/cart' +import { authFactory } from '@/business/auth' + +const auth = authFactory() +const catalog = catalogFactory() +const cart = cartFactory({ auth }) + +const MainRoute = () => ( + + + +) +``` + +## Корневой компонент screen + +Файл: `screens/main/main.screen.tsx`. + +Screen получает нужные домены из композиции и достаёт из API готовые хуки, компоненты или функции. В JSX используются уже локальные `useCategories` и `CategoryList`, а не обращение к фабричному API через точку. + +```tsx +import { useMainComposition } from './hooks/use-main-composition.hook' +import { FeaturedProducts } from './parts/featured-products' + +export const MainScreen = () => { + const { catalog } = useMainComposition() + const { useCategories, CategoryList } = catalog + const categories = useCategories() + + return ( +
+ + +
+ ) +} +``` + +## Вложенный part + +Файл: `screens/main/parts/featured-products/featured-products.tsx`. + +Вложенный модуль получает доступ к той же композиции родительского screen. Промежуточные компоненты не прокидывают домены через props. + +Из API доменов достаются готовые сущности: `useFeatured`, `ProductCard` и `addItem`. Компонент работает с ними напрямую. + +```tsx +import { useMainComposition } from '../../hooks/use-main-composition.hook' + +export const FeaturedProducts = () => { + const { catalog, cart } = useMainComposition() + const { useFeatured, ProductCard } = catalog + const { addItem } = cart + const products = useFeatured() + + return ( +
+ {products.map((product) => ( + addItem(product.id)} + /> + ))} +
+ ) +} +``` + +Файл: `screens/main/parts/featured-products/index.ts`. + +```ts +export { FeaturedProducts } from './featured-products' +``` + +## Публичный API screen-модуля + +Файл: `screens/main/index.ts`. + +Наружу экспортируются только screen и его Provider. `MainComposition`, `MainCompositionContext` и `useMainComposition` остаются деталями реализации. + +```ts +export { MainScreen } from './main.screen' +export { MainCompositionProvider } from './providers/main-composition.provider' +``` + +## Почему тип композиции не экспортируется + +Внутренний тип закрывает доступ к форме композиции из внешних модулей. Бизнес-модуль не должен знать, какие домены собраны для конкретного screen. + +Такой импорт из бизнес-модуля не должен быть возможен через публичный API screen. + +```ts +import type { MainComposition } from '@/screens/main' +``` + +Когда тип остаётся внутренним, такая связь невозможна через публичный API screen-модуля. + +## Почему хук не экспортируется + +Если хук доступа сделать публичным, любой модуль сможет вызвать его напрямую. Внутренний хук доступен только через относительные импорты внутри screen-модуля и его `parts/`. + +## Почему Provider экспортируется + +Provider безопасно экспортировать: сам по себе он не даёт доступ к данным, а только принимает готовую композицию и передаёт её детям внутри React-дерева. + +## Стабильность value + +Фабрики создаются на уровне модуля, поэтому `catalog` и `cart` сохраняют ссылки между рендерами `MainRoute`. + +Если домены зависят от runtime-параметров, граф нужно собирать в отдельном композиторе для этих параметров и передавать в Provider уже готовую композицию. + +## Расширение на другие screen-модули + +Паттерн повторяется для каждого screen, которому нужна композиция бизнес-доменов. + +```text +screens/checkout/providers/checkout-composition.provider.tsx +screens/checkout/hooks/use-checkout-composition.hook.ts +screens/checkout/types/checkout-composition.type.ts +``` + +Имена включают имя screen-модуля. Не используйте универсальные названия вроде `useComposition` или `useScope`: по имени файла должно быть понятно, к какой странице привязан Context. diff --git a/docs/docs/slm-design/examples/react/factory-composition.md b/docs/docs/slm-design/examples/react/factory-composition.md new file mode 100644 index 0000000..460321a --- /dev/null +++ b/docs/docs/slm-design/examples/react/factory-composition.md @@ -0,0 +1,52 @@ +--- +title: Композиция фабрик +description: "Раздел показывает, как собрать API нескольких business-модулей в React screen-модуле. Пример подходит для простой композиции, когда screen сам является точкой использования доменов." +--- + +# Композиция фабрик + +Раздел показывает, как собрать API нескольких business-модулей в React screen-модуле. Пример подходит для простой композиции, когда screen сам является точкой использования доменов. + +## Идея + +Композиция фабрик выполняется в модуле-потребителе: screen, layout или другом модуле группы «Композиция». Business-модули не импортируют runtime-код друг друга напрямую, а cross-domain зависимости получают только через аргументы фабрик. + +## Структура screen-модуля + +```text +screens/home/ +├── home.screen.tsx +└── index.ts +``` + +## Сборка фабрик + +Файл: `screens/home/home.screen.tsx`. + +```tsx +import { customerFactory } from '@/business/customer' +import { orderFactory } from '@/business/order' + +const customer = customerFactory() +const order = orderFactory({ customer }) + +const { useOrder, OrderCard } = order + +export const HomeScreen = () => { + const currentOrder = useOrder() + + return +} +``` + +`customerFactory` создаётся первой, потому что `orderFactory` зависит от части API домена `customer`. Модуль `order` не импортирует `customer` в runtime — зависимость передаётся снаружи. + +## Публичный API screen-модуля + +Файл: `screens/home/index.ts`. + +```ts +export { HomeScreen } from './home.screen' +``` + +Screen экспортирует только собственный публичный API. Собранные экземпляры business API остаются деталями реализации screen-модуля. diff --git a/docs/docs/slm-design/examples/react/factory.md b/docs/docs/slm-design/examples/react/factory.md new file mode 100644 index 0000000..e9d8abb --- /dev/null +++ b/docs/docs/slm-design/examples/react/factory.md @@ -0,0 +1,114 @@ +--- +title: Создание фабрики +description: "Раздел показывает, как оформить фабрику business-модуля в React-проекте: описать публичный API, зависимости и функцию, возвращающую runtime API." +--- + +# Создание фабрики + +Раздел показывает, как оформить фабрику business-модуля в React-проекте: описать публичный API, зависимости и функцию, возвращающую runtime API. + +## Структура business-модуля + +Фабрика лежит в корне business-модуля. Типы публичного API и зависимостей размещаются в `types/`. + +```text +business/customer/ +├── customer.factory.ts +├── hooks/ +├── types/ +│ ├── customer.type.ts +│ ├── customer-api.type.ts +│ └── customer-factory.type.ts +├── ui/ +└── index.ts +``` + +## Тип публичного API + +Публичный API описывает runtime-возможности, которые модуль отдаёт потребителям: хуки, компоненты и сценарные методы. + +```ts +// business/customer/types/customer-api.type.ts +import type { ReactNode } from 'react' +import type { Customer } from './customer.type' + +export type CustomerCardProps = { + customer: Customer +} + +export type CustomerApi = { + useCustomer: () => Customer | null + CustomerCard: (props: CustomerCardProps) => ReactNode +} +``` + +```ts +// business/customer/types/customer-factory.type.ts +import type { CustomerApi } from './customer-api.type' + +export type CustomerFactory = () => CustomerApi +``` + +## Фабрика без зависимостей + +Если модулю не нужны другие домены в runtime, фабрика создаётся без аргументов. + +```ts +// business/customer/customer.factory.ts +import { useCustomer } from './hooks/use-customer.hook' +import { CustomerCard } from './ui/customer-card' +import type { CustomerFactory } from './types/customer-factory.type' + +export const customerFactory: CustomerFactory = () => { + return { + useCustomer, + CustomerCard, + } +} +``` + +```ts +// business/customer/index.ts +export { customerFactory } from './customer.factory' + +export type { Customer } from './types/customer.type' +export type { CustomerApi } from './types/customer-api.type' +export type { CustomerFactory } from './types/customer-factory.type' +``` + +## Фабрика с зависимостями + +Если модулю нужен другой домен в runtime, зависимость передаётся аргументом фабрики. Тип зависимости описывает только нужную часть API. + +```ts +// business/order/types/order-deps.type.ts +import type { CustomerApi } from '@/business/customer' + +export type OrderDeps = { + customer: Pick +} +``` + +```ts +// business/order/types/order-factory.type.ts +import type { OrderApi } from './order-api.type' +import type { OrderDeps } from './order-deps.type' + +export type OrderFactory = (deps: OrderDeps) => OrderApi +``` + +```ts +// business/order/order.factory.ts +import { createUseOrder } from './hooks/use-order.hook' +import { OrderCard } from './ui/order-card' +import type { OrderFactory } from './types/order-factory.type' + +export const orderFactory: OrderFactory = (deps) => { + const useOrder = createUseOrder(deps) + + return { + useOrder, + OrderCard, + } +} +```