11 KiB
Зависимости
Зависимость связывает архитектурных владельцев. Исходный файл создаёт ребро от своего ближайшего модуля к ближайшему модулю импортируемого файла.
Модульный граф
Узлами архитектурного графа являются модули, включая вложенные. Группы, сегменты, framework-компоненты, hooks, stores и другие файлы реализации отдельных узлов не создают.
Для каждой связи определяются:
- Ближайший модуль-владелец исходного файла.
- Ближайший модуль-владелец целевого файла.
- Слои исходного и целевого владельцев.
- Публичный фасет, через который пересечена граница.
Обычный импорт, import type и реэкспорт одинаково создают архитектурное ребро. Связь файлов внутри одного модуля остаётся внутренней реализацией и не создаёт межмодульную зависимость.
Вложенный модуль начинает новый узел. Импорт из родительского модуля во вложенный или обратно проверяется как обычная межмодульная связь.
Публичная граница
При пересечении модульной границы используется только объявленный публичный фасет целевого модуля:
// Допустимо
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, предметный сценарий остаётся ответственностью доменного модуля, а техническая возможность — ответственностью инфраструктурного.
Допустимость связи и владение
Матрица отвечает только на вопрос, может ли один слой зависеть от другого. Она не разрешает исходному модулю реализовывать ответственность, которая по своей роли принадлежит целевому или другому слою.
compositions может зависеть от domains и infra, но использует эти направления по-разному:
- доменный сценарий доступен композиции только как готовый публичный API доменного модуля;
- инфраструктурный API может обслуживать собственную техническую потребность композиции, например тему, локализацию или доставку метрики показа страницы;
- инфраструктурный HTTP-клиент, SDK или storage не используются композицией для реализации продуктовой операции, загрузки предметных данных или определения доменного исхода.
// Допустимо: композиция использует готовый доменный UI.
import { OrdersList } from '@/domains/orders/client'
// Допустимо: техническая возможность обслуживает саму композицию.
import { useTheme } from '@/infra/theme/client'
// Направление импорта допустимо, но ответственность выбрана неверно.
import { http } from '@/infra/http'
await http.get('/orders')
В последнем примере запрос получает предметные данные и участвует в доменном сценарии. Его смысл, параметры, продуктовые исходы и вызов принадлежат доменному модулю, который открывает композиции готовый API.
Разрешённая зависимость domains от infra также не объединяет их контракты. Домен может использовать HTTP-транспорт, SDK или storage через публичный API инфраструктурного модуля, но DTO и ошибки источника остаются только во внутреннем интеграционном коде домена. До попадания в правила, состояние, доменный UI или публичный результат данные адаптируются к доменному контракту, а ошибка источника интерпретируется в терминах текущего сценария. Полные требования описаны в разделе Домены.
Аналогично, технической доставкой метрик владеет infra, но смысл события определяется модулем-владельцем наблюдаемого поведения. Разрешённый вызов telemetry API из композиции не позволяет ей объявлять события доменного сценария от своего имени.
Композиция может размещать и связывать несколько доменных API. Если эта связь задаёт обязательный порядок, продуктовые условия, общий предметный результат или политику ошибок между доменами, она является отдельным доменным сценарием, а не внутренней логикой композиции.
Зависимости внутри слоя
Модули одного слоя могут зависеть друг от друга в любом направлении при одновременном выполнении двух условий:
- Целевой модуль используется только через публичный API.
- Общий модульный граф остаётся ацикличным.
Принадлежность модулей одной или разным группам не влияет на разрешение связи. Группа не имеет API и не является промежуточным узлом импорта.
SLM не задаёт отдельные same-layer матрицы для pages, layouts, widgets, доменов, инфраструктуры, UI или shared. Если проекту нужна более строгая локальная политика, она является дополнительным проектным ограничением, а не общим правилом SLM.
Запрет циклов
Общий граф модулей внутри одного SLM root остаётся ацикличным. Запрет действует для модулей одного слоя, разных разрешённых слоёв и вложенных модулей.
Проверки только файлового графа недостаточно. Например:
module-a/file-1.ts → module-b/file-1.ts
module-b/file-2.ts → module-a/file-2.ts
Между конкретными файлами может не существовать замкнутого пути, но после сопоставления файлов владельцам возникает архитектурный цикл:
module-a ↔ module-b
Lint-проверка модульных циклов должна:
- Сопоставить каждый файл ближайшему модулю-владельцу.
- Свернуть внутренние импорты файлов одного модуля.
- Добавить межмодульные рёбра для импортов типов, исполняемого кода и реэкспортов.
- Считать каждый вложенный модуль отдельным узлом.
- Блокировать любое сильносвязное множество из нескольких модулей.
Стандартная file-level проверка циклов может использоваться дополнительно, но не заменяет проверку модульного графа.
Проверка связи
Для каждого нового или изменённого импорта проверяются три условия:
- Направление разрешено матрицей слоёв.
- Целевая модульная граница пересечена через публичный фасет.
- После добавления ребра модульный граф остаётся ацикличным.