chore: sync

This commit is contained in:
2026-08-10 12:37:32 +03:00
parent af155fff0a
commit 1ba664f445
15 changed files with 684 additions and 238 deletions

View File

@@ -1,75 +1,107 @@
--- ---
layout: home layout: home
title: SLM Design title: Архитектура фронтенд-приложений
description: Архитектура фронтенд-приложений с явным владением ответственностями description: SLM Design помогает командам сохранять понятную структуру и предсказуемо развивать фронтенд-приложения по мере роста продукта.
hero: hero:
name: SLM Design name: SLM Design
text: Архитектура владения ответственностями text: Архитектура фронтенд-приложений
tagline: Сначала определяется ответственность и её владелец. Слои, группы, модули и сегменты выражают уже принятое архитектурное решение. tagline: Практичная модель для растущих команд и продуктов. Меньше споров о структуре, безопаснее изменения и понятнее код.
image: image:
src: /logo.svg src: /logo.svg
alt: SLM Design alt: SLM Design
actions: actions:
- theme: brand - theme: brand
text: Изучить архитектуру text: Узнать, как это работает
link: /architecture/ link: /architecture/
- theme: alt - theme: alt
text: Открыть правила text: Посмотреть правила
link: /rules/registry link: /rules/registry
features: features:
- title: Ответственность раньше структуры - title: Один язык для всей команды
details: Модуль появляется из самостоятельной ответственности, а не из размера каталога, количества файлов или выбранного фреймворка. details: Разработчики одинаково понимают границы, ответственность и место нового кода. Архитектурные решения перестают зависеть от личных предпочтений.
- title: Явная структурная модель - title: Предсказуемые изменения
details: Слой определяет роль, группа классифицирует модули, модуль владеет ответственностью, сегмент организует реализацию. details: Локальная правка остаётся локальной. Команда может развивать внутреннюю реализацию, не переписывая половину приложения.
- title: Проверяемые границы - title: Рост без хаоса
details: Публичные API, направление зависимостей и правила жизненного цикла делают архитектурное решение наблюдаемым и проверяемым. details: Структура усложняется только вместе с продуктом, а не из-за количества файлов, компонентов или выбранных библиотек.
- title: Архитектура видна в репозитории
details: Правила выражены кодом и структурой проекта, поэтому документация не расходится с реальным приложением.
- title: Независимость от стека
details: SLM не требует конкретного фреймворка, state manager или способа работы с данными и не ограничивает внутреннюю реализацию.
- title: Постепенное внедрение
details: Начните с одного спорного участка и расширяйте модель по мере необходимости, без полной перестройки приложения.
--- ---
SLM Design (Scoped Layered Module Design) — структурная архитектура фронтенд-приложений, основанная на явном владении ответственностями. ## Папки перестают быть архитектурой, когда продукт начинает расти
Архитектурное решение начинается не с папки или имени файла. Сначала определяется ответственность, затем её владелец, роль владельца в приложении и только после этого физическое размещение кода. На старте почти любая структура выглядит понятной. Затем появляются десятки компонентов, общие hooks, Providers, stores, глубокие импорты и модули, которые знают друг о друге слишком много. Папки остаются на месте, но границы ответственности исчезают.
## Основа SLM возвращает архитектуре наблюдаемый смысл:
SLM использует четыре структурных понятия: > **Модуль владеет ответственностью. Весь код внутри ближайшей модульной границы реализует её.**
1. **Слой** классифицирует код по архитектурной роли и ограничивает направление зависимостей. Это правило одинаково работает для страницы, доменного сценария, UI-библиотеки, инфраструктурного сервиса и небольшого внутреннего модуля.
2. **Группа** помогает классифицировать модули внутри слоя, но ничего не реализует и ничем не владеет.
3. **Модуль** владеет самостоятельной ответственностью, её публичным API, зависимостями, состоянием и жизненным циклом. ## Что меняется для команды
4. **Сегмент** организует внутреннее содержимое одного модуля и не создаёт нового владельца.
| Когда границ нет | С SLM Design |
|---|---|
| Решение о размещении кода принимается по похожей папке | Сначала определяется ответственность и её владелец |
| Компоненты и services становятся скрытыми архитектурными центрами | Любой внутренний механизм остаётся реализацией ближайшего модуля |
| Потребители импортируют удобный внутренний файл | Чужой модуль доступен только через публичный фасет |
| Циклы обнаруживаются во время большого рефакторинга | Модульный граф можно проверять lint-инструментами |
| Новые уровни создаются из-за размера каталога | Вложенный модуль появляется только для самостоятельной подответственности |
Результат: меньше случайной связанности, меньше споров о папках и предсказуемый радиус каждого изменения.
## Не ещё один шаблон директорий
SLM не диктует, какие библиотеки, state managers или framework-механизмы использовать. Внутри модуля могут находиться компоненты, Providers, Guards, hooks, stores, services, utilities и сторонние SDK.
Архитектура отвечает на другие вопросы:
1. За какой результат отвечает этот код?
2. Какой модуль владеет его контрактом и состоянием?
3. Что действительно нужно внешним потребителям?
4. Какие зависимости допустимы и не создают ли они цикл?
5. Где заканчивается область жизни ресурсов?
Файловая структура появляется после ответов, а не заменяет их.
## От одного файла до дерева владельцев
Модуль может начинаться с одного главного файла и расти без смены архитектурной сущности:
```text ```text
Слой → [Группа*] → Модуль → [Сегмент*] checkout/
├── index.ts # Публичный контракт
├── checkout.tsx # Главная реализация
├── components/ # Внутренний код checkout
├── hooks/
├── services/
└── modules/
└── form-session/ # Самостоятельная подответственность
├── index.ts
├── form-session.provider.tsx
└── hooks/
``` ```
Знак `*` означает, что элементов может не быть или их может быть несколько. Группы могут быть вложены друг в друга внутри одного слоя. Сегменты всегда остаются внутри одного модуля. Размер, количество файлов и framework-роли не создают владельца. Только отдельно сформулированная ответственность получает модульную границу.
## Документация ## Правила, которые можно проверить
### Архитектура SLM разделяет смысловые решения и структурные инварианты:
- [Обзор архитектуры](./architecture/) - ответственность, владелец и минимальный публичный контракт проверяются на архитектурном ревью;
- [Слои](./architecture/layers.md) - направления между слоями, глубокие импорты, публичные фасеты и модульные циклы проверяются автоматически;
- [Модули](./architecture/modules.md) - каждый rule имеет стабильный код и одно место нормативной формулировки;
- [Сегменты](./architecture/segments.md) - framework-компоненты не образуют бесконечную файловую рекурсию, а новые уровни появляются только через вложенные модули.
### Правила Вы получаете не абстрактный набор рекомендаций, а модель, которую можно обсуждать одинаковыми терминами, видеть в репозитории и постепенно автоматизировать.
- [Как устроены правила](./rules/) ## Начните с одной ответственности
- [Реестр правил](./rules/registry.md)
### Справочные материалы Не нужно переписывать приложение целиком. Выберите один спорный участок, сформулируйте его ответственность, назначьте владельца и закройте внутреннюю реализацию публичным API. Этого достаточно, чтобы увидеть разницу между папкой и архитектурной границей.
- [Терминология](./reference/terminology.md) [Спроектировать первый модуль](./architecture/modules.md) · [Разобрать зависимости](./architecture/dependencies.md) · [Проверить существующую структуру](./reference/validation.md)
- [Проверка архитектуры](./reference/validation.md)
## Порядок принятия решения
1. Сформулировать ответственность без упоминания папок, файлов и библиотек.
2. Назначить одного владельца ответственности.
3. Выбрать слой по роли владельца.
4. Определить публичный контракт, зависимости, состояние и жизненный цикл.
5. Организовать реализацию сегментами, если это упрощает навигацию.
6. Представить принятое решение папками, файлами и публичными точками входа.

View File

