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.