Files
slm-design/DRAFT/level-3/domains/factory-ports-adapters.md

89 lines
6.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Фабрика, порты и адаптеры
> Пояснение границы между бизнес-логикой и средой выполнения.
## Связанные правила
- [`SLM-L3-FACTORY-R005`](../../rules/level-3.md#slm-l3-factory-r005)
- [`SLM-L3-FACTORY-R006`](../../rules/level-3.md#slm-l3-factory-r006)
- [`SLM-L3-PORT-R007`](../../rules/level-3.md#slm-l3-port-r007)
- [`SLM-L3-ADAPTER-R008`](../../rules/level-3.md#slm-l3-adapter-r008)
- [`SLM-L3-BUSINESS-A004`](../../rules/level-3.md#slm-l3-business-a004)
## Фабрика и экземпляр API
```text
фабрика + реализации портов → экземпляр API бизнес-логики
```
Фабрика принадлежит модулю `business`, получает полный набор `AuthDeps` и возвращает `AuthApi`:
```ts
export type AuthFactory = (deps: AuthDeps) => AuthApi
```
Все типовые сборки одной фабрики предоставляют полный набор портов и получают API одного контракта. Браузер, обработчик запроса и серверное действие не требуют разных фабрик только из-за среды выполнения. Сборка может открыть потребителю более узкое представление API, но не меняет контракт фабрики.
Вызов фабрики создаёт только объекты и замыкания без побочных эффектов. Он не выполняет запросы, не читает файлы cookie, хранилище или переменные окружения, не запускает подписки и таймеры, не обращается к API платформы, не выбирает адаптер и не выполняет операции жизненного цикла фреймворка.
## Независимый от среды граф импортов
Проверяется весь граф рабочего кода, достижимый из точки входа `business`, а не только файл фабрики. Он не должен достигать:
- React, Vue, Next.js и служебных меток фреймворка;
- границ `client-only`, `server-only`, API браузера или Node.js;
- SDK, сгенерированного клиента, реализации хранилища или конкретной библиотеки состояния;
- адаптеров, сборок, модулей фреймворков и конфигурации среды.
Удаление неиспользуемого кода при сборке не доказывает изоляцию. Импорт только типов из конкретной реализации создаёт ту же архитектурную зависимость и также запрещён.
## Порты
Порт принадлежит бизнес-логике и описывает возможность на языке предметной области:
```ts
export type AuthPhonePort = {
requestCode: (phone: string) => Promise<unknown>
verifyCode: (data: VerifyPhoneOtpData) => Promise<unknown>
}
export type AuthSessionPort = {
getSnapshot: () => AuthState
subscribe: (listener: () => void) => () => void
setToken: (token: string | null) => void
}
```
Порт не принимает клиент SDK, сгенерированную операцию, `Request`, `Window`, React-хук, `StoreApi` или тип конкретной среды. Он отделяет контракт от реализации, а не скрывает отсутствие возможности. Необязательный порт или метод, который намеренно падает в одной из сред, нарушает контракт фабрики.
`unknown` допустим только на границе непроверенного внешнего результата. Бизнес-логика обязана проверить такое значение до преобразования в предметный результат, состояние или ошибку. Если адаптер уже может вернуть устойчивый предметный результат, порт описывает этот результат, а не DTO конкретного транспорта.
## Адаптеры
Адаптер соединяет порт с конкретной технической реализацией:
```text
порт business ← адаптер → SDK / хранилище / платформа / данные запроса
```
Адаптер может преобразовать предметные аргументы в транспортные, вызвать внешний источник, привести технический результат к контракту порта и вернуть исходный сбой. Он не определяет код ошибки домена, резервное предметное поведение, инвариант или публичный метод `AuthApi`.
По умолчанию адаптер является закрытым сегментом минимальной типовой сборки:
```text
domains/auth/presets/application/
├── adapters/
│ └── auth-phone.adapter.ts
└── index.ts
```
Если адаптер нужен нескольким сборкам или имеет самостоятельную ответственность интеграции, он становится отдельным модулем:
```text
domains/auth/adapters/
└── identity-provider/
└── index.ts
```
Самостоятельный адаптер сохраняет минимальный публичный API. Его появление не делает конкретный SDK частью публичного контракта `business`.