This commit is contained in:
S. Gromov
2026-08-10 09:12:22 +03:00
parent b5db9e5158
commit 691069af8e
55 changed files with 1519 additions and 3383 deletions

View File

@@ -0,0 +1,84 @@
# Архитектура SLM
SLM описывает владение ответственностями внутри одного фронтенд-приложения. Слой определяет роль кода, группа классифицирует модули, модуль владеет ответственностью, а сегмент организует реализацию владельца.
## Владение как основа
**Ответственность** — связная часть приложения с одной причиной изменяться. Она становится самостоятельной, когда ей нужны собственный публичный контракт, зависимости, состояние или область жизни.
У каждой самостоятельной ответственности есть ровно один владелец. В SLM владельцем является модуль. Он определяет:
- какие возможности доступны внешним потребителям;
- от каких других возможностей зависит ответственность;
- кому принадлежат данные и изменяемое состояние;
- когда создаются и уничтожаются долгоживущие ресурсы;
- как устроена внутренняя реализация.
Место выполнения кода не меняет владельца. Компонент, провайдер, маршрут или точка запуска могут технически вызывать код ответственности, но не получают владение ею автоматически.
## Структурная модель
```text
SLM root
└── слой
├── модуль
│ └── сегмент
└── группа
├── модуль
└── группа
└── модуль
└── сегмент
```
| Сущность | Назначение | Владеет ответственностью |
|---|---|---|
| Слой | Классифицирует код по архитектурной роли | Нет |
| Группа | Классифицирует модули внутри слоя | Нет |
| Модуль | Реализует одну самостоятельную ответственность | Да |
| Сегмент | Организует внутренности одного модуля | Нет |
Слой и группа находятся снаружи модульной границы. Сегмент находится внутри неё. Компоненты, хуки, сервисы, хранилища и другие детали реализации принадлежат ближайшему модулю-владельцу, если сами не образуют вложенный модуль.
## Порядок проектирования
Архитектурное решение принимается от смысла к структуре:
1. Описать результат или поведение, за которое должен отвечать код.
2. Определить одну причину изменения этой ответственности.
3. Найти связанные данные, поведение, состояние и жизненный цикл.
4. Назначить модуль владельцем и определить его внешних потребителей.
5. Выбрать [слой](./layers.md) по роли ответственности.
6. Спроектировать публичный API и допустимые зависимости [модуля](./modules.md).
7. При необходимости организовать реализацию [сегментами](./segments.md).
8. Только после этого выбрать физические пути и имена файлов.
Если ответственность или владелец не определены, файловая структура не может исправить архитектурную неопределённость.
## Логическая и физическая границы
Модуль не определяется наличием папки, `index.ts` или нескольких файлов. Его определяет самостоятельная ответственность и владение её контрактом, зависимостями, состоянием и жизненным циклом.
После принятия решения модуль получает отдельную папку и публичные точки входа. Это обязательное физическое представление модульной границы, необходимое потребителям и автоматическим проверкам.
Верны обе формулировки:
- самостоятельная ответственность требует модульной границы;
- отдельная папка сама по себе не доказывает наличие модуля.
Пути сопоставляются со слоями, группами, модулями и сегментами в стайлгайде или конфигурации конкретного проекта. Имя пути не меняет нормативный смысл сущности.
## Область применения
SLM применяется внутри **SLM root** — границы структурной архитектуры одного приложения. Это может быть `src/` или другая область, установленная проектом.
Архитектура определяет:
- роли слоёв и допустимые направления зависимостей;
- владельцев самостоятельных ответственностей;
- публичные границы модулей;
- назначение групп и сегментов;
- владение состоянием и жизненным циклом ресурсов.
SLM не задаёт обязательный поток данных, полный файловый стайлгайд, фиксированный набор сегментов, правила монорепозиториев или обязательную внутреннюю форму каждого модуля.
Нормативный смысл терминов находится в [терминологии](../reference/terminology.md). Точные блокирующие требования объявлены только в [реестре правил](../rules/registry.md).

120
docs/architecture/layers.md Normal file
View File

