Files
slm-design/docs/architecture/modules.md

254 lines
22 KiB
Markdown
Raw Normal View History

2026-08-10 09:12:22 +03:00
# Модули
2026-08-10 12:37:32 +03:00
Модуль является владельцем самостоятельной ответственности. Публичный API, зависимости, состояние, жизненный цикл и внутренняя реализация ответственности относятся к модулю независимо от того, в каком файле или механизме выполняется код.
2026-08-10 09:12:22 +03:00
## Ответственность и владелец
Перед созданием модуля ответственность формулируется без упоминания желаемой папки, файла или библиотеки.
Самостоятельность ответственности определяется вопросами:
2026-08-10 12:37:32 +03:00
- какой один результат или поведение она обеспечивает;
- что модуль должен делать сам для получения этого результата;
- какие возможности ему нужны от других модулей;
2026-08-10 09:12:22 +03:00
- нужен ли внешним потребителям собственный контракт;
2026-08-10 12:37:32 +03:00
- требуют ли зависимости отдельного архитектурного владения;
- владеет ли она смыслом данных или изменяемого состояния;
2026-08-10 09:12:22 +03:00
- нужна ли ей собственная область жизни;
- можно ли назвать её независимо от внутренней реализации.
2026-08-10 12:37:32 +03:00
Props, импорты, Context, локальное состояние, lifecycle-код или количество файлов сами по себе не доказывают самостоятельность ответственности. Если ответственность самостоятельна, она получает ровно один модуль-владелец.
2026-08-10 09:12:22 +03:00
2026-08-10 14:42:29 +03:00
Для доменного сценария выбор владельца дополнительно ограничен ролью слоя: такой сценарий принадлежит модулю `domains`. Этот модуль является [доменом](./domains.md) и дополнительно владеет предметным контрактом, ожидаемыми неуспешными исходами и адаптацией источников. Модуль `compositions` может владеть представлением страницы или экрана и использовать готовый доменный API, но не становится владельцем сценария из-за места вызова, единственного потребителя или отсутствия уже созданного доменного модуля. Подробная граница описана в разделе [Слои](./layers.md#граница-доменов-и-композиций).
2026-08-10 09:12:22 +03:00
Размер не определяет модуль. Модуль может состоять из одного файла реализации, а большая папка может оставаться частью другого владельца.
2026-08-10 12:37:32 +03:00
## Ближайшая граница
Каждый файл принадлежит ближайшей модульной границе и реализует ответственность этого модуля. Вид кода не меняет владельца: framework-компонент, Provider, Guard, hook, store, service или utility остаются внутренней реализацией ближайшего модуля.
Вложенный модуль начинает новую границу. Его содержимое реализует выделенную подответственность, а сам вложенный модуль как единица участвует в реализации общего результата родителя:
```text
checkout/ # Владеет ответственностью checkout
├── checkout.tsx # Реализует checkout
├── components/ # Реализуют checkout
└── modules/
└── form-session/ # Владеет подответственностью form session
├── form-session.provider.tsx
└── hooks/ # Реализуют form session
```
Родитель и вложенный модуль не владеют одной ответственностью одновременно. Родитель определяет общий результат, а вложенный модуль — отдельно сформулированную связную часть этого результата.
2026-08-10 09:12:22 +03:00
## Граница владения
Модуль определяет:
- публичные возможности ответственности;
- допустимые внешние зависимости;
- модели и правила, принадлежащие ответственности;
- состояние и источник истины;
- создание и очистку долгоживущих ресурсов;
- устройство внутренней реализации.
2026-08-10 12:37:32 +03:00
Внутренний файл может технически выполнять часть или всю эту работу. Архитектурное владение остаётся у модуля и не переносится в компонент, Provider, store или другой механизм реализации.
2026-08-10 09:12:22 +03:00
## Публичный API
У модуля один логический публичный API. Он является контрактом между владельцем и внешними потребителями, а не перечнем всех внутренних файлов.
Публичный API:
- открывает только возможности, необходимые реальным внешним потребителям;
- скрывает детали реализации и изменяемые внутренние механизмы;
- не раскрывает внутренние сегменты;
- представлен объявленными публичными фасетами;
- является единственным способом доступа к модулю извне.
Внутри своей границы модуль может обращаться к собственным файлам напрямую. Требование публичного API действует при пересечении модульной границы.
### Фасеты
Фасет — публичная точка входа, открывающая часть единого API для определённой среды выполнения.
| Фасет | Назначение |
|---|---|
| `index` | Универсальные типы и исполняемый код, совместимый с серверным рендерингом, включая RSC, и клиентским выполнением |
| `client` | Клиентская граница фреймворка, участвующая в серверной предварительной отрисовке и выполняющаяся при гидратации и в браузере |
2026-08-10 12:37:32 +03:00
| `browser` | Browser-only и динамически подключаемый клиентский код; фасет всегда импортируется динамически с отключённым SSR |
2026-08-10 09:12:22 +03:00
| `server` | Код только для сервера, недоступный универсальной и клиентской среде выполнения |
`index` обязателен. Остальные фасеты создаются только при наличии реального потребителя и несовместимых требований к среде.
Ограничение среды распространяется на все исполняемые импорты и реэкспорты фасета, включая транзитивные. Имя файла, директива `use client`, tree shaking или проверка `typeof window` сами по себе не доказывают совместимость.
Один исполняемый экспорт размещается в минимально подходящем фасете и не дублируется между ними. Универсальный фасет не реэкспортирует специализированные фасеты.
```text
auth/
├── index.ts # Обязательный универсальный фасет
├── client.ts # При необходимости
├── browser.ts # При необходимости
├── server.ts # При необходимости
└── ... # Внутренняя реализация
```
Эта файловая форма представляет уже определённую публичную границу. Наличие `index.ts` само по себе не создаёт модуль.
2026-08-10 12:37:32 +03:00
## Зависимости
2026-08-10 09:12:22 +03:00
2026-08-10 12:37:32 +03:00
Модули одного слоя могут зависеть друг от друга. При пересечении любой модульной границы код использует публичный фасет целевого модуля, а общий граф модулей должен оставаться ацикличным.
2026-08-10 09:12:22 +03:00
```ts
// Допустимо
import { Button } from '@/ui/button'
// Недопустимый глубокий импорт
import { Button } from '@/ui/button/button'
```
2026-08-10 12:37:32 +03:00
Направления между слоями, same-layer импорты, роль групп и требования к lint-проверке описаны в разделе [Зависимости](./dependencies.md).
2026-08-10 09:12:22 +03:00
2026-08-10 12:37:32 +03:00
## Корень модуля
2026-08-10 09:12:22 +03:00
2026-08-10 12:37:32 +03:00
Корень модуля не используется как плоский каталог реализации. В нём находятся:
2026-08-10 09:12:22 +03:00
2026-08-10 12:37:32 +03:00
- объявленные публичные фасеты;
- не более одного опционального главного implementation- или assembly-файла.
2026-08-10 09:12:22 +03:00
2026-08-10 12:37:32 +03:00
Главный файл непосредственно реализует или собирает ответственность модуля и однозначно соответствует ей по имени и роли. Возможные примеры:
2026-08-10 09:12:22 +03:00
2026-08-10 12:37:32 +03:00
```text
header/header.tsx
footer/footer.tsx
auth-guard/auth-guard.provider.tsx
```
2026-08-10 09:12:22 +03:00
2026-08-10 12:37:32 +03:00
Архитектурным владельцем остаётся модуль, а главный файл является основной реализацией или точкой её сборки. Если проблемно доказать, что файл является главным, его не выносят в корень. Вся остальная реализация размещается в [сегментах](./segments.md).
2026-08-10 09:12:22 +03:00
2026-08-10 12:37:32 +03:00
Главный framework-файл не обязан экспортироваться через `index`. Модуль открывает его через минимально подходящий фасет среды выполнения:
2026-08-10 09:12:22 +03:00
```text
2026-08-10 12:37:32 +03:00
theme/
├── index.ts # Универсальные публичные типы
├── client.ts # Экспортирует ThemeProvider и useTheme
├── theme.provider.tsx # Главная framework-реализация
├── context/
│ └── theme-context.ts
├── hooks/
│ └── use-theme.ts
2026-08-10 09:12:22 +03:00
├── types/
2026-08-10 12:37:32 +03:00
└── styles/
2026-08-10 09:12:22 +03:00
```
2026-08-10 12:37:32 +03:00
`ThemeProvider` может хранить состояние, синхронизироваться с платформой, предоставлять Context и очищать ресурсы при размонтировании. Он технически реализует ответственность, но её смысл, контракт, зависимости и область жизни определяет модуль `theme`.
2026-08-10 09:12:22 +03:00
2026-08-10 12:37:32 +03:00
## Framework-компоненты
SLM не вводит собственный вид компонента. Модуль может содержать любые framework-компоненты: визуальные элементы, Providers, Guards, Error Boundaries и другие сущности, которые используемый фреймворк считает компонентами.
Помимо опционального главного framework-файла в корне, остальные framework-компоненты организуются на одном внутреннем уровне относительно модуля. Они могут быть одиночными файлами или каталогами с локальными стилями, типами, hooks, тестами и внутренним `index.ts`, но каталог компонентной единицы не содержит другие компонентные единицы или вложенные модули.
```text
main-layout/ # Модуль
├── index.ts # Публичный фасет
├── main-layout.tsx # Главная реализация
├── components/ # Сегмент
│ ├── header/
│ │ ├── index.ts # Локальная точка входа
│ │ ├── header.tsx
│ │ ├── styles/
│ │ └── types/
│ ├── navigation-item.tsx
│ └── footer.tsx
└── providers/ # Сегмент
└── layout-state/
├── layout-state.provider.tsx
├── hooks/
└── types/
```
`Header` может рендерить `NavigationItem`, но их файловые области остаются соседними относительно `main-layout`. Ограничение относится к организации файлов, а не к runtime-дереву фреймворка.
2026-08-10 09:12:22 +03:00
2026-08-10 12:37:32 +03:00
Локальный `index.ts` компонентной единицы упрощает импорты внутри модуля, но не создаёт публичный API, владельца или узел графа зависимостей. Если компонент нужен внешнему потребителю, фасет модуля явно реэкспортирует его, а внешний код по-прежнему импортирует модуль.
Названия `components`, `providers`, `styles`, `types` и `hooks` являются примерами локального стайлгайда, а не обязательными путями SLM.
2026-08-10 09:12:22 +03:00
## Вложенные модули
2026-08-10 12:37:32 +03:00
Вложенный модуль — обычный модуль, физически размещённый внутри родительского. Он владеет отдельно сформулированной связной подответственностью, имеет публичный API, собственные зависимости и область жизни и подчиняется всем правилам модулей.
```text
checkout/ # Родительский модуль
├── index.ts
├── checkout.tsx # Главная реализация checkout
├── components/
│ ├── order-summary.tsx
│ └── submit-order.tsx
└── modules/
└── form-session/ # Вложенный модуль
├── index.ts # Универсальные публичные типы
├── client.ts # Экспортирует Provider и hook
├── form-session.provider.tsx # Главная реализация form session
├── hooks/
│ └── use-form-session.ts
└── types/
```
Framework-компонент не превращается в архитектурную сущность. Если окружающему его коду требуется самостоятельная ответственность, вокруг кода создаётся вложенный модуль, а компонент остаётся его обычной framework-реализацией.
2026-08-10 09:12:22 +03:00
Код родительского модуля использует вложенный модуль через его публичный API. Код за пределами родительской границы не импортирует вложенный модуль напрямую и получает необходимые возможности через API родителя.
2026-08-10 12:37:32 +03:00
Вложенный модуль может рекурсивно содержать другие вложенные модули. Именно модульная граница, а не каталог компонента, создаёт каждый следующий структурный уровень. Если вложенный модуль становится нужен нескольким внешним владельцам, его переносят в минимальную общую область.
## Колокация и рост
Код создаётся в самой узкой допустимой области внутри своего архитектурного владельца:
1. Вспомогательный код, нужный одной компонентной единице, колоцируется в её файле или каталоге.
2. Каждая выделенная framework-компонентная единица размещается на общем внутреннем уровне модуля, даже если используется только одним другим компонентом.
3. Код, нужный нескольким внутренним единицам, поднимается в их ближайший общий сегмент.
4. При появлении самостоятельной подответственности вокруг кода создаётся вложенный модуль.
5. Вложенный модуль переносится в общую область, когда прямой доступ к нему требуется внешним владельцам.
Расширение числа потребителей меняет колокацию внутри владельца. Появление самостоятельной ответственности меняет модульный граф.
2026-08-10 09:12:22 +03:00
## Состояние и жизненный цикл
2026-08-10 12:37:32 +03:00
Состояние принадлежит модулю, ответственность которого определяет его смысл, допустимые изменения и область жизни. Место хранения состояния не переносит владение в store, Provider или Context.
2026-08-10 09:12:22 +03:00
Для каждого долгоживущего ресурса модуль-владелец определяет:
- место создания;
- момент запуска;
- область жизни;
- допустимое число экземпляров;
- способ остановки, отмены или освобождения.
2026-08-10 12:37:32 +03:00
Подписка, обработчик событий, таймер, наблюдатель, запрос или соединение не остаются активными после завершения своей области жизни. Очистку может технически выполнить framework-компонент или сам фреймворк, но ответственность за корректную границу остаётся у модуля.
2026-08-10 09:12:22 +03:00
Размещение экземпляра на уровне файла не доказывает область жизни всего приложения. Точка входа `app` может запустить ресурс через публичный API модуля, но не становится его владельцем.
## Связанные правила
- [`SLM-MODULE-A004`](../rules/registry.md#slm-module-a004)
- [`SLM-MODULE-A014`](../rules/registry.md#slm-module-a014)
2026-08-10 14:42:29 +03:00
- [`SLM-DOMAIN-R022`](../rules/registry.md#slm-domain-r022)
- [`SLM-DOMAIN-R023`](../rules/registry.md#slm-domain-r023)
- [`SLM-DOMAIN-R024`](../rules/registry.md#slm-domain-r024)
- [`SLM-DOMAIN-R025`](../rules/registry.md#slm-domain-r025)
- [`SLM-DOMAIN-R026`](../rules/registry.md#slm-domain-r026)
2026-08-10 09:12:22 +03:00
- [`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)
2026-08-10 12:37:32 +03:00
- [`SLM-MODULE-R020`](../rules/registry.md#slm-module-r020)
- [`SLM-MODULE-R021`](../rules/registry.md#slm-module-r021)
2026-08-10 09:12:22 +03:00
- [`SLM-DEPENDENCY-A005`](../rules/registry.md#slm-dependency-a005)
- [`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)