mirror of
https://github.com/gromlab-ru/slm-design.git
synced 2026-08-22 07:30:16 +03:00
sync
This commit is contained in:
200
DRAFT/level-3/domains/factory-ports-adapters.md
Normal file
200
DRAFT/level-3/domains/factory-ports-adapters.md
Normal file
@@ -0,0 +1,200 @@
|
||||
# Factory, ports и adapters
|
||||
|
||||
> Рабочая заметка. Не является нормативным разделом спецификации.
|
||||
|
||||
## Терминология
|
||||
|
||||
### FAC-N003: Собирается API instance, а не factory
|
||||
|
||||
```text
|
||||
Factory + Deps implementations → business API instance
|
||||
```
|
||||
|
||||
- Factory является функцией создания.
|
||||
- Ports являются business-owned контрактами capabilities.
|
||||
- `Deps` группирует ports, нужные factory.
|
||||
- Adapters реализуют ports в concrete runtime.
|
||||
- Assembly site вызывает factory и получает API instance.
|
||||
- Preset является готовой конфигурацией assembly.
|
||||
|
||||
Формулировка «собранная фабрика» неточна. Factory конфигурируется зависимостями и создаёт собранный API.
|
||||
|
||||
## Business factory
|
||||
|
||||
### FAC-N004: Factory является framework-neutral и environment-neutral
|
||||
|
||||
Factory не знает, где будет использована:
|
||||
|
||||
- в browser;
|
||||
- во время SSR;
|
||||
- в server action;
|
||||
- в background process;
|
||||
- в unit test;
|
||||
- в React, Vue или другом framework.
|
||||
|
||||
```ts
|
||||
export type AuthFactory = (deps: AuthDeps) => AuthApi
|
||||
```
|
||||
|
||||
### FAC-N005: Factory имеет стабильную форму результата
|
||||
|
||||
Все presets одной factory создают один и тот же business API contract. Среда не выбирается через аргумент `mode`, а форма API не зависит от наличия optional dependency.
|
||||
|
||||
Не рекомендуется:
|
||||
|
||||
```ts
|
||||
authFactory({
|
||||
mode: 'server',
|
||||
serverAdminClient: optionalClient,
|
||||
})
|
||||
```
|
||||
|
||||
Не рекомендуется возвращать методы, которые существуют в общем API, но намеренно падают в одной из сред.
|
||||
|
||||
### FAC-N006: Factory construction не выполняет side effects
|
||||
|
||||
Вызов factory не должен:
|
||||
|
||||
- выполнять network request;
|
||||
- читать cookies, storage или env;
|
||||
- запускать subscription или timer;
|
||||
- обращаться к browser либо Node API;
|
||||
- создавать скрытый application singleton;
|
||||
- выбирать concrete adapter;
|
||||
- выполнять framework lifecycle.
|
||||
|
||||
Factory может синхронно создать детерминированные services и связать их с переданными ports.
|
||||
|
||||
## Гигиена import graph
|
||||
|
||||
### FAC-N007: Весь достижимый из business import graph должен быть изоморфным
|
||||
|
||||
Недостаточно проверить только файл `{domain}.factory.ts`. Ни один production import, достижимый из business public entrypoint, не должен приводить к:
|
||||
|
||||
- React, Vue, Next.js и другим frameworks;
|
||||
- `'use client'`, `client-only` или `server-only` boundary;
|
||||
- browser API;
|
||||
- Node-only API;
|
||||
- concrete SDK/client;
|
||||
- concrete storage;
|
||||
- state/query runtime;
|
||||
- adapters и presets;
|
||||
- environment configuration.
|
||||
|
||||
Tree shaking не используется как доказательство изоляции.
|
||||
|
||||
## Ports
|
||||
|
||||
### PORT-N001: Port принадлежит business
|
||||
|
||||
Port описывает capability на языке business, а не форму concrete implementation.
|
||||
|
||||
```ts
|
||||
export type AuthPhonePort = {
|
||||
requestCode: (phone: string) => Promise<unknown>
|
||||
verifyCode: (data: VerifyPhoneOtpData) => Promise<unknown>
|
||||
}
|
||||
```
|
||||
|
||||
Port не должен раскрывать SDK client, generated operation, `Request`, `Window`, React hook, Zustand `StoreApi` и другие environment/framework types.
|
||||
|
||||
### PORT-N002: Ports абстрагируют implementation, но не доступность capability
|
||||
|
||||
Одна factory возможна, пока каждый preset способен реализовать одинаковые ports.
|
||||
|
||||
Server capability может остаться общим port, если browser adapter реализует её через безопасный HTTP/RPC boundary. Если capability принципиально невозможно реализовать в одной из поддерживаемых сред, её нельзя маскировать optional dependency общего API.
|
||||
|
||||
### PORT-N003: Reactive port должен быть framework-neutral
|
||||
|
||||
Client hook в `Deps` делает контракт client-oriented. Вместо `useToken` базовый port может описывать framework-neutral observation protocol:
|
||||
|
||||
```ts
|
||||
export type AuthSessionPort = {
|
||||
getSnapshot: () => AuthState
|
||||
subscribe: (listener: () => void) => () => void
|
||||
setToken: (token: string | null) => void
|
||||
}
|
||||
```
|
||||
|
||||
React binding может построить `useAuth` поверх `getSnapshot` и `subscribe`. Vue binding использует тот же port через собственный lifecycle.
|
||||
|
||||
Точная форма reactive ports требует отдельной проверки на реальном state manager.
|
||||
|
||||
## Adapters
|
||||
|
||||
### ADP-N001: Adapter реализует business port
|
||||
|
||||
Adapter знает одновременно business contract и concrete runtime:
|
||||
|
||||
```text
|
||||
business port ← adapter → SDK / storage / browser / request
|
||||
```
|
||||
|
||||
Adapter может:
|
||||
|
||||
- преобразовать domain arguments в transport arguments;
|
||||
- вызвать concrete source;
|
||||
- привести concrete runtime к минимальной форме port;
|
||||
- управлять техническими деталями конкретной integration.
|
||||
|
||||
Adapter не должен:
|
||||
|
||||
- определять business error code;
|
||||
- выбирать domain fallback;
|
||||
- менять business invariant;
|
||||
- расширять public business API методами concrete client.
|
||||
|
||||
### ADP-N002: Adapter размещается у минимального владельца
|
||||
|
||||
SLM не задаёт обязательную структуру `adapters/browser`, `adapters/server` или другую техническую классификацию.
|
||||
|
||||
Adapter может быть:
|
||||
|
||||
- private файлом или segment конкретного preset module;
|
||||
- самостоятельным Domain module после появления нескольких assembly consumers;
|
||||
- частью пользовательской logical group, если она действительно упрощает навигацию.
|
||||
|
||||
Default colocation для adapter, принадлежащего одной assembly:
|
||||
|
||||
```text
|
||||
domains/auth/presets/{preset-name}/
|
||||
├── adapters/
|
||||
├── create-auth.ts
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
Возможный promotion переиспользуемого adapter:
|
||||
|
||||
```text
|
||||
domains/auth/adapters/ # optional logical group
|
||||
└── {adapter-name}/ # adapter module
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
Environment-specific code не должен быть достижим из entrypoint, объявленного framework-neutral или environment-neutral. Способ физической изоляции выбирает проект. Группировка по `browser/server` допустима как локальное соглашение, но не является требованием SLM.
|
||||
|
||||
## Assembly sites
|
||||
|
||||
### ASM-N001: Вызов factory определяет роль assembler
|
||||
|
||||
Factory может быть вызвана в preset, provider, route/request composition, test setup или другом месте. Путь сам по себе не запрещает сборку.
|
||||
|
||||
Assembly site обязан:
|
||||
|
||||
- предоставить полный `Deps`;
|
||||
- выбрать concrete adapters;
|
||||
- определить предполагаемый scope API instance;
|
||||
- вернуть необходимые lifecycle/dispose handles;
|
||||
- не скрывать создание graph от фактического владельца.
|
||||
|
||||
После возврата результата lifecycle принадлежит caller/graph owner, который удерживает API instance. Например, request владеет request-scoped instance, а Provider владеет instance до unmount. Preset описывает создание и передачу ownership, но не становится долгоживущим владельцем только из-за своего расположения.
|
||||
|
||||
### ASM-N002: Consumer использует готовый API
|
||||
|
||||
Screen, component или service, который только выполняет business-сценарий, получает готовый business API, например `AuthApi`. Если такой consumer вызывает factory, он становится assembler и должен удовлетворять всем требованиям assembly role.
|
||||
|
||||
### ASM-N003: Cross-domain dependency получает собранный API
|
||||
|
||||
Business одного Domain не создаёт factory другого Domain внутри себя. Он описывает необходимую capability через свой `Deps`, а graph owner передаёт уже собранный API.
|
||||
|
||||
Tests вправе напрямую вызывать factory с mocks и fakes. Это один из основных сценариев существования factory.
|
||||
Reference in New Issue
Block a user