@@ -4,17 +4,17 @@ SLM описывает владение ответственностями вн
## Владение как основа ## Владение как основа
**Ответственность**связная часть приложения с одной причиной изменяться. Она становится самостоятельной, когда ей нужны собственный публичный контракт, зависимости, состояние или область жизни. **Ответственность**результат или поведение приложения, за которое отвечает один модуль-владелец. Она становится самостоятельной, когда ей нужны собственный публичный контракт, зависимости, состояние или область жизни, а не только внутренняя роль в работе другого модуля. Props, импорты, локальное состояние и lifecycle-код сами по себе этого не доказывают.
У каждой самостоятельной ответственности есть ровно один владелец. В SLM владельцем является модуль. Он определяет: У каждой самостоятельной ответственности есть ровно один владелец. В SLM владельцем является модуль. Он определяет:
- какие возможности доступны внешним потребителям; - какие возможности доступны внешним потребителям;
- от каких других возможностей зависит ответственность; - от каких других модулей зависит ответственность;
- кому принадлежат данные и изменяемое состояние; - кому принадлежат данные и изменяемое состояние;
- когда создаются и уничтожаются долгоживущие ресурсы; - когда создаются и уничтожаются долгоживущие ресурсы;
- как устроена внутренняя реализация. - как устроена внутренняя реализация.
Место выполнения кода не меняет владельца. Компонент, провайдер, маршрут или точка запуска могут технически вызывать код ответственности, но не получают владение ею автоматически. Каждый файл принадлежит ближайшей модульной границе и реализует ответственность этого модуля. Место выполнения или вид кода не меняют владельца.
## Структурная модель ## Структурная модель
@@ -22,41 +22,58 @@ SLM описывает владение ответственностями вн
SLM root SLM root
└── слой └── слой
├── модуль ├── модуль
── сегмент ── сегмент
│ └── вложенный модуль
│ ├── сегмент
│ └── вложенный модуль
└── группа └── группа
├── модуль ├── модуль
└── группа └── группа
└── модуль └── модуль
└── сегмент
``` ```
| Сущность | Назначение | Владеет ответственностью | | Сущность | Назначение | Владеет ответственностью |
|---|---|---| |---|---|---|
| Слой | Классифицирует код по архитектурной роли | Нет | | Слой | Классифицирует код по архитектурной роли | Нет |
| Группа | Классифицирует модули внутри слоя | Нет | | Группа | Навигационно классифицирует модули внутри слоя | Нет |
| Модуль | Реализует одну самостоятельную ответственность | Да | | Модуль | Владеет одной самостоятельной ответственностью | Да |
| Сегмент | Организует внутренности одного модуля | Нет | | Сегмент | Организует внутренности одного модуля | Нет |
Слой и группа находятся снаружи модульной границы. Сегмент находится внутри неё. Компоненты, хуки, сервисы, хранилища и другие детали реализации принадлежат ближайшему модулю-владельцу, если сами не образуют вложенный модуль. Вложенный модуль является обычным модулем, размещённым внутри родительского. Он владеет отдельно сформулированной подответственностью и создаёт следующий рекурсивный структурный уровень.
Слой и группа находятся снаружи модульной границы. Сегмент находится внутри неё. Framework-компоненты, Providers, Guards, hooks, stores, services и другой внутренний код не являются архитектурными сущностями SLM и принадлежат ближайшему модулю.
## Внутренняя реализация
Модуль может содержать любой код, относящийся к его ответственности. SLM ограничивает не набор framework-механизмов, а владение и наблюдаемую структуру:
- помимо опционального главного framework-файла в корне, остальные компонентные единицы располагаются на одном внутреннем уровне относительно модуля;
- каталог компонентной единицы не содержит другие компонентные единицы или вложенные модули;
- рекурсивная структурная вложенность создаётся только вложенными модулями;
- помимо публичных фасетов, в корне находится не более одного главного implementation- или assembly-файла;
- если главный файл нельзя определить уверенно, реализация размещается в сегментах.
Ограничение глубины framework-компонентов относится к файловой организации, а не к runtime-дереву фреймворка.
## Порядок проектирования ## Порядок проектирования
Архитектурное решение принимается от смысла к структуре: Архитектурное решение принимается от смысла к структуре:
1. Описать результат или поведение, за которое должен отвечать код. 1. Описать результат, который должен получить пользователь или приложение.
2. Определить одну причину изменения этой ответственности. 2. Назначить модуль, который отвечает за этот результат.
3. Найти связанные данные, поведение, состояние и жизненный цикл. 3. Определить, что модуль делает сам, а что получает от других модулей.
4. Назначить модуль владельцем и определить его внешних потребителей. 4. Определить, кто использует результат работы модуля.
5. Выбрать [слой](./layers.md) по роли ответственности. 5. Выбрать [слой](./layers.md) по роли ответственности.
6. Спроектировать публичный API и допустимые зависимости [модуля](./modules.md). 6. Спроектировать публичный API и допустимые [зависимости](./dependencies.md).
7. При необходимости организовать реализацию [сегментами](./segments.md). 7. При необходимости классифицировать модули [группами](./groups.md), организовать внутренний код [сегментами](./segments.md) и выбрать физические пути.
8. Только после этого выбрать физические пути и имена файлов.
Например, Header сам определяет расположение шапки, отображение навигации и состояние мобильного меню. Текущего пользователя он получает от модуля `auth`, а `Button` и `Avatar` — от модулей `ui`. Если удалить Header, авторизация и UI-компоненты останутся нужны приложению, поэтому Header использует их, но не владеет ими.
Если ответственность или владелец не определены, файловая структура не может исправить архитектурную неопределённость. Если ответственность или владелец не определены, файловая структура не может исправить архитектурную неопределённость.
## Логическая и физическая границы ## Логическая и физическая границы
Модуль не определяется наличием папки, `index.ts` или нескольких файлов. Его определяет самостоятельная ответственность и владение её контрактом, зависимостями, состоянием и жизненным циклом. Модуль не определяется наличием папки, `index.ts`, framework-компонента или нескольких файлов. Его определяет самостоятельная ответственность и владение её контрактом, зависимостями, состоянием и жизненным циклом.
После принятия решения модуль получает отдельную папку и публичные точки входа. Это обязательное физическое представление модульной границы, необходимое потребителям и автоматическим проверкам. После принятия решения модуль получает отдельную папку и публичные точки входа. Это обязательное физическое представление модульной границы, необходимое потребителям и автоматическим проверкам.
@@ -65,7 +82,7 @@ SLM root
- самостоятельная ответственность требует модульной границы; - самостоятельная ответственность требует модульной границы;
- отдельная папка сама по себе не доказывает наличие модуля. - отдельная папка сама по себе не доказывает наличие модуля.
Пути сопоставляются со слоями, группами, модулями и сегментами в стайлгайде или конфигурации конкретного проекта. Имя пути не меняет нормативный смысл сущности. Пути сопоставляются со слоями, группами, модулями, вложенными модулями и сегментами в стайлгайде или конфигурации конкретного проекта. Имя пути не меняет нормативный смысл сущности.
## Область применения ## Область применения
@@ -73,12 +90,14 @@ SLM применяется внутри **SLM root** — границы стру
Архитектура определяет: Архитектура определяет:
- роли слоёв и допустимые направления зависимостей; - роли слоёв и допустимые межслойные направления;
- владельцев самостоятельных ответственностей; - владельцев самостоятельных ответственностей;
- публичные границы модулей; - публичные границы модулей;
- общий ацикличный граф модулей;
- назначение групп и сегментов; - назначение групп и сегментов;
- внутреннюю глубину framework-компонентных единиц;
- владение состоянием и жизненным циклом ресурсов. - владение состоянием и жизненным циклом ресурсов.
SLM не задаёт обязательный поток данных, полный файловый стайлгайд, фиксированный набор сегментов, правила монорепозиториев или обязательную внутреннюю форму каждого модуля. SLM не задаёт обязательный поток данных, конкретный framework, фиксированные имена сегментов, правила монорепозиториев или полный файловый стайлгайд.
Нормативный смысл терминов находится в [терминологии](../reference/terminology.md). Точные блокирующие требования объявлены только в [реестре правил](../rules/registry.md). Нормативный смысл терминов находится в [терминологии](../reference/terminology.md). Точные блокирующие требования объявлены только в [реестре правил](../rules/registry.md).

