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:
118
docs/README.md
118
docs/README.md
@@ -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. Представить принятое решение папками, файлами и публичными точками входа.
|
|
||||||
|
|||||||
@@ -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).
|
||||||
|
|||||||
102
docs/architecture/dependencies.md
Normal file
102
docs/architecture/dependencies.md
Normal 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)
|
||||||
81
docs/architecture/groups.md
Normal file
81
docs/architecture/groups.md
Normal 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)
|
||||||
@@ -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) организует код внутри одного владельца.
|
|
||||||
|
|
||||||
## Немодульные исключения
|
## Немодульные исключения
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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`) и реэкспорт одинаково создают архитектурную зависимость.
|
||||||
|
|
||||||
Зависимость внутреннего файла, сегмента или компонента относится к ближайшему модулю-владельцу. Вложенный модуль начинает собственную границу зависимостей.
|
Зависимость внутреннего файла или сегмента относится к ближайшему модулю-владельцу. Вложенный модуль начинает собственную границу зависимостей.
|
||||||
|
|
||||||
### Нормативная матрица слоёв
|
### Нормативная матрица слоёв
|
||||||
|
|
||||||
|
|||||||
@@ -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-компоненты имеют одноуровневую файловую организацию с единственным допустимым исключением для главного файла в корне;
|
||||||
|
- корень и сегменты соответствуют своим назначениям;
|
||||||
- состояние и ресурсы имеют владельца и корректную область жизни;
|
- состояние и ресурсы имеют владельца и корректную область жизни;
|
||||||
- физическая структура однозначно выражает принятое решение;
|
- физическая структура однозначно выражает принятое решение;
|
||||||
- применимые автоматические проверки и архитектурное ревью пройдены.
|
- применимые автоматические проверки и архитектурное ревью пройдены.
|
||||||
|
|||||||
@@ -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` | Совместимость публичных фасетов со средами выполнения |
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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` - нормативные определения;
|
||||||
|
|||||||
Reference in New Issue
Block a user