Skip to content

Корень композиции ​

Корень композиции (composition root) находится на границе приложения: здесь выбирают конкретные реализации и связывают их друг с другом. Доменный код описывает, что ему нужно, инфраструктурный код реализует эти требования, а InferDI используется только при сборке приложения.

Доменный контракт ​

Интерфейс объявлен в доменном слое, поскольку от него зависит бизнес-операция. В этом файле нет импортов контейнера или фреймворка.

ts
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}`
  }
}

Реализация инфраструктуры ​

Инфраструктурный класс реализует доменный контракт. В настоящем адаптере здесь был бы вызов клиента базы данных; в примере оставлен простой вызов, чтобы показать связь между слоями.

ts
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.

ts
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-команда или обработчик сообщения из очереди получает сервис верхнего уровня из контейнера. Затем бизнес-операция вызывается обычным методом этого сервиса.

ts
import { container } from './container'

export async function handleUser(id: string) {
  return container.get('greeting').execute(id)
}

Если операции нужны данные запроса или задания, создайте дочерний скоуп здесь же. Здесь же организуйте освобождение его ресурсов или поручите это адаптеру фреймворка.

Прямой тест домена ​

Модульный тест передаёт тестовую реализацию UserStore напрямую в тот же класс GetGreeting, который используется в приложении. Контейнер для этого не нужен.

ts
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() пригодится в интеграционном тесте, когда нужно проверить настоящий граф приложения, заменив одну реализацию. Отдельный доменный сервис обычно проще создать напрямую. Подробнее: Тестирование и подмена.