Skip to content

Point de composition

Le point de composition, ou composition root, est la limite de l’application où les implémentations concrètes sont choisies et reliées. Le domaine exprime ses besoins ; l’infrastructure fournit les implémentations ; InferDI n’apparaît que dans le code d’assemblage.

Contrat du domaine

L’interface appartient au domaine, car le cas d’usage en dépend. Ce fichier n’importe ni conteneur ni framework.

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

Implémentation d’infrastructure

L’infrastructure implémente le contrat du domaine. Un véritable adaptateur utiliserait un client de base de données ; cet exemple reste court pour rendre l’appel facile à suivre.

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

Assemblage de l’application

Seul ce fichier importe InferDI. Il choisit PostgresUserStore, lui fournit son DSN et le relie à 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'])

Le tuple d’enregistrement est vérifié contre chaque constructeur. Modifier GetGreeting ou PostgresUserStore révèle à cette limite les connexions devenues incorrectes.

Utiliser le service à la limite de l’application

Une route HTTP, une commande CLI ou un consommateur de file résout le service de premier niveau. L’opération métier reste un appel de méthode ordinaire.

ts
import { container } from './container'

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

Ouvre ici un scope enfant si l’opération nécessite des données de requête ou de job. Libère-le à cette même limite, ou laisse un adaptateur le rattacher au cycle de vie du framework.

Tester directement le domaine

Le test unitaire ne construit pas de conteneur. Il passe un faux UserStore à la classe GetGreeting utilisée en production.

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

Utilise .override() pour les tests d’intégration qui doivent exercer le véritable graphe avec une implémentation remplacée. Pour un seul service métier, l’instanciation directe est généralement plus claire. Voir Tests et substitutions.