@@ -0,0 +1,120 @@
# Слои
Слой классифицирует код по архитектурной роли и задаёт допустимые направления зависимостей. Слой не владеет ответственностью: владельцем остаётся модуль, размещённый в этом слое.
Слой выбирается после определения ответственности. Похожее имя папки или наличие зависимости от конкретной библиотеки не являются основанием для выбора слоя.
## Роли слоёв
SLM определяет шесть ролей:
| Слой | Роль |
|---|---|
| `app` | Связь приложения с фреймворком: запуск, маршруты, преобразование внешних входных данных и подключение готовых публичных API |
| `compositions` | Сборка продуктового интерфейса: страницы, макеты, экраны, виджеты и другие композиции |
| `domains` | Предметные модели, правила, сценарии и продуктовое состояние |
| `infra` | Технические сервисы и возможности приложения без собственной предметной модели |
| `ui` | Универсальные интерфейсные модули без зависимости от конкретной продуктовой композиции |
| `shared` | Детерминированный фундамент без знания о продукте, изменяемого состояния и ввода-вывода |
Отсутствующая роль не требует пустой папки. Проект создаёт слой только тогда, когда в нём появляется соответствующая ответственность.
### App
`app` содержит точки связи с фреймворком: запуск, файлы маршрутов и преобразование внешних входных данных. Они подключают готовые публичные API других слоёв, но не присваивают их ответственность.
Точки входа `app` являются специальным немодульным исключением. Страница, макет, провайдер или другой самостоятельный продуктовый владелец реализуется в подходящем модуле и только подключается из `app`.
### Compositions
`compositions` содержит владельцев продуктовых композиций: страниц, макетов, экранов, виджетов, результатов маршрутов и интерфейса, объединяющего несколько ответственностей.
Конкретная организация слоя определяется продуктом и фреймворком. Названия `pages`, `layouts`, `screens` и `widgets` могут использоваться как группы, но не являются дополнительными слоями.
### Domains
`domains` содержит модули-владельцы предметных ответственностей: моделей, правил, сценариев и продуктового состояния.
Доменный модуль является обычным SLM-модулем. Ему не требуется отдельная архитектурная форма только потому, что он находится в `domains`.
### Infra
`infra` содержит технические возможности приложения: аналитику, локализацию, тему, телеметрию, интеграции с платформой и другие сервисы без собственной предметной модели.
Технический способ выполнения предметного сценария не переносит владение сценарием из `domains` в `infra`.
### UI
`ui` содержит универсальные интерфейсные модули, которые не знают о конкретной странице, маршруте или продуктовой композиции.
### Shared
`shared` содержит детерминированный фундамент, не зависящий от продукта и не имеющий ввода-вывода, изменяемого состояния или жизненного цикла.
В `shared` могут находиться обычные модули и небольшие немодульные ресурсы: чистые функции, общие типы, стили, декларативная конфигурация и статические файлы.
## Направление зависимостей
Матрица определяет, от каких слоёв может зависеть исходный слой:
| Исходный слой | Допустимые целевые слои |
|---|---|
| `app` | `app`, `compositions`, `domains`, `infra`, `ui`, `shared` |
| `compositions` | `compositions`, `domains`, `infra`, `ui`, `shared` |
| `domains` | `domains`, `infra`, `ui`, `shared` |
| `infra` | `infra`, `ui`, `shared` |
| `ui` | `ui`, `shared` |
| `shared` | `shared` |
Разрешённая зависимость может пропускать промежуточные слои. Например, `compositions` может напрямую использовать модуль `ui`, не создавая посредника в `domains` или `infra`.
Обычный импорт, импорт типа (`import type`) и реэкспорт одинаково участвуют в архитектурном графе. Для связи между модулями дополнительно действуют их [публичные границы и запрет циклов](./modules.md#зависимости-между-модулями).
Разрешённое направление не переносит владение. Если модуль `domains` использует `infra`, предметный сценарий остаётся ответственностью доменного модуля, а техническая возможность — ответственностью инфраструктурного.
`infra` может использовать `ui`, когда технической возможности нужно собственное визуальное представление: CAPTCHA, uploader, карта или инструмент разработчика. `ui` не использует `infra`; необходимые технические возможности универсальный UI получает через входной контракт.
## Группировка модулей
Группа классифицирует модули внутри одного слоя или другой группы. Она нужна, когда плоский список модулей перестаёт быть понятным.
```text
compositions/
├── pages/ # Группа
│ ├── catalog/ # Модуль
│ └── profile/ # Модуль
├── layouts/ # Группа
│ └── main/ # Модуль
└── widgets/ # Группа
└── cart-summary/ # Модуль
```
Группа:
- содержит только модули и вложенные группы;
- не владеет ответственностью или реализацией;
- не имеет состояния и жизненного цикла;
- не предоставляет публичный API;
- не является узлом графа зависимостей;
- не реэкспортирует содержащиеся в ней модули.
Модуль может находиться непосредственно в слое. Группа вводится только ради реальной классификации, а её названия и глубину определяет проект.
Группа организует несколько владельцев внутри слоя. [Сегмент](./segments.md) организует код внутри одного владельца.
## Немодульные исключения
Внутри SLM root код по умолчанию принадлежит модулю. Исключения ограничены двумя случаями:
- точка входа `app` непосредственно связывает приложение с фреймворком;
- ресурс `shared` является небольшой самостоятельной детерминированной единицей без внутренней границы.
Если ресурсу `shared` нужны несколько файлов реализации, собственные архитектурные зависимости, изменяемое состояние, ввод-вывод или жизненный цикл, ему требуется модуль-владелец.
## Связанные правила
- [`SLM-LAYER-R001`](../rules/registry.md#slm-layer-r001)
- [`SLM-LAYER-A002`](../rules/registry.md#slm-layer-a002)
- [`SLM-LAYER-R003`](../rules/registry.md#slm-layer-r003)
- [`SLM-GROUP-R007`](../rules/registry.md#slm-group-r007)
- [`SLM-MODULE-R011`](../rules/registry.md#slm-module-r011)

View File

@@ -0,0 +1,176 @@
# Модули
Модуль является владельцем самостоятельной ответственности. Публичный API, зависимости, состояние, жизненный цикл и внутренняя реализация ответственности относятся к модулю независимо от того, в каком файле или сущности фреймворка выполняется код.
## Ответственность и владелец
Перед созданием модуля ответственность формулируется без упоминания желаемой папки, файла или библиотеки.
Самостоятельность ответственности определяется вопросами:
- есть ли у неё отдельная причина изменяться;
- нужен ли внешним потребителям собственный контракт;
- есть ли у неё архитектурные зависимости;
- владеет ли она данными или изменяемым состоянием;
- нужна ли ей собственная область жизни;
- можно ли назвать её независимо от внутренней реализации.
Если ответственность самостоятельна, она получает ровно один модуль-владелец. Если несколько частей изменяются по несвязанным причинам, им нужны разные владельцы.
Размер не определяет модуль. Модуль может состоять из одного файла реализации, а большая папка может оставаться частью другого владельца.
## Граница владения
Модуль определяет:
- публичные возможности ответственности;
- допустимые внешние зависимости;
- модели и правила, принадлежащие ответственности;
- состояние и источник истины;
- создание и очистку долгоживущих ресурсов;
- устройство внутренней реализации.
Компонент, хук, провайдер, хранилище, сервис или маршрут могут выполнять часть этой работы технически. Владение остаётся у ближайшего модуля, пока часть кода не получает самостоятельную ответственность и собственную модульную границу.
## Публичный API
У модуля один логический публичный API. Он является контрактом между владельцем и внешними потребителями, а не перечнем всех внутренних файлов.
Публичный API:
- открывает только возможности, необходимые реальным внешним потребителям;
- скрывает детали реализации и изменяемые внутренние механизмы;
- не раскрывает внутренние сегменты;
- представлен объявленными публичными фасетами;
- является единственным способом доступа к модулю извне.
Внутри своей границы модуль может обращаться к собственным файлам напрямую. Требование публичного API действует при пересечении модульной границы.
### Фасеты
Фасет — публичная точка входа, открывающая часть единого API для определённой среды выполнения.
| Фасет | Назначение |
|---|---|
| `index` | Универсальные типы и исполняемый код, совместимый с серверным рендерингом, включая RSC, и клиентским выполнением |
| `client` | Клиентская граница фреймворка, участвующая в серверной предварительной отрисовке и выполняющаяся при гидратации и в браузере |
| `browser` | Код только для браузера, подключаемый динамически через границу с отключённым SSR |
| `server` | Код только для сервера, недоступный универсальной и клиентской среде выполнения |
`index` обязателен. Остальные фасеты создаются только при наличии реального потребителя и несовместимых требований к среде.
Ограничение среды распространяется на все исполняемые импорты и реэкспорты фасета, включая транзитивные. Имя файла, директива `use client`, tree shaking или проверка `typeof window` сами по себе не доказывают совместимость.
Один исполняемый экспорт размещается в минимально подходящем фасете и не дублируется между ними. Универсальный фасет не реэкспортирует специализированные фасеты.
```text
auth/
├── index.ts # Обязательный универсальный фасет
├── client.ts # При необходимости
├── browser.ts # При необходимости
├── server.ts # При необходимости
└── ... # Внутренняя реализация
```
Эта файловая форма представляет уже определённую публичную границу. Наличие `index.ts` само по себе не создаёт модуль.
## Зависимости между модулями
Зависимость связывает владельца исходного кода с владельцем импортируемого кода. Импорт внутреннего файла, сегмента или компонента относится к ближайшему модулю-владельцу. Вложенный модуль начинает собственную границу зависимостей.
При пересечении модульной границы код использует только публичный фасет целевого модуля:
```ts
// Допустимо
import { Button } from '@/ui/button'
// Недопустимый глубокий импорт
import { Button } from '@/ui/button/button'
```
Для каждой связи выполняются три условия:
1. Направление разрешено [матрицей слоёв](./layers.md#направление-зависимостей).
2. Целевой модуль используется только через публичный API.
3. Общий граф модулей остаётся ацикличным.
Модули одного слоя могут зависеть друг от друга, если соблюдены публичные границы и не возникает цикл. SLM не требует обязательного посредника или отдельного механизма dependency injection для любой межмодульной связи.
## Компоненты
Компонент является сущностью фреймворка, реализующей часть интерфейса родительского модуля. Он не образует самостоятельного владельца только из-за наличия отдельного файла, каталога, props, локального состояния, доступа к данным или кода жизненного цикла.
Зависимости, состояние и ресурсы компонента относятся к родительскому модулю. Если UI-часть получает самостоятельную ответственность, публичный API, собственные зависимости или область жизни, ей требуется модульная граница.
Модуль может состоять из одного корневого компонента. В этом случае компонент реализует интерфейс, а модуль остаётся владельцем ответственности.
Компонент может быть оформлен отдельным каталогом и иметь локальный `index.ts`:
```text
button-submit/
├── button-submit.tsx
├── styles/
│ └── button-submit.module.css
├── types/
│ └── button-submit.types.ts
└── index.ts
```
Такой `index.ts` является внутренней точкой входа компонента. Он упрощает импорты внутри модуля, но не создаёт SLM public API, нового владельца или узел графа зависимостей.
| `index.ts` компонента | Публичный фасет модуля |
|---|---|
| Действует внутри ближайшего модуля | Действует при пересечении модульной границы |
| Экспортирует локальную реализацию и типы | Экспортирует контракт владельца |
| Не создаёт архитектурную границу | Представляет архитектурную границу |
| Не делает компонент модулем | Принадлежит уже определённому модулю |
Если компонент входит в публичный контракт владельца, корневой фасет модуля явно реэкспортирует его локальную точку входа. Внешний код по-прежнему импортирует модуль, а не внутренний путь компонента.
## Вложенные модули
Вложенный модуль — самостоятельный владелец, физически размещённый внутри родительского модуля. Он имеет собственные ответственность, публичный API и границу зависимостей и подчиняется общим правилам модулей.
Код родительского модуля использует вложенный модуль через его публичный API. Код за пределами родительской границы не импортирует вложенный модуль напрямую и получает необходимые возможности через API родителя.
Если вложенный модуль становится нужен нескольким внешним владельцам, его можно перенести в минимальную общую область. Сам факт повторного использования внутри родителя переноса не требует.
## Состояние и жизненный цикл
Состояние принадлежит модулю, ответственность которого определяет его смысл, допустимые изменения и область жизни. Место хранения состояния не переносит владение в файл хранилища, провайдер или контекст фреймворка.
Для каждого долгоживущего ресурса модуль-владелец определяет:
- место создания;
- момент запуска;
- область жизни;
- допустимое число экземпляров;
- способ остановки, отмены или освобождения.
Подписка, обработчик событий, таймер, наблюдатель, запрос или соединение не остаются активными после завершения своей области жизни. Очистку может технически выполнить компонент, провайдер или фреймворк, но ответственность за корректную границу остаётся у модуля.
Размещение экземпляра на уровне файла не доказывает область жизни всего приложения. Точка входа `app` может запустить ресурс через публичный API модуля, но не становится его владельцем.
## Внутренняя организация
После определения ответственности и публичной границы модуль может организовать реализацию [сегментами](./segments.md), компонентами и вложенными модулями. SLM не требует полного каркаса и не задаёт обязательный набор внутренних папок.
Каждый модуль физически размещается в отдельной папке. Это делает логическую границу наблюдаемой для потребителей и автоматических проверок, но не заменяет решение о владельце.
Наличие `index.ts` не используется как единственный признак модуля. Модульную границу определяют ответственность, владелец, публичные потребители и сопоставление путей, принятое проектом.
## Связанные правила
- [`SLM-MODULE-A004`](../rules/registry.md#slm-module-a004)
- [`SLM-MODULE-A014`](../rules/registry.md#slm-module-a014)
- [`SLM-MODULE-R006`](../rules/registry.md#slm-module-r006)
- [`SLM-MODULE-R011`](../rules/registry.md#slm-module-r011)
- [`SLM-MODULE-R012`](../rules/registry.md#slm-module-r012)
- [`SLM-DEPENDENCY-A005`](../rules/registry.md#slm-dependency-a005)
- [`SLM-COMPONENT-R009`](../rules/registry.md#slm-component-r009)
- [`SLM-NESTED_MODULE-A010`](../rules/registry.md#slm-nested_module-a010)
- [`SLM-LIFECYCLE-R013`](../rules/registry.md#slm-lifecycle-r013)
- [`SLM-ENVIRONMENT-R016`](../rules/registry.md#slm-environment-r016)
- [`SLM-ENVIRONMENT-R017`](../rules/registry.md#slm-environment-r017)
- [`SLM-ENVIRONMENT-R018`](../rules/registry.md#slm-environment-r018)
- [`SLM-ENVIRONMENT-R019`](../rules/registry.md#slm-environment-r019)

View File

@@ -0,0 +1,92 @@
# Сегменты
Сегмент организует внутреннее содержимое одного модуля по назначению. Он помогает ориентироваться в реализации владельца, но не создаёт новую ответственность или архитектурную границу.
## Место в модели
Сегмент появляется только внутри уже определённого модуля:
```text
Слой → [Группа*] → Модуль → [Сегмент*]
```
Группа классифицирует несколько модулей внутри слоя. Сегмент классифицирует код одного модуля. Ни группа, ни сегмент не являются владельцами.
Все файлы, компоненты, состояние, зависимости и код жизненного цикла сегмента принадлежат родительскому модулю. Исключением является только вложенный модуль, который начинает собственную границу владения.
## Назначение
Сегмент используется, когда группировка внутренних файлов по назначению упрощает навигацию. Набор и названия сегментов определяет проект.
Возможная структура:
```text
profile/
├── index.ts
├── profile.tsx
├── hooks/ # Возможный сегмент
├── services/ # Возможный сегмент
├── stores/ # Возможный сегмент
├── types/ # Возможный сегмент
└── ui/ # Возможный сегмент
```
Ни один из показанных сегментов не обязателен. Маленький модуль может хранить реализацию в корне без дополнительных каталогов.
Сегмент:
- не имеет самостоятельной ответственности;
- не предоставляет публичный API;
- не владеет состоянием или жизненным циклом;
- не является узлом графа зависимостей;
- не импортируется внешним кодом как отдельная архитектурная сущность.
Локальный `index.ts` может использоваться во внутренней единице сегмента, например в каталоге компонента. Он не превращает эту единицу или сегмент в модульную границу.
## Компоненты и вложенные модули
Сегмент может содержать компоненты и вспомогательные файлы родительского модуля. Компонент вправе иметь локальные `styles/`, `types/`, `tests/` и внутренний `index.ts`; всё это остаётся реализацией ближайшего модуля.
```text
header/ # Модуль
└── components/ # Сегмент
└── button-submit/ # Компонент
├── button-submit.tsx
├── styles/
│ └── button-submit.module.css
├── types/
│ └── button-submit.types.ts
└── index.ts # Внутренняя точка входа
```
Внешний код не импортирует `components/button-submit`. Если компонент нужен снаружи, модуль-владелец реэкспортирует его через собственный публичный фасет.
Сегмент также может содержать вложенные модули. В отличие от остальных файлов сегмента, каждый вложенный модуль сам владеет отдельной ответственностью и имеет публичный API и границу зависимостей.
```text
landing/ # Родительский модуль
└── parts/ # Сегмент
└── hero/ # Вложенный модуль
├── hero.tsx
└── index.ts
```
Имя `parts` является примером, а не обязательным соглашением SLM.
## Выбор границы
| Ситуация | Решение |
|---|---|
| Код относится к существующему владельцу и группируется только по назначению | Сегмент |
| Части нужна собственная ответственность, API, зависимости, состояние или область жизни | Модуль |
| Несколько модулей слоя нужно классифицировать для навигации | Группа |
| Нескольким файлам не нужна отдельная группировка | Оставить в корне модуля |
Размер каталога и количество файлов не определяют выбор между сегментом и модулем.
## Связанные правила
- [`SLM-SEGMENT-R008`](../rules/registry.md#slm-segment-r008)
- [`SLM-MODULE-A004`](../rules/registry.md#slm-module-a004)
- [`SLM-COMPONENT-R009`](../rules/registry.md#slm-component-r009)
- [`SLM-NESTED_MODULE-A010`](../rules/registry.md#slm-nested_module-a010)