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

6.1 KiB
Raw Blame History

Фабрика, порты и адаптеры

Пояснение границы между бизнес-логикой и средой выполнения.

Связанные правила

Фабрика и экземпляр API

фабрика + реализации портов → экземпляр API бизнес-логики

Фабрика принадлежит модулю business, получает полный набор AuthDeps и возвращает AuthApi:

export type AuthFactory = (deps: AuthDeps) => AuthApi

Все типовые сборки одной фабрики предоставляют полный набор портов и получают API одного контракта. Браузер, обработчик запроса и серверное действие не требуют разных фабрик только из-за среды выполнения. Сборка может открыть потребителю более узкое представление API, но не меняет контракт фабрики.

Вызов фабрики создаёт только объекты и замыкания без побочных эффектов. Он не выполняет запросы, не читает файлы cookie, хранилище или переменные окружения, не запускает подписки и таймеры, не обращается к API платформы, не выбирает адаптер и не выполняет операции жизненного цикла фреймворка.

Независимый от среды граф импортов

Проверяется весь граф рабочего кода, достижимый из точки входа business, а не только файл фабрики. Он не должен достигать:

  • React, Vue, Next.js и служебных меток фреймворка;
  • границ client-only, server-only, API браузера или Node.js;
  • SDK, сгенерированного клиента, реализации хранилища или конкретной библиотеки состояния;
  • адаптеров, сборок, модулей фреймворков и конфигурации среды.

Удаление неиспользуемого кода при сборке не доказывает изоляцию. Импорт только типов из конкретной реализации создаёт ту же архитектурную зависимость и также запрещён.

Порты

Порт принадлежит бизнес-логике и описывает возможность на языке предметной области:

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 конкретного транспорта.

Адаптеры

Адаптер соединяет порт с конкретной технической реализацией:

порт business ← адаптер → SDK / хранилище / платформа / данные запроса

Адаптер может преобразовать предметные аргументы в транспортные, вызвать внешний источник, привести технический результат к контракту порта и вернуть исходный сбой. Он не определяет код ошибки домена, резервное предметное поведение, инвариант или публичный метод AuthApi.

По умолчанию адаптер является закрытым сегментом минимальной типовой сборки:

domains/auth/presets/application/
├── adapters/
│   └── auth-phone.adapter.ts
└── index.ts

Если адаптер нужен нескольким сборкам или имеет самостоятельную ответственность интеграции, он становится отдельным модулем:

domains/auth/adapters/
└── identity-provider/
    └── index.ts

Самостоятельный адаптер сохраняет минимальный публичный API. Его появление не делает конкретный SDK частью публичного контракта business.