8.3 KiB
Фабрики, зависимости и adapters
Пояснение границы между
businessи технической средой.
Связанные правила
SLM-L2-BUSINESS-A007SLM-L2-FACTORY-R008SLM-L2-ERROR-R010SLM-L2-DEPENDENCY-A012SLM-L2-BUSINESS-R018SLM-L2-BUSINESS-A019SLM-L2-ADAPTER-R021SLM-L2-BUSINESS-A022SLM-L2-BUSINESS-R024
Одна фабрика на API
явные зависимости + business factory → один Domain API
Модуль business предоставляет одну именованную фабрику для каждого объявленного API. Фабрики экспортируются через общий runtime-фасет business/factory:
import type {
AuthAdministrationApi,
AuthAdministrationDeps,
AuthSessionApi,
AuthSessionDeps,
} from '@/domains/auth/business'
export type AuthSessionFactory = (
deps: AuthSessionDeps,
) => AuthSessionApi
export type AuthAdministrationFactory = (
deps: AuthAdministrationDeps,
) => AuthAdministrationApi
import {
authAdministrationFactory,
authSessionFactory,
} from '@/domains/auth/business/factory'
Фабрика не выбирает environment, assembly или конкретную production-реализацию зависимости. Разные API могут иметь разные наборы deps и собираться независимо.
Технические зависимости
Зависимости фабрики описывают возможности, необходимые business-сценариям. Они не раскрывают конкретный SDK, framework hook, generated operation или объект платформы.
export type AuthPhoneDependency = {
requestCode: (phone: string) => Promise<unknown>
verifyCode: (code: string) => Promise<unknown>
}
unknown допустим на границе непроверенного внешнего результата. business проверяет его до превращения в предметные данные или состояние.
Техническими зависимостями также являются:
- concrete state/query runtime;
- subscription и event source;
- browser, Node.js и framework capabilities;
- request data и abort signal;
- текущее время и timer;
- random и ID generator;
- environment и runtime configuration provider.
export type VerificationDeps = {
clock: { now: () => number }
ids: { create: () => string }
timer: { delay: (ms: number) => Promise<void> }
}
Date.now(), new Date() без аргумента, Math.random(), crypto.randomUUID(), скрытый env и глобальный timer не читаются напрямую business-кодом, если влияют на поведение сценария. Детерминированная арифметика над переданным timestamp остаётся business-safe.
Cancellation также описывается business-owned контрактом. Concrete AbortSignal или другой platform type остаётся внутри adapter, пока не принят отдельный общий публичный контракт.
Термин и обязательная поведенческая форма технического порта пока не закреплены. В частности, позднее нужно определить cancellation, timeout, retry, concurrency и subscription semantics.
Cross-domain API dependency
Готовый API другого домена не считается техническим adapter-портом. Это отдельная cross-domain API dependency, выраженная type-only контрактом:
import type { AuthSessionApi } from '@/domains/auth/business'
export type UserDeps = {
auth: Pick<AuthSessionApi, 'getSnapshot'>
}
Runtime-значение место сборки графа передаёт через assembly либо напрямую зависимой business-фабрике. user/business не импортирует executable factory, assembly или instance Auth.
Если exception-модели нужен runtime guard чужой ожидаемой ошибки, зависимый business может импортировать его из детерминированного auth/business/runtime. Этот импорт остаётся обычным ребром DAG.
Adapter module
Adapter module соединяет явную техническую зависимость фабрики с конкретной системой:
business dependency ← adapter → SDK / query runtime / platform / request data
Adapter преобразует аргументы и технический результат к контракту зависимости. Он не определяет публичный метод Domain API, предметный fallback или код доменной ошибки.
Ожидаемый исходный сбой возвращается business, который выбирает собственный error code. Поэтому приложение не строит поведение по HTTP status, SDK error class или storage exception.
Adapter может использовать TanStack Query, Apollo, Zustand или другой state/query runtime, если реализует business-owned dependency. Library types, query keys и mutable clients не протекают в public types business.
Размещение adapters
Каждая связная production-реализация является отдельным SLM-модулем в Group adapters:
auth/adapters/
├── phone-http/
│ └── index.ts
├── browser-session/
│ └── index.ts
└── browser-runtime/
└── index.ts
Один adapter-модуль может реализовать несколько тесно связанных зависимостей одного technical provider. Например, browser-runtime может предоставить clock, timer и ID generator. Правило не требует отдельного модуля для каждого метода deps.
Adapter module имеет собственные ответственность, публичный API, environment label и тестовую границу. Group adapters не имеет index.ts и не реэкспортирует дочерние модули.
Production adapter запрещено определять:
- закрытым сегментом assembly;
- inline-функцией в
compositionилиapp; - частью framework binding module;
- скрытой реализацией внутри
business.
Assembly и одноразовое место сборки импортируют конкретные adapter-модули через их публичные API:
import { authSessionFactory } from '@/domains/auth/business/factory'
import { createPhoneHttpAdapter } from '@/domains/auth/adapters/phone-http'
import { createBrowserRuntimeAdapter } from '@/domains/auth/adapters/browser-runtime'
const session = authSessionFactory({
phone: createPhoneHttpAdapter(),
runtime: createBrowserRuntimeAdapter(),
})
Если ни одна фабрика не имеет технических зависимостей, Group adapters не обязательна. Cross-domain API dependency не считается adapter и передаётся отдельно.
Локальные fake implementations в business-тестах не являются production adapters и не требуют SLM-модулей. Они существуют только внутри тестовой границы и не экспортируются в рабочий код.