mirror of
https://github.com/gromlab-ru/slm-design.git
synced 2026-08-22 07:30:16 +03:00
chore: sync
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# Модули
|
||||
|
||||
Модуль является владельцем самостоятельной ответственности. Публичный API, зависимости, состояние, жизненный цикл и внутренняя реализация ответственности относятся к модулю независимо от того, в каком файле или сущности фреймворка выполняется код.
|
||||
Модуль является владельцем самостоятельной ответственности. Публичный API, зависимости, состояние, жизненный цикл и внутренняя реализация ответственности относятся к модулю независимо от того, в каком файле или механизме выполняется код.
|
||||
|
||||
## Ответственность и владелец
|
||||
|
||||
@@ -8,17 +8,37 @@
|
||||
|
||||
Самостоятельность ответственности определяется вопросами:
|
||||
|
||||
- есть ли у неё отдельная причина изменяться;
|
||||
- какой один результат или поведение она обеспечивает;
|
||||
- что модуль должен делать сам для получения этого результата;
|
||||
- какие возможности ему нужны от других модулей;
|
||||
- нужен ли внешним потребителям собственный контракт;
|
||||
- есть ли у неё архитектурные зависимости;
|
||||
- владеет ли она данными или изменяемым состоянием;
|
||||
- требуют ли зависимости отдельного архитектурного владения;
|
||||
- владеет ли она смыслом данных или изменяемого состояния;
|
||||
- нужна ли ей собственная область жизни;
|
||||
- можно ли назвать её независимо от внутренней реализации.
|
||||
|
||||
Если ответственность самостоятельна, она получает ровно один модуль-владелец. Если несколько частей изменяются по несвязанным причинам, им нужны разные владельцы.
|
||||
Props, импорты, Context, локальное состояние, lifecycle-код или количество файлов сами по себе не доказывают самостоятельность ответственности. Если ответственность самостоятельна, она получает ровно один модуль-владелец.
|
||||
|
||||
Размер не определяет модуль. Модуль может состоять из одного файла реализации, а большая папка может оставаться частью другого владельца.
|
||||
|
||||
## Ближайшая граница
|
||||
|
||||
Каждый файл принадлежит ближайшей модульной границе и реализует ответственность этого модуля. Вид кода не меняет владельца: 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
|
||||
```
|
||||
|
||||
Родитель и вложенный модуль не владеют одной ответственностью одновременно. Родитель определяет общий результат, а вложенный модуль — отдельно сформулированную связную часть этого результата.
|
||||
|
||||
## Граница владения
|
||||
|
||||
Модуль определяет:
|
||||
@@ -30,7 +50,7 @@
|
||||
- создание и очистку долгоживущих ресурсов;
|
||||
- устройство внутренней реализации.
|
||||
|
||||
Компонент, хук, провайдер, хранилище, сервис или маршрут могут выполнять часть этой работы технически. Владение остаётся у ближайшего модуля, пока часть кода не получает самостоятельную ответственность и собственную модульную границу.
|
||||
Внутренний файл может технически выполнять часть или всю эту работу. Архитектурное владение остаётся у модуля и не переносится в компонент, Provider, store или другой механизм реализации.
|
||||
|
||||
## Публичный API
|
||||
|
||||
@@ -54,7 +74,7 @@
|
||||
|---|---|
|
||||
| `index` | Универсальные типы и исполняемый код, совместимый с серверным рендерингом, включая RSC, и клиентским выполнением |
|
||||
| `client` | Клиентская граница фреймворка, участвующая в серверной предварительной отрисовке и выполняющаяся при гидратации и в браузере |
|
||||
| `browser` | Код только для браузера, подключаемый динамически через границу с отключённым SSR |
|
||||
| `browser` | Browser-only и динамически подключаемый клиентский код; фасет всегда импортируется динамически с отключённым SSR |
|
||||
| `server` | Код только для сервера, недоступный универсальной и клиентской среде выполнения |
|
||||
|
||||
`index` обязателен. Остальные фасеты создаются только при наличии реального потребителя и несовместимых требований к среде.
|
||||
@@ -74,11 +94,9 @@ auth/
|
||||
|
||||
Эта файловая форма представляет уже определённую публичную границу. Наличие `index.ts` само по себе не создаёт модуль.
|
||||
|
||||
## Зависимости между модулями
|
||||
## Зависимости
|
||||
|
||||
Зависимость связывает владельца исходного кода с владельцем импортируемого кода. Импорт внутреннего файла, сегмента или компонента относится к ближайшему модулю-владельцу. Вложенный модуль начинает собственную границу зависимостей.
|
||||
|
||||
При пересечении модульной границы код использует только публичный фасет целевого модуля:
|
||||
Модули одного слоя могут зависеть друг от друга. При пересечении любой модульной границы код использует публичный фасет целевого модуля, а общий граф модулей должен оставаться ацикличным.
|
||||
|
||||
```ts
|
||||
// Допустимо
|
||||
@@ -88,56 +106,115 @@ import { Button } from '@/ui/button'
|
||||
import { Button } from '@/ui/button/button'
|
||||
```
|
||||
|
||||
Для каждой связи выполняются три условия:
|
||||
Направления между слоями, same-layer импорты, роль групп и требования к lint-проверке описаны в разделе [Зависимости](./dependencies.md).
|
||||
|
||||
1. Направление разрешено [матрицей слоёв](./layers.md#направление-зависимостей).
|
||||
2. Целевой модуль используется только через публичный API.
|
||||
3. Общий граф модулей остаётся ацикличным.
|
||||
## Корень модуля
|
||||
|
||||
Модули одного слоя могут зависеть друг от друга, если соблюдены публичные границы и не возникает цикл. SLM не требует обязательного посредника или отдельного механизма dependency injection для любой межмодульной связи.
|
||||
Корень модуля не используется как плоский каталог реализации. В нём находятся:
|
||||
|
||||
## Компоненты
|
||||
- объявленные публичные фасеты;
|
||||
- не более одного опционального главного implementation- или assembly-файла.
|
||||
|
||||
Компонент является сущностью фреймворка, реализующей часть интерфейса родительского модуля. Он не образует самостоятельного владельца только из-за наличия отдельного файла, каталога, props, локального состояния, доступа к данным или кода жизненного цикла.
|
||||
|
||||
Зависимости, состояние и ресурсы компонента относятся к родительскому модулю. Если UI-часть получает самостоятельную ответственность, публичный API, собственные зависимости или область жизни, ей требуется модульная граница.
|
||||
|
||||
Модуль может состоять из одного корневого компонента. В этом случае компонент реализует интерфейс, а модуль остаётся владельцем ответственности.
|
||||
|
||||
Компонент может быть оформлен отдельным каталогом и иметь локальный `index.ts`:
|
||||
Главный файл непосредственно реализует или собирает ответственность модуля и однозначно соответствует ей по имени и роли. Возможные примеры:
|
||||
|
||||
```text
|
||||
button-submit/
|
||||
├── button-submit.tsx
|
||||
├── styles/
|
||||
│ └── button-submit.module.css
|
||||
├── types/
|
||||
│ └── button-submit.types.ts
|
||||
└── index.ts
|
||||
header/header.tsx
|
||||
footer/footer.tsx
|
||||
auth-guard/auth-guard.provider.tsx
|
||||
```
|
||||
|
||||
Такой `index.ts` является внутренней точкой входа компонента. Он упрощает импорты внутри модуля, но не создаёт SLM public API, нового владельца или узел графа зависимостей.
|
||||
Архитектурным владельцем остаётся модуль, а главный файл является основной реализацией или точкой её сборки. Если проблемно доказать, что файл является главным, его не выносят в корень. Вся остальная реализация размещается в [сегментах](./segments.md).
|
||||
|
||||
| `index.ts` компонента | Публичный фасет модуля |
|
||||
|---|---|
|
||||
| Действует внутри ближайшего модуля | Действует при пересечении модульной границы |
|
||||
| Экспортирует локальную реализацию и типы | Экспортирует контракт владельца |
|
||||
| Не создаёт архитектурную границу | Представляет архитектурную границу |
|
||||
| Не делает компонент модулем | Принадлежит уже определённому модулю |
|
||||
Главный framework-файл не обязан экспортироваться через `index`. Модуль открывает его через минимально подходящий фасет среды выполнения:
|
||||
|
||||
Если компонент входит в публичный контракт владельца, корневой фасет модуля явно реэкспортирует его локальную точку входа. Внешний код по-прежнему импортирует модуль, а не внутренний путь компонента.
|
||||
```text
|
||||
theme/
|
||||
├── index.ts # Универсальные публичные типы
|
||||
├── client.ts # Экспортирует ThemeProvider и useTheme
|
||||
├── theme.provider.tsx # Главная framework-реализация
|
||||
├── context/
|
||||
│ └── theme-context.ts
|
||||
├── hooks/
|
||||
│ └── use-theme.ts
|
||||
├── types/
|
||||
└── styles/
|
||||
```
|
||||
|
||||
`ThemeProvider` может хранить состояние, синхронизироваться с платформой, предоставлять Context и очищать ресурсы при размонтировании. Он технически реализует ответственность, но её смысл, контракт, зависимости и область жизни определяет модуль `theme`.
|
||||
|
||||
## 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-дереву фреймворка.
|
||||
|
||||
Локальный `index.ts` компонентной единицы упрощает импорты внутри модуля, но не создаёт публичный API, владельца или узел графа зависимостей. Если компонент нужен внешнему потребителю, фасет модуля явно реэкспортирует его, а внешний код по-прежнему импортирует модуль.
|
||||
|
||||
Названия `components`, `providers`, `styles`, `types` и `hooks` являются примерами локального стайлгайда, а не обязательными путями SLM.
|
||||
|
||||
## Вложенные модули
|
||||
|
||||
Вложенный модуль — самостоятельный владелец, физически размещённый внутри родительского модуля. Он имеет собственные ответственность, публичный API и границу зависимостей и подчиняется общим правилам модулей.
|
||||
Вложенный модуль — обычный модуль, физически размещённый внутри родительского. Он владеет отдельно сформулированной связной подответственностью, имеет публичный 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-реализацией.
|
||||
|
||||
Код родительского модуля использует вложенный модуль через его публичный API. Код за пределами родительской границы не импортирует вложенный модуль напрямую и получает необходимые возможности через API родителя.
|
||||
|
||||
Если вложенный модуль становится нужен нескольким внешним владельцам, его можно перенести в минимальную общую область. Сам факт повторного использования внутри родителя переноса не требует.
|
||||
Вложенный модуль может рекурсивно содержать другие вложенные модули. Именно модульная граница, а не каталог компонента, создаёт каждый следующий структурный уровень. Если вложенный модуль становится нужен нескольким внешним владельцам, его переносят в минимальную общую область.
|
||||
|
||||
## Колокация и рост
|
||||
|
||||
Код создаётся в самой узкой допустимой области внутри своего архитектурного владельца:
|
||||
|
||||
1. Вспомогательный код, нужный одной компонентной единице, колоцируется в её файле или каталоге.
|
||||
2. Каждая выделенная framework-компонентная единица размещается на общем внутреннем уровне модуля, даже если используется только одним другим компонентом.
|
||||
3. Код, нужный нескольким внутренним единицам, поднимается в их ближайший общий сегмент.
|
||||
4. При появлении самостоятельной подответственности вокруг кода создаётся вложенный модуль.
|
||||
5. Вложенный модуль переносится в общую область, когда прямой доступ к нему требуется внешним владельцам.
|
||||
|
||||
Расширение числа потребителей меняет колокацию внутри владельца. Появление самостоятельной ответственности меняет модульный граф.
|
||||
|
||||
## Состояние и жизненный цикл
|
||||
|
||||
Состояние принадлежит модулю, ответственность которого определяет его смысл, допустимые изменения и область жизни. Место хранения состояния не переносит владение в файл хранилища, провайдер или контекст фреймворка.
|
||||
Состояние принадлежит модулю, ответственность которого определяет его смысл, допустимые изменения и область жизни. Место хранения состояния не переносит владение в store, Provider или Context.
|
||||
|
||||
Для каждого долгоживущего ресурса модуль-владелец определяет:
|
||||
|
||||
@@ -147,18 +224,10 @@ button-submit/
|
||||
- допустимое число экземпляров;
|
||||
- способ остановки, отмены или освобождения.
|
||||
|
||||
Подписка, обработчик событий, таймер, наблюдатель, запрос или соединение не остаются активными после завершения своей области жизни. Очистку может технически выполнить компонент, провайдер или фреймворк, но ответственность за корректную границу остаётся у модуля.
|
||||
Подписка, обработчик событий, таймер, наблюдатель, запрос или соединение не остаются активными после завершения своей области жизни. Очистку может технически выполнить framework-компонент или сам фреймворк, но ответственность за корректную границу остаётся у модуля.
|
||||
|
||||
Размещение экземпляра на уровне файла не доказывает область жизни всего приложения. Точка входа `app` может запустить ресурс через публичный API модуля, но не становится его владельцем.
|
||||
|
||||
## Внутренняя организация
|
||||
|
||||
После определения ответственности и публичной границы модуль может организовать реализацию [сегментами](./segments.md), компонентами и вложенными модулями. SLM не требует полного каркаса и не задаёт обязательный набор внутренних папок.
|
||||
|
||||
Каждый модуль физически размещается в отдельной папке. Это делает логическую границу наблюдаемой для потребителей и автоматических проверок, но не заменяет решение о владельце.
|
||||
|
||||
Наличие `index.ts` не используется как единственный признак модуля. Модульную границу определяют ответственность, владелец, публичные потребители и сопоставление путей, принятое проектом.
|
||||
|
||||
## Связанные правила
|
||||
|
||||
- [`SLM-MODULE-A004`](../rules/registry.md#slm-module-a004)
|
||||
@@ -166,8 +235,9 @@ button-submit/
|
||||
- [`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-MODULE-R020`](../rules/registry.md#slm-module-r020)
|
||||
- [`SLM-MODULE-R021`](../rules/registry.md#slm-module-r021)
|
||||
- [`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)
|
||||
|
||||
Reference in New Issue
Block a user