mirror of
https://github.com/gromlab-ru/slm-design.git
synced 2026-08-22 07:30:16 +03:00
177 lines
15 KiB
Markdown
177 lines
15 KiB
Markdown
# Модули
|
||
|
||
Модуль является владельцем самостоятельной ответственности. Публичный 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)
|