Skip to content

Der Composition Root

Der Composition Root ist die Stelle am Rand der Anwendung, an der konkrete Implementierungen ausgewählt und verbunden werden. Fachlicher Code beschreibt seine Anforderungen; Infrastruktur liefert die Implementierung. InferDI kommt nur im Kompositionscode vor.

Vertrag der Fachdomäne

Das Interface gehört zur Domäne, weil der Anwendungsfall davon abhängt. Diese Datei importiert weder Container noch 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}`
  }
}

Implementierung in der Infrastruktur

Die Infrastruktur implementiert den Domänenvertrag. Ein echter Adapter würde einen Datenbankclient verwenden; das kleine Beispiel hält den Aufruf übersichtlich.

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

Die Anwendung zusammensetzen

Nur diese Datei importiert InferDI. Sie wählt PostgresUserStore, übergibt dessen DSN und verbindet ihn mit 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'])

Das Registrierungstupel wird gegen den jeweiligen Konstruktor geprüft. Änderungen an GetGreeting oder PostgresUserStore machen eine veraltete Verdrahtung an dieser Grenze sichtbar.

Den Service an der Anwendungsgrenze verwenden

Eine HTTP-Route, ein CLI-Befehl oder ein Queue-Consumer löst den obersten Service auf. Die fachliche Operation wird weiterhin über dessen gewöhnliche Methode aufgerufen.

ts
import { container } from './container'

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

Benötigt die Operation Anfrage- oder Jobdaten, öffne hier einen Kind-Scope. Gib ihn an derselben Grenze frei oder überlasse die Anbindung an den Framework-Lebenszyklus einem Adapter.

Die Domäne direkt testen

Der Unit-Test baut keinen Container auf. Er übergibt einen UserStore-Fake an dieselbe Klasse GetGreeting, die auch produktiv verwendet wird.

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

Nutze .override() für Integrationstests, die den echten Graphen mit einer ausgetauschten Implementierung prüfen sollen. Für einen einzelnen fachlichen Service ist direkte Konstruktion meist klarer. Mehr dazu unter Tests und Overrides.