Корень композиции
Корень композиции (composition root) находится на границе приложения: здесь выбирают конкретные реализации и связывают их друг с другом. Доменный код описывает, что ему нужно, инфраструктурный код реализует эти требования, а InferDI используется только при сборке приложения.
Доменный контракт
Интерфейс объявлен в доменном слое, поскольку от него зависит бизнес-операция. В этом файле нет импортов контейнера или фреймворка.
export interface UserStore {
findName(id: string): Promise<string | undefined>
}
export class GetGreeting {
constructor(private readonly users: UserStore) {}
async execute(id: string) {
const name = await this.users.findName(id)
return name === undefined ? 'Hello, stranger' : `Hello, ${name}`
}
}Реализация инфраструктуры
Инфраструктурный класс реализует доменный контракт. В настоящем адаптере здесь был бы вызов клиента базы данных; в примере оставлен простой вызов, чтобы показать связь между слоями.
import type { UserStore } from './domain'
export class PostgresUserStore implements UserStore {
constructor(private readonly dsn: string) {}
async findName(id: string) {
console.info(`query ${this.dsn} for ${id}`)
return id === '42' ? 'Ada' : undefined
}
}Сборка приложения
InferDI импортируется только здесь. Код выбирает PostgresUserStore, передаёт ему DSN и соединяет с GetGreeting.
import { Container } from '@inferdi/inferdi'
import { GetGreeting } from './domain'
import { PostgresUserStore } from './infrastructure'
export const container = new Container()
.registerValue('dsn', 'postgres://localhost/app')
.registerClass('users', PostgresUserStore, ['dsn'])
.registerClass('greeting', GetGreeting, ['users'])Кортежи зависимостей проверяются по сигнатурам конструкторов. Если изменить GetGreeting или PostgresUserStore и не обновить сборку, TypeScript укажет на ошибку именно здесь.
Вызов сервиса на границе
Обработчик HTTP-маршрута, CLI-команда или обработчик сообщения из очереди получает сервис верхнего уровня из контейнера. Затем бизнес-операция вызывается обычным методом этого сервиса.
import { container } from './container'
export async function handleUser(id: string) {
return container.get('greeting').execute(id)
}Если операции нужны данные запроса или задания, создайте дочерний скоуп здесь же. Здесь же организуйте освобождение его ресурсов или поручите это адаптеру фреймворка.
Прямой тест домена
Модульный тест передаёт тестовую реализацию UserStore напрямую в тот же класс GetGreeting, который используется в приложении. Контейнер для этого не нужен.
import { GetGreeting, type UserStore } from './domain'
const fakeUsers: UserStore = {
async findName(id) {
return id === '42' ? 'Ada' : undefined
}
}
const service = new GetGreeting(fakeUsers)
const result = await service.execute('42')
if (result !== 'Hello, Ada') {
throw new Error(`Unexpected greeting: ${result}`)
}.override() пригодится в интеграционном тесте, когда нужно проверить настоящий граф приложения, заменив одну реализацию. Отдельный доменный сервис обычно проще создать напрямую. Подробнее: Тестирование и подмена.