View File

@@ -0,0 +1,102 @@
# Зависимости
Зависимость связывает архитектурных владельцев. Исходный файл создаёт ребро от своего ближайшего модуля к ближайшему модулю импортируемого файла.
## Модульный граф
Узлами архитектурного графа являются модули, включая вложенные. Группы, сегменты, framework-компоненты, hooks, stores и другие файлы реализации отдельных узлов не создают.
Для каждой связи определяются:
1. Ближайший модуль-владелец исходного файла.
2. Ближайший модуль-владелец целевого файла.
3. Слои исходного и целевого владельцев.
4. Публичный фасет, через который пересечена граница.
Обычный импорт, `import type` и реэкспорт одинаково создают архитектурное ребро. Связь файлов внутри одного модуля остаётся внутренней реализацией и не создаёт межмодульную зависимость.
Вложенный модуль начинает новый узел. Импорт из родительского модуля во вложенный или обратно проверяется как обычная межмодульная связь.
## Публичная граница
При пересечении модульной границы используется только объявленный публичный фасет целевого модуля:
```ts
// Допустимо
import { Button } from '@/ui/button'
// Недопустимый глубокий импорт
import { Button } from '@/ui/button/button'
```
Разрешённое направление слоя или отсутствие цикла не делает глубокий импорт допустимым.
## Направление между слоями
Матрица определяет, от каких слоёв может зависеть исходный слой:
| Исходный слой | Допустимые целевые слои |
|---|---|
| `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`.
Разрешённое направление не переносит владение. Если модуль `domains` использует `infra`, предметный сценарий остаётся ответственностью доменного модуля, а техническая возможность — ответственностью инфраструктурного.
## Зависимости внутри слоя
Модули одного слоя могут зависеть друг от друга в любом направлении при одновременном выполнении двух условий:
1. Целевой модуль используется только через публичный API.
2. Общий модульный граф остаётся ацикличным.
Принадлежность модулей одной или разным [группам](./groups.md) не влияет на разрешение связи. Группа не имеет API и не является промежуточным узлом импорта.
SLM не задаёт отдельные same-layer матрицы для `pages`, `layouts`, `widgets`, доменов, инфраструктуры, UI или shared. Если проекту нужна более строгая локальная политика, она является дополнительным проектным ограничением, а не общим правилом SLM.
## Запрет циклов
Общий граф модулей внутри одного SLM root остаётся ацикличным. Запрет действует для модулей одного слоя, разных разрешённых слоёв и вложенных модулей.
Проверки только файлового графа недостаточно. Например:
```text
module-a/file-1.ts → module-b/file-1.ts
module-b/file-2.ts → module-a/file-2.ts
```
Между конкретными файлами может не существовать замкнутого пути, но после сопоставления файлов владельцам возникает архитектурный цикл:
```text
module-a ↔ module-b
```
Lint-проверка модульных циклов должна:
1. Сопоставить каждый файл ближайшему модулю-владельцу.
2. Свернуть внутренние импорты файлов одного модуля.
3. Добавить межмодульные рёбра для импортов типов, исполняемого кода и реэкспортов.
4. Считать каждый вложенный модуль отдельным узлом.
5. Блокировать любое сильносвязное множество из нескольких модулей.
Стандартная file-level проверка циклов может использоваться дополнительно, но не заменяет проверку модульного графа.
## Проверка связи
Для каждого нового или изменённого импорта проверяются три условия:
1. Направление разрешено матрицей слоёв.
2. Целевая модульная граница пересечена через публичный фасет.
3. После добавления ребра модульный граф остаётся ацикличным.
## Связанные правила
- [`SLM-LAYER-A002`](../rules/registry.md#slm-layer-a002)
- [`SLM-MODULE-A004`](../rules/registry.md#slm-module-a004)
- [`SLM-DEPENDENCY-A005`](../rules/registry.md#slm-dependency-a005)
- [`SLM-NESTED_MODULE-A010`](../rules/registry.md#slm-nested_module-a010)

View File

@@ -0,0 +1,81 @@
# Группы
Группа является необязательным навигационным классификатором модулей внутри одного слоя. Она помогает ориентироваться в дереве владельцев, но не реализует ответственность и не создаёт архитектурную границу.
## Место в модели
Модуль может находиться непосредственно в слое или внутри одной или нескольких групп:
```text
слой
├── модуль
└── группа
├── модуль
└── группа
└── модуль
```
Группа создаётся, когда плоский список модулей перестаёт быть понятным. Названия и глубину групп определяет проект.
```text
compositions/
├── pages/ # Группа
│ ├── catalog/ # Модуль
│ └── profile/ # Модуль
├── layouts/ # Группа
│ └── main/ # Модуль
└── widgets/ # Группа
└── cart-summary/ # Модуль
```
Названия `pages`, `layouts` и `widgets` показывают один из вариантов навигации и не создают дополнительные слои или обязательные роли.
## Ограничения группы
Группа:
- содержит только модули и вложенные группы;
- не владеет файлами реализации;
- не имеет состояния или жизненного цикла;
- не предоставляет публичный API;
- не является узлом графа зависимостей;
- не импортируется внешним кодом;
- не реэкспортирует содержащиеся в ней модули.
Barrel-файл, открывающий несколько модулей группы как единый контракт, превращает каталог в новую модульную границу. Если такой контракт действительно нужен, для него определяется ответственность и создаётся обычный модуль.
## Группы и зависимости
Принадлежность модулей одной или разным группам не влияет на допустимость импорта. Модули одного слоя могут зависеть друг от друга через публичные API, если общий граф остаётся ацикличным.
Код импортирует конкретный модуль:
```ts
import { CartSummary } from '@/compositions/widgets/cart-summary'
```
Группа не становится промежуточной точкой доступа:
```ts
// Недопустимый API группы
import { CartSummary } from '@/compositions/widgets'
```
Полные правила графа находятся в разделе [Зависимости](./dependencies.md).
## Группа и сегмент
Группа организует несколько модулей внутри слоя. [Сегмент](./segments.md) организует код внутри одного модуля.
| Группа | Сегмент |
|---|---|
| Находится снаружи модульной границы | Находится внутри модульной границы |
| Содержит модули и группы | Содержит внутреннюю реализацию владельца |
| Не принадлежит одному модулю | Всегда принадлежит ближайшему модулю |
| Не содержит файлы реализации | Существует для организации файлов реализации |
## Связанные правила
- [`SLM-GROUP-R007`](../rules/registry.md#slm-group-r007)
- [`SLM-MODULE-R011`](../rules/registry.md#slm-module-r011)
- [`SLM-DEPENDENCY-A005`](../rules/registry.md#slm-dependency-a005)

View File

@@ -1,6 +1,6 @@
# Слои # Слои
Слой классифицирует код по архитектурной роли и задаёт допустимые направления зависимостей. Слой не владеет ответственностью: владельцем остаётся модуль, размещённый в этом слое. Слой классифицирует код по архитектурной роли. Слой не владеет ответственностью: владельцем остаётся модуль, размещённый в этом слое.
Слой выбирается после определения ответственности. Похожее имя папки или наличие зависимости от конкретной библиотеки не являются основанием для выбора слоя. Слой выбирается после определения ответственности. Похожее имя папки или наличие зависимости от конкретной библиотеки не являются основанием для выбора слоя.
@@ -23,13 +23,13 @@ SLM определяет шесть ролей:
`app` содержит точки связи с фреймворком: запуск, файлы маршрутов и преобразование внешних входных данных. Они подключают готовые публичные API других слоёв, но не присваивают их ответственность. `app` содержит точки связи с фреймворком: запуск, файлы маршрутов и преобразование внешних входных данных. Они подключают готовые публичные API других слоёв, но не присваивают их ответственность.
Точки входа `app` являются специальным немодульным исключением. Страница, макет, провайдер или другой самостоятельный продуктовый владелец реализуется в подходящем модуле и только подключается из `app`. Точки входа `app` являются специальным немодульным исключением. Самостоятельная продуктовая ответственность, даже если она представлена страницей, макетом или Provider, реализуется в подходящем модуле и только подключается из `app`.
### Compositions ### Compositions
`compositions` содержит владельцев продуктовых композиций: страниц, макетов, экранов, виджетов, результатов маршрутов и интерфейса, объединяющего несколько ответственностей. `compositions` содержит владельцев продуктовых композиций: страниц, макетов, экранов, виджетов, результатов маршрутов и интерфейса, объединяющего несколько ответственностей.
Конкретная организация слоя определяется продуктом и фреймворком. Названия `pages`, `layouts`, `screens` и `widgets` могут использоваться как группы, но не являются дополнительными слоями. Конкретная организация слоя определяется продуктом и фреймворком. Названия `pages`, `layouts`, `screens` и `widgets` могут использоваться как [группы](./groups.md), но не являются дополнительными слоями и не задают направление импортов.
### Domains ### Domains
@@ -55,52 +55,13 @@ SLM определяет шесть ролей:
## Направление зависимостей ## Направление зависимостей
Матрица определяет, от каких слоёв может зависеть исходный слой: Слой ограничивает только межслойное направление. Модули одного слоя могут зависеть друг от друга через публичные API, если общий граф остаётся ацикличным.
| Исходный слой | Допустимые целевые слои | Нормативная матрица, правила same-layer импортов и требования к lint-проверке находятся в разделе [Зависимости](./dependencies.md).
|---|---|
| `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#зависимости-между-модулями). Модули могут находиться непосредственно в слое или объединяться в необязательные навигационные [группы](./groups.md). Группа не владеет кодом и не влияет на допустимость зависимостей.
Разрешённое направление не переносит владение. Если модуль `domains` использует `infra`, предметный сценарий остаётся ответственностью доменного модуля, а техническая возможность — ответственностью инфраструктурного.
`infra` может использовать `ui`, когда технической возможности нужно собственное визуальное представление: CAPTCHA, uploader, карта или инструмент разработчика. `ui` не использует `infra`; необходимые технические возможности универсальный UI получает через входной контракт.
## Группировка модулей
Группа классифицирует модули внутри одного слоя или другой группы. Она нужна, когда плоский список модулей перестаёт быть понятным.
```text
compositions/
├── pages/ # Группа
│ ├── catalog/ # Модуль
│ └── profile/ # Модуль
├── layouts/ # Группа
│ └── main/ # Модуль
└── widgets/ # Группа
└── cart-summary/ # Модуль
```
Группа:
- содержит только модули и вложенные группы;
- не владеет ответственностью или реализацией;
- не имеет состояния и жизненного цикла;
- не предоставляет публичный API;
- не является узлом графа зависимостей;
- не реэкспортирует содержащиеся в ней модули.
Модуль может находиться непосредственно в слое. Группа вводится только ради реальной классификации, а её названия и глубину определяет проект.
Группа организует несколько владельцев внутри слоя. [Сегмент](./segments.md) организует код внутри одного владельца.
## Немодульные исключения ## Немодульные исключения

View File

@@ -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 ## Публичный API
@@ -54,7 +74,7 @@
|---|---| |---|---|
| `index` | Универсальные типы и исполняемый код, совместимый с серверным рендерингом, включая RSC, и клиентским выполнением | | `index` | Универсальные типы и исполняемый код, совместимый с серверным рендерингом, включая RSC, и клиентским выполнением |
| `client` | Клиентская граница фреймворка, участвующая в серверной предварительной отрисовке и выполняющаяся при гидратации и в браузере | | `client` | Клиентская граница фреймворка, участвующая в серверной предварительной отрисовке и выполняющаяся при гидратации и в браузере |
| `browser` | Код только для браузера, подключаемый динамически через границу с отключённым SSR | | `browser` | Browser-only и динамически подключаемый клиентский код; фасет всегда импортируется динамически с отключённым SSR |
| `server` | Код только для сервера, недоступный универсальной и клиентской среде выполнения | | `server` | Код только для сервера, недоступный универсальной и клиентской среде выполнения |
`index` обязателен. Остальные фасеты создаются только при наличии реального потребителя и несовместимых требований к среде. `index` обязателен. Остальные фасеты создаются только при наличии реального потребителя и несовместимых требований к среде.
@@ -74,11 +94,9 @@ auth/
Эта файловая форма представляет уже определённую публичную границу. Наличие `index.ts` само по себе не создаёт модуль. Эта файловая форма представляет уже определённую публичную границу. Наличие `index.ts` само по себе не создаёт модуль.
## Зависимости между модулями ## Зависимости
Зависимость связывает владельца исходного кода с владельцем импортируемого кода. Импорт внутреннего файла, сегмента или компонента относится к ближайшему модулю-владельцу. Вложенный модуль начинает собственную границу зависимостей. Модули одного слоя могут зависеть друг от друга. При пересечении любой модульной границы код использует публичный фасет целевого модуля, а общий граф модулей должен оставаться ацикличным.
При пересечении модульной границы код использует только публичный фасет целевого модуля:
```ts ```ts
// Допустимо // Допустимо
@@ -88,56 +106,115 @@ import { Button } from '@/ui/button'
import { Button } from '@/ui/button/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 ```text
button-submit/ header/header.tsx
├── button-submit.tsx footer/footer.tsx
├── styles/ auth-guard/auth-guard.provider.tsx
│ └── button-submit.module.css
├── types/
│ └── button-submit.types.ts
└── index.ts
``` ```
Такой `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 родителя. Код родительского модуля использует вложенный модуль через его публичный API. Код за пределами родительской границы не импортирует вложенный модуль напрямую и получает необходимые возможности через API родителя.
Если вложенный модуль становится нужен нескольким внешним владельцам, его можно перенести в минимальную общую область. Сам факт повторного использования внутри родителя переноса не требует. Вложенный модуль может рекурсивно содержать другие вложенные модули. Именно модульная граница, а не каталог компонента, создаёт каждый следующий структурный уровень. Если вложенный модуль становится нужен нескольким внешним владельцам, его переносят в минимальную общую область.
## Колокация и рост
Код создаётся в самой узкой допустимой области внутри своего архитектурного владельца:
1. Вспомогательный код, нужный одной компонентной единице, колоцируется в её файле или каталоге.
2. Каждая выделенная framework-компонентная единица размещается на общем внутреннем уровне модуля, даже если используется только одним другим компонентом.
3. Код, нужный нескольким внутренним единицам, поднимается в их ближайший общий сегмент.
4. При появлении самостоятельной подответственности вокруг кода создаётся вложенный модуль.
5. Вложенный модуль переносится в общую область, когда прямой доступ к нему требуется внешним владельцам.
Расширение числа потребителей меняет колокацию внутри владельца. Появление самостоятельной ответственности меняет модульный граф.
## Состояние и жизненный цикл ## Состояние и жизненный цикл
Состояние принадлежит модулю, ответственность которого определяет его смысл, допустимые изменения и область жизни. Место хранения состояния не переносит владение в файл хранилища, провайдер или контекст фреймворка. Состояние принадлежит модулю, ответственность которого определяет его смысл, допустимые изменения и область жизни. Место хранения состояния не переносит владение в store, Provider или Context.
Для каждого долгоживущего ресурса модуль-владелец определяет: Для каждого долгоживущего ресурса модуль-владелец определяет:
@@ -147,18 +224,10 @@ button-submit/
- допустимое число экземпляров; - допустимое число экземпляров;
- способ остановки, отмены или освобождения. - способ остановки, отмены или освобождения.
Подписка, обработчик событий, таймер, наблюдатель, запрос или соединение не остаются активными после завершения своей области жизни. Очистку может технически выполнить компонент, провайдер или фреймворк, но ответственность за корректную границу остаётся у модуля. Подписка, обработчик событий, таймер, наблюдатель, запрос или соединение не остаются активными после завершения своей области жизни. Очистку может технически выполнить framework-компонент или сам фреймворк, но ответственность за корректную границу остаётся у модуля.
Размещение экземпляра на уровне файла не доказывает область жизни всего приложения. Точка входа `app` может запустить ресурс через публичный API модуля, но не становится его владельцем. Размещение экземпляра на уровне файла не доказывает область жизни всего приложения. Точка входа `app` может запустить ресурс через публичный API модуля, но не становится его владельцем.
## Внутренняя организация
После определения ответственности и публичной границы модуль может организовать реализацию [сегментами](./segments.md), компонентами и вложенными модулями. SLM не требует полного каркаса и не задаёт обязательный набор внутренних папок.
Каждый модуль физически размещается в отдельной папке. Это делает логическую границу наблюдаемой для потребителей и автоматических проверок, но не заменяет решение о владельце.
Наличие `index.ts` не используется как единственный признак модуля. Модульную границу определяют ответственность, владелец, публичные потребители и сопоставление путей, принятое проектом.
## Связанные правила ## Связанные правила
- [`SLM-MODULE-A004`](../rules/registry.md#slm-module-a004) - [`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-R006`](../rules/registry.md#slm-module-r006)
- [`SLM-MODULE-R011`](../rules/registry.md#slm-module-r011) - [`SLM-MODULE-R011`](../rules/registry.md#slm-module-r011)
- [`SLM-MODULE-R012`](../rules/registry.md#slm-module-r012) - [`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-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-NESTED_MODULE-A010`](../rules/registry.md#slm-nested_module-a010)
- [`SLM-LIFECYCLE-R013`](../rules/registry.md#slm-lifecycle-r013) - [`SLM-LIFECYCLE-R013`](../rules/registry.md#slm-lifecycle-r013)
- [`SLM-ENVIRONMENT-R016`](../rules/registry.md#slm-environment-r016) - [`SLM-ENVIRONMENT-R016`](../rules/registry.md#slm-environment-r016)

View File

@@ -10,28 +10,27 @@
Слой → [Группа*] → Модуль → [Сегмент*] Слой → [Группа*] → Модуль → [Сегмент*]
``` ```
Группа классифицирует несколько модулей внутри слоя. Сегмент классифицирует код одного модуля. Ни группа, ни сегмент не являются владельцами. Группа классифицирует модули внутри слоя. Сегмент классифицирует код одного модуля. Ни группа, ни сегмент не являются владельцами.
Все файлы, компоненты, состояние, зависимости и код жизненного цикла сегмента принадлежат родительскому модулю. Исключением является только вложенный модуль, который начинает собственную границу владения. Все файлы, framework-компоненты, состояние, зависимости и lifecycle-код сегмента принадлежат ближайшему модулю. Исключением является только вложенный модуль, который начинает собственную границу владения.
## Назначение ## Назначение
Сегмент используется, когда группировка внутренних файлов по назначению упрощает навигацию. Набор и названия сегментов определяет проект. Сегмент используется, когда группировка внутренних файлов по назначению упрощает навигацию. Набор и названия сегментов определяет проект.
Возможная структура:
```text ```text
profile/ profile/
├── index.ts ├── index.ts # Публичный фасет
├── profile.tsx ├── profile.tsx # Главная реализация
├── components/ # Возможный сегмент
├── hooks/ # Возможный сегмент ├── hooks/ # Возможный сегмент
├── services/ # Возможный сегмент ├── services/ # Возможный сегмент
├── stores/ # Возможный сегмент ├── stores/ # Возможный сегмент
├── types/ # Возможный сегмент ├── types/ # Возможный сегмент
└── ui/ # Возможный сегмент └── styles/ # Возможный сегмент
``` ```
Ни один из показанных сегментов не обязателен. Маленький модуль может хранить реализацию в корне без дополнительных каталогов. Ни один сегмент не создаётся заранее. Модуль может обойтись без сегментов, если помимо публичных фасетов содержит только один главный implementation- или assembly-файл, однозначно выражающий его ответственность. Любая остальная реализация размещается в подходящих сегментах. Если главный файл нельзя определить уверенно, вся реализация остаётся в сегментах.
Сегмент: Сегмент:
@@ -41,52 +40,67 @@ profile/
- не является узлом графа зависимостей; - не является узлом графа зависимостей;
- не импортируется внешним кодом как отдельная архитектурная сущность. - не импортируется внешним кодом как отдельная архитектурная сущность.
Локальный `index.ts` может использоваться во внутренней единице сегмента, например в каталоге компонента. Он не превращает эту единицу или сегмент в модульную границу. Локальный `index.ts` может использоваться во внутренней единице сегмента. Он не превращает эту единицу или сегмент в модульную границу.
## Компоненты и вложенные модули ## Framework-компоненты
Сегмент может содержать компоненты и вспомогательные файлы родительского модуля. Компонент вправе иметь локальные `styles/`, `types/`, `tests/` и внутренний `index.ts`; всё это остаётся реализацией ближайшего модуля. Framework-компоненты являются обычным внутренним кодом модуля. Они могут выполнять визуальные и невизуальные роли, включая Provider, Guard или Error Boundary, если используемый фреймворк считает соответствующую сущность компонентом.
Помимо опционального главного framework-файла в корне, остальные компонентные единицы размещаются на одном внутреннем уровне относительно модуля. Каталог такой единицы может содержать локальные `styles`, `types`, `hooks`, `tests` и внутренний `index.ts`, но не содержит другие компонентные единицы или вложенные модули.
```text ```text
header/ # Модуль header/ # Модуль
├── index.ts # Публичный фасет
├── header.tsx # Главная реализация
└── components/ # Сегмент └── components/ # Сегмент
── button-submit/ # Компонент ── button-submit/
├── button-submit.tsx ├── index.ts # Локальная точка входа
├── styles/ ├── button-submit.tsx
── button-submit.module.css ── styles/
├── types/ ├── types/
│ └── button-submit.types.ts │ └── hooks/
└── index.ts # Внутренняя точка входа └── icon.tsx # Соседняя компонентная единица
``` ```
`ButtonSubmit` может рендерить `Icon`, но их файловые области не вкладываются друг в друга. Ограничение относится к файловой структуре, а не к runtime-дереву.
Внешний код не импортирует `components/button-submit`. Если компонент нужен снаружи, модуль-владелец реэкспортирует его через собственный публичный фасет. Внешний код не импортирует `components/button-submit`. Если компонент нужен снаружи, модуль-владелец реэкспортирует его через собственный публичный фасет.
Сегмент также может содержать вложенные модули. В отличие от остальных файлов сегмента, каждый вложенный модуль сам владеет отдельной ответственностью и имеет публичный API и границу зависимостей. ## Вложенные модули
Сегмент может содержать вложенные модули. В отличие от остальных файлов сегмента, каждый вложенный модуль владеет отдельно сформулированной подответственностью и имеет публичный API и границу зависимостей.
```text ```text
landing/ # Родительский модуль landing/ # Родительский модуль
└── parts/ # Сегмент └── modules/ # Сегмент
└── hero/ # Вложенный модуль └── hero/ # Вложенный модуль
├── hero.tsx ├── index.ts # Публичный фасет вложенного модуля
├── hero.tsx # Главная реализация hero
└── modules/ # Допустимая модульная рекурсия
└── media/
└── index.ts └── index.ts
``` ```
Имя `parts` является примером, а не обязательным соглашением SLM. Компонентный каталог не содержит `components` или `modules`. Рекурсивная структурная вложенность допускается только через вложенные модули. Имена `components` и `modules` являются примерами локального стайлгайда, а не обязательными соглашениями SLM.
## Выбор границы ## Выбор размещения
| Ситуация | Решение | | Ситуация | Решение |
|---|---| |---|---|
| Код относится к существующему владельцу и группируется только по назначению | Сегмент | | Код относится к существующему владельцу и группируется по назначению | Сегмент |
| Части нужна собственная ответственность, API, зависимости, состояние или область жизни | Модуль | | Вспомогательный код нужен только одной компонентной единице | Колоцировать в её локальном каталоге |
| Несколько модулей слоя нужно классифицировать для навигации | Группа | | Выделена отдельная framework-компонентная единица | Разместить на общем внутреннем уровне модуля |
| Нескольким файлам не нужна отдельная группировка | Оставить в корне модуля | | Код нужен нескольким внутренним единицам модуля | Поднять в ближайший общий сегмент |
| Появилась самостоятельная связная подответственность | Создать вложенный модуль |
| Файл однозначно является главной реализацией или сборкой ответственности | Допустимо разместить в корне модуля |
| Файл не является главным или его роль неоднозначна | Разместить в подходящем сегменте |
Размер каталога и количество файлов не определяют выбор между сегментом и модулем. Размер каталога и количество файлов не определяют модульную границу. Её создаёт только самостоятельная ответственность и назначение нового владельца.
## Связанные правила ## Связанные правила
- [`SLM-SEGMENT-R008`](../rules/registry.md#slm-segment-r008) - [`SLM-SEGMENT-R008`](../rules/registry.md#slm-segment-r008)
- [`SLM-MODULE-A004`](../rules/registry.md#slm-module-a004) - [`SLM-MODULE-A004`](../rules/registry.md#slm-module-a004)
- [`SLM-COMPONENT-R009`](../rules/registry.md#slm-component-r009) - [`SLM-MODULE-R020`](../rules/registry.md#slm-module-r020)
- [`SLM-MODULE-R021`](../rules/registry.md#slm-module-r021)
- [`SLM-NESTED_MODULE-A010`](../rules/registry.md#slm-nested_module-a010) - [`SLM-NESTED_MODULE-A010`](../rules/registry.md#slm-nested_module-a010)

View File

@@ -10,11 +10,11 @@
### Ответственность ### Ответственность
Связная часть приложения с одной причиной изменяться. Ответственность является самостоятельной, когда ей нужны собственный публичный API, зависимости, состояние или область жизни. Результат или поведение приложения, за которое отвечает один модуль-владелец. Ответственность является самостоятельной, когда ей нужны собственный публичный контракт, зависимости, состояние или область жизни, а не только внутренняя роль в работе другого модуля. Наличие у framework-сущности props, импортов, локального состояния или lifecycle-кода само по себе не создаёт самостоятельную ответственность.
### Владелец ### Владелец
Модуль, который определяет публичный API ответственности, её зависимости, состояние, область жизни и внутреннюю реализацию. Место выполнения кода не переносит владение. Модуль, который определяет публичный API ответственности, её зависимости, состояние, область жизни и внутреннюю реализацию. Каждый файл принадлежит ближайшей модульной границе и реализует ответственность этого модуля. Место выполнения кода или вид framework-сущности не переносит владение.
## Структурные сущности ## Структурные сущности
@@ -34,13 +34,11 @@
Необязательная внутренняя часть одного модуля, группирующая его содержимое по назначению. Сегмент не является владельцем, публичным API или границей зависимостей. Необязательная внутренняя часть одного модуля, группирующая его содержимое по назначению. Сегмент не является владельцем, публичным API или границей зависимостей.
### Компонент
Сущность фреймворка, реализующая часть интерфейса родительского модуля. Зависимости, состояние и жизненный цикл компонента принадлежат этому модулю и сами по себе не создают нового владельца. Компонент может иметь внутренний `index.ts`, который не является публичным фасетом SLM.
### Вложенный модуль ### Вложенный модуль
Обычный модуль, физически размещённый внутри родительского модуля. Он сам владеет отдельной ответственностью и имеет публичный API и границу зависимостей, но остаётся внутренней реализацией родителя для внешнего кода. Обычный модуль, физически размещённый внутри родительского модуля. Он владеет отдельно сформулированной связной частью ответственности родителя, имеет публичный API и собственную границу зависимостей. Родитель владеет общим результатом, а вложенный модуль — выделенной подответственностью; одна и та же ответственность не получает двух владельцев.
Для кода за пределами родительской границы вложенный модуль остаётся внутренней реализацией родителя. Внутри вложенного модуля снова действуют все правила обычного модуля, поэтому рекурсивная структурная вложенность создаётся только модульными границами.
## Публичная граница ## Публичная граница
@@ -62,7 +60,7 @@
Направленная статическая связь между архитектурными границами внутри одного SLM root. Обычный импорт, импорт типа (`import type`) и реэкспорт одинаково создают архитектурную зависимость. Направленная статическая связь между архитектурными границами внутри одного SLM root. Обычный импорт, импорт типа (`import type`) и реэкспорт одинаково создают архитектурную зависимость.
Зависимость внутреннего файла, сегмента или компонента относится к ближайшему модулю-владельцу. Вложенный модуль начинает собственную границу зависимостей. Зависимость внутреннего файла или сегмента относится к ближайшему модулю-владельцу. Вложенный модуль начинает собственную границу зависимостей.
### Нормативная матрица слоёв ### Нормативная матрица слоёв

View File

@@ -28,14 +28,18 @@
На ревью проверяется смысл кода, который нельзя надёжно вывести из файловой системы: На ревью проверяется смысл кода, который нельзя надёжно вывести из файловой системы:
- одна ли связная ответственность находится внутри модуля; - одна ли связная ответственность находится внутри модульной границы;
- есть ли у каждой самостоятельной ответственности ровно один владелец; - есть ли у каждой самостоятельной ответственности ровно один ближайший владелец;
- соответствует ли ответственность роли выбранного слоя; - соответствует ли ответственность роли выбранного слоя;
- не стали ли группа, сегмент или компонент скрытыми владельцами; - владеет ли вложенный модуль отдельно сформулированной подответственностью;
- не стали ли группа или сегмент скрытыми владельцами;
- реализует ли внутренний код ответственность ближайшего модуля;
- не принимаются ли props, Context, локальное состояние или lifecycle-код за достаточное основание для новой модульной границы;
- является ли главный файл в корне однозначной реализацией или сборкой ответственности;
- не лежат ли прочие файлы реализации в корне вместо подходящих сегментов;
- нужен ли каждый экспорт реальному внешнему потребителю; - нужен ли каждый экспорт реальному внешнему потребителю;
- не раскрывает ли публичный API изменяемые внутренние механизмы; - не раскрывает ли публичный API изменяемые внутренние механизмы;
- определены ли владелец, область жизни, число экземпляров и очистка каждого ресурса; - определены ли владелец, область жизни, число экземпляров и очистка каждого ресурса.
- не переносится ли владение из-за места вызова, провайдера фреймворка или точки маршрута.
Окончательные смысловые требования имеют класс `R` в [реестре правил](../rules/registry.md). Окончательные смысловые требования имеют класс `R` в [реестре правил](../rules/registry.md).
@@ -43,14 +47,14 @@
Проект сопоставляет физические пути с SLM root, слоями, группами, модулями, вложенными модулями, сегментами, фасетами, точками входа `app` и ресурсами `shared`. Сопоставление задаётся локальной конфигурацией и не меняет смысл сущностей. Проект сопоставляет физические пути с SLM root, слоями, группами, модулями, вложенными модулями, сегментами, фасетами, точками входа `app` и ресурсами `shared`. Сопоставление задаётся локальной конфигурацией и не меняет смысл сущностей.
Наличие `index.ts` само по себе не объявляет модуль. Проверка отличает объявленные корни модулей и их публичные фасеты от локальных точек входа компонентов и других внутренних единиц. Наличие `index.ts` само по себе не объявляет модуль. Проверка отличает объявленные корни модулей и их публичные фасеты от локальных точек входа и других внутренних единиц.
Автоматически проверяются: Автоматически проверяются:
- допустимое направление импортов по матрице слоёв; - допустимое направление импортов по матрице слоёв;
- отдельная папка каждого модуля; - отдельная папка каждого модуля;
- доступ к чужому модулю только через объявленные фасеты; - доступ к чужому модулю только через объявленные фасеты;
- отсутствие циклов между модулями; - отсутствие циклов в свёрнутом модульном графе;
- отсутствие прямого внешнего доступа к вложенным модулям; - отсутствие прямого внешнего доступа к вложенным модулям;
- допустимый транзитивный граф исполняемых импортов каждого фасета среды выполнения; - допустимый транзитивный граф исполняемых импортов каждого фасета среды выполнения;
- динамическое подключение `browser`-фасета с отключённым SSR. - динамическое подключение `browser`-фасета с отключённым SSR.
@@ -61,13 +65,26 @@
Для каждого внешнего импорта определяется: Для каждого внешнего импорта определяется:
1. Модуль-владелец исходного файла. 1. Ближайший модуль-владелец исходного файла.
2. Модуль-владелец целевого файла. 2. Ближайший модуль-владелец целевого файла.
3. Слои исходного и целевого владельцев. 3. Слои исходного и целевого владельцев.
4. Публичный фасет, через который выполнен импорт. 4. Публичный фасет, через который выполнен импорт.
5. Отсутствие цикла после добавления связи. 5. Отсутствие цикла после добавления межмодульного ребра.
Импорты типов (`import type`) и реэкспорты проверяются как архитектурные связи. Относительные импорты внутри одного модуля не пересекают модульную границу. Импорты типов (`import type`) и реэкспорты проверяются как архитектурные связи. Импорты файлов одного модуля сворачиваются и не создают межмодульного ребра. Вложенный модуль считается отдельным узлом.
File-level проверка не заменяет модульную: два модуля могут зависеть друг от друга через разные файлы без замкнутого пути между конкретными файлами. Полный алгоритм описан в разделе [Зависимости](../architecture/dependencies.md#запрет-циклов).
## Проверка внутренней структуры
Ревью или дополнительный project lint подтверждают:
- помимо опционального главного framework-файла, остальные компонентные единицы размещены на одном внутреннем уровне модуля;
- их локальные каталоги не содержат другие компонентные единицы или вложенные модули;
- runtime-дерево компонентов не используется как файловая иерархия;
- рекурсивная структурная вложенность проходит только через вложенные модули;
- в корне модуля находятся только фасеты и опциональный главный implementation- или assembly-файл;
- остальная реализация организована сегментами.
## Проверка фасетов ## Проверка фасетов
@@ -78,6 +95,7 @@
- `index` не достигает `client`, `browser` или `server`; - `index` не достигает `client`, `browser` или `server`;
- `client` не достигает `browser` или `server`; - `client` не достигает `browser` или `server`;
- `browser` и `server` не достигают друг друга; - `browser` и `server` не достигают друг друга;
- `browser` экспортирует только browser-only или предназначенный для динамического подключения клиентский код;
- `browser` доступен только через поддерживаемую динамическую границу без SSR; - `browser` доступен только через поддерживаемую динамическую границу без SSR;
- специализированный фасет существует ради реального потребителя; - специализированный фасет существует ради реального потребителя;
- один исполняемый экспорт не дублируется между фасетами. - один исполняемый экспорт не дублируется между фасетами.
@@ -88,11 +106,15 @@
Изменение соответствует SLM, когда одновременно выполнены условия: Изменение соответствует SLM, когда одновременно выполнены условия:
- ответственность и единственный владелец определены; - ответственность и единственный ближайший владелец определены;
- роль слоя соответствует ответственности; - роль слоя соответствует ответственности;
- публичный API минимален и используется всеми внешними потребителями; - публичный API минимален и используется всеми внешними потребителями;
- зависимости разрешены и не образуют циклов; - зависимости разрешены и не образуют модульных циклов;
- группа и сегменты не подменяют модульную границу; - группа и сегменты не подменяют модульную границу;
- вложенные модули владеют отдельными подответственностями;
- внутренний код реализует ответственность ближайшего модуля;
- framework-компоненты имеют одноуровневую файловую организацию с единственным допустимым исключением для главного файла в корне;
- корень и сегменты соответствуют своим назначениям;
- состояние и ресурсы имеют владельца и корректную область жизни; - состояние и ресурсы имеют владельца и корректную область жизни;
- физическая структура однозначно выражает принятое решение; - физическая структура однозначно выражает принятое решение;
- применимые автоматические проверки и архитектурное ревью пройдены. - применимые автоматические проверки и архитектурное ревью пройдены.

View File

@@ -41,11 +41,10 @@ SLM-{group}-{class}{number}
| Код | Предмет | | Код | Предмет |
|---|---| |---|---|
| `LAYER` | Роль слоя и направление зависимостей | | `LAYER` | Роль слоя и направление зависимостей |
| `MODULE` | Ответственность, владение и публичная граница модуля | | `MODULE` | Ответственность, владение, публичная граница и внутренняя структура модуля |
| `DEPENDENCY` | Граф зависимостей модулей | | `DEPENDENCY` | Граф зависимостей модулей |
| `GROUP` | Навигационная группировка модулей | | `GROUP` | Навигационная группировка модулей |
| `SEGMENT` | Внутренняя организация модуля | | `SEGMENT` | Внутренняя организация модуля |
| `COMPONENT` | Принадлежность компонента модулю |
| `NESTED_MODULE` | Доступ к вложенному модулю | | `NESTED_MODULE` | Доступ к вложенному модулю |
| `LIFECYCLE` | Владение долгоживущими ресурсами | | `LIFECYCLE` | Владение долгоживущими ресурсами |
| `ENVIRONMENT` | Совместимость публичных фасетов со средами выполнения | | `ENVIRONMENT` | Совместимость публичных фасетов со средами выполнения |

View File

@@ -40,13 +40,13 @@
> **Ответственность модуля** > **Ответственность модуля**
> >
> Одна модульная граница содержит код одной связной ответственности; части, которые изменяются по несвязанным причинам, размещаются в разных модулях. > Одна модульная граница содержит только код, который модуль выполняет сам для обеспечения одного результата или поведения; код, отвечающий за другой результат, принадлежит другой модульной границе.
### SLM-MODULE-R011 ### SLM-MODULE-R011
> **Владелец ответственности** > **Владелец ответственности**
> >
> Каждая самостоятельная ответственность и относящийся к ней код принадлежат ровно одному модулю; вне модульной границы допускаются только точки входа `app` и нормативные ресурсы `shared`. > Каждая самостоятельная ответственность и относящийся к ней код принадлежат ровно одной ближайшей модульной границе; вне модульной границы допускаются только точки входа `app` и нормативные ресурсы `shared`.
### SLM-MODULE-R012 ### SLM-MODULE-R012
@@ -54,6 +54,18 @@
> >
> Публичный API модуля открывает только контракт, необходимый реальным внешним потребителям; детали реализации и изменяемые внутренние механизмы остаются закрытыми. > Публичный API модуля открывает только контракт, необходимый реальным внешним потребителям; детали реализации и изменяемые внутренние механизмы остаются закрытыми.
### SLM-MODULE-R020
> **Глубина framework-компонентов**
>
> Помимо опционального главного framework-файла в корне модуля, остальные framework-компоненты, в том числе выполняющие роли Provider, Guard или Error Boundary, размещаются на одном внутреннем уровне относительно модуля; каталог такой единицы может содержать локальный вспомогательный код, но не содержит другие компонентные единицы или вложенные модули.
### SLM-MODULE-R021
> **Семантика корня модуля**
>
> Помимо объявленных публичных фасетов, в корне модуля допускается только один опциональный главный implementation- или assembly-файл, который однозначно отражает, непосредственно реализует или собирает ответственность модуля; вся остальная реализация размещается в сегментах.
## Зависимости между модулями ## Зависимости между модулями
### SLM-DEPENDENCY-A005 ### SLM-DEPENDENCY-A005
@@ -78,14 +90,6 @@
> >
> Сегмент организует код только внутри одного модуля и не имеет собственной ответственности, публичного API или границы зависимостей. > Сегмент организует код только внутри одного модуля и не имеет собственной ответственности, публичного API или границы зависимостей.
## Ответственность компонентов
### SLM-COMPONENT-R009
> **Ответственность компонента**
>
> Компонент реализует часть ответственности одного родительского модуля; все его зависимости, состояние и жизненный цикл принадлежат этому модулю и не образуют самостоятельную архитектурную границу.
## Границы вложенных модулей ## Границы вложенных модулей
### SLM-NESTED_MODULE-A010 ### SLM-NESTED_MODULE-A010
@@ -120,7 +124,7 @@
> **Браузерный фасет** > **Браузерный фасет**
> >
> Фасет `browser` экспортирует только browser-only код, а потребители импортируют его только динамически с отключённым SSR. > Фасет `browser` экспортирует browser-only код и клиентский код, предназначенный для динамического подключения без SSR; потребители всегда импортируют его динамически с отключённым SSR.
### SLM-ENVIRONMENT-R019 ### SLM-ENVIRONMENT-R019

View File

@@ -13,6 +13,8 @@ const ruleRegistries = [
const expectedPages = [ const expectedPages = [
'404.html', '404.html',
'index.html', 'index.html',
'architecture/dependencies.html',
'architecture/groups.html',
'architecture/index.html', 'architecture/index.html',
'architecture/layers.html', 'architecture/layers.html',
'architecture/modules.html', 'architecture/modules.html',
@@ -138,7 +140,7 @@ if (!notFoundHtml.includes('Такой страницы нет')) {
} }
const homeHtml = htmlByPage.get('index.html') const homeHtml = htmlByPage.get('index.html')
if (!homeHtml.includes('Архитектура владения ответственностями')) { if (!homeHtml.includes('Архитектура фронтенд-приложений')) {
throw new Error('Home page does not render the documentation-owned hero') throw new Error('Home page does not render the documentation-owned hero')
} }
@@ -163,8 +165,6 @@ for (const forbiddenRoute of [
'/ru/', '/ru/',
'/specification/', '/specification/',
'/architecture/domains', '/architecture/domains',
'/architecture/dependencies',
'/architecture/groups',
'/architecture/components', '/architecture/components',
'/architecture/nested-modules', '/architecture/nested-modules',
'/architecture/lifecycle', '/architecture/lifecycle',

View File

@@ -10,9 +10,11 @@ const documentationSidebar = [
text: 'Архитектурная модель', text: 'Архитектурная модель',
items: [ items: [
{ text: 'Владение и структура', link: '/architecture/' }, { text: 'Владение и структура', link: '/architecture/' },
{ text: 'Слои и группы', link: '/architecture/layers' }, { text: 'Слои', link: '/architecture/layers' },
{ text: 'Группы', link: '/architecture/groups' },
{ text: 'Модули и границы', link: '/architecture/modules' }, { text: 'Модули и границы', link: '/architecture/modules' },
{ text: 'Сегменты', link: '/architecture/segments' }, { text: 'Сегменты', link: '/architecture/segments' },
{ text: 'Зависимости', link: '/architecture/dependencies' },
], ],
}, },
{ {

View File

@@ -49,10 +49,16 @@ html {
letter-spacing: -0.045em; letter-spacing: -0.045em;
} }
.VPHero .text {
max-width: 820px;
font-size: clamp(2.6rem, 5vw, 4.4rem);
line-height: 1.02;
}
.VPHero .tagline { .VPHero .tagline {
max-width: 660px; max-width: 760px;
font-size: 19px; font-size: 20px;
line-height: 1.65; line-height: 1.6;
} }
.VPFeature { .VPFeature {
@@ -61,6 +67,113 @@ html {
backdrop-filter: blur(8px); backdrop-filter: blur(8px);
} }
.VPFeature .title {
font-size: 17px;
letter-spacing: -0.02em;
}
.VPFeature .details {
line-height: 1.65;
}
.VPContent.is-home .vp-doc {
max-width: 1120px;
padding-bottom: 112px;
}
.VPContent.is-home .vp-doc > h2 {
max-width: 860px;
margin-top: 96px;
padding-top: 0;
border-top: 0;
font-size: clamp(2rem, 4vw, 3.25rem);
line-height: 1.08;
letter-spacing: -0.045em;
}
.VPContent.is-home .vp-doc > h2:first-child {
margin-top: 48px;
}
.VPContent.is-home .vp-doc > p {
max-width: 820px;
font-size: 17px;
line-height: 1.78;
}
.VPContent.is-home .vp-doc > blockquote {
max-width: 920px;
margin: 40px 0;
padding: 28px 32px;
border: 1px solid color-mix(in srgb, var(--vp-c-brand-2) 36%, var(--vp-c-divider));
border-left: 4px solid var(--vp-c-brand-2);
border-radius: 0 14px 14px 0;
background: color-mix(in srgb, var(--vp-c-brand-soft) 62%, var(--vp-c-bg-soft));
color: var(--vp-c-text-1);
font-size: clamp(1.25rem, 2.5vw, 1.75rem);
line-height: 1.45;
letter-spacing: -0.025em;
}
.VPContent.is-home .vp-doc > blockquote p {
margin: 0;
}
.VPContent.is-home .vp-doc table {
display: table;
width: 100%;
margin: 36px 0;
border-collapse: separate;
border-spacing: 0;
overflow: hidden;
border: 1px solid var(--vp-c-divider);
border-radius: 14px;
}
.VPContent.is-home .vp-doc th,
.VPContent.is-home .vp-doc td {
width: 50%;
padding: 18px 20px;
border: 0;
border-bottom: 1px solid var(--vp-c-divider);
vertical-align: top;
}
.VPContent.is-home .vp-doc th {
background: var(--vp-c-bg-soft);
color: var(--vp-c-text-1);
font-size: 14px;
letter-spacing: 0.04em;
text-transform: uppercase;
}
.VPContent.is-home .vp-doc tr:last-child td {
border-bottom: 0;
}
.VPContent.is-home .vp-doc th + th,
.VPContent.is-home .vp-doc td + td {
border-left: 1px solid var(--vp-c-divider);
}
.VPContent.is-home .vp-doc ol,
.VPContent.is-home .vp-doc ul {
max-width: 860px;
}
.VPContent.is-home .vp-doc li {
margin: 10px 0;
line-height: 1.7;
}
.VPContent.is-home .vp-doc div[class*='language-'] {
max-width: 920px;
margin: 36px 0;
border: 1px solid var(--vp-c-divider);
border-radius: 14px;
box-shadow: 0 24px 64px rgba(41, 37, 36, 0.08);
}
.vp-doc h1, .vp-doc h1,
.vp-doc h2, .vp-doc h2,
.vp-doc h3 { .vp-doc h3 {
@@ -128,10 +241,37 @@ html {
} }
@media (max-width: 640px) { @media (max-width: 640px) {
.VPHero .text {
font-size: clamp(2.15rem, 11vw, 3rem);
line-height: 1.06;
}
.VPHero .tagline { .VPHero .tagline {
font-size: 16px; font-size: 16px;
} }
.VPContent.is-home .vp-doc {
padding-bottom: 72px;
}
.VPContent.is-home .vp-doc > h2 {
margin-top: 72px;
}
.VPContent.is-home .vp-doc > blockquote {
margin-inline: 0;
padding: 22px 20px;
}
.VPContent.is-home .vp-doc table {
font-size: 14px;
}
.VPContent.is-home .vp-doc th,
.VPContent.is-home .vp-doc td {
padding: 14px 12px;
}
.vp-doc h3[id^='slm-'] + blockquote { .vp-doc h3[id^='slm-'] + blockquote {
margin-inline: -8px; margin-inline: -8px;
padding: 42px 14px 14px; padding: 42px 14px 14px;

View File

@@ -8,9 +8,11 @@
- `/` - главная страница; - `/` - главная страница;
- `/architecture/` - владение и структурная модель; - `/architecture/` - владение и структурная модель;
- `/architecture/layers` - слои и группы; - `/architecture/layers` - слои;
- `/architecture/groups` - группы модулей;
- `/architecture/modules` - модули и публичные границы; - `/architecture/modules` - модули и публичные границы;
- `/architecture/segments` - внутренняя организация модулей; - `/architecture/segments` - внутренняя организация модулей;
- `/architecture/dependencies` - направления импортов и модульный граф;
- `/rules/` - устройство правил; - `/rules/` - устройство правил;
- `/rules/registry` - единый реестр правил; - `/rules/registry` - единый реестр правил;
- `/reference/terminology` - нормативные определения; - `/reference/terminology` - нормативные определения;