<link rel="stylesheet" href="styles-3GBANQL6.css">
← Zurück zum Blog

NestJS

Vermeidung von Circular Dependencies in NestJS-Anwendungen

Vermeidung von Circular Dependencies in NestJS-Anwendungen

Von John Doe | 24. März 2023

Einleitung

Während der Entwicklung Ihrer NestJS-Serveranwendung läuft alles reibungslos, bis Sie auf den gefürchteten Fehler einer zirkulären Abhängigkeit stoßen:

[Nest] 116557  - 03/07/2023, 14:12:40   ERROR [ExceptionHandler] 
Nest kann die Instanz von ExampleModule nicht erstellen. 
Das Modul an Index [0] des "imports"-Arrays von ExampleModule ist undefiniert.
    

Dieser Fehler kann zunächst beunruhigend erscheinen, deutet jedoch häufig auf eine zirkuläre Abhängigkeit zwischen Ihren Modulen oder Diensten hin. In diesem Beitrag erläutern wir, was zirkuläre Abhängigkeiten sind, welche Arten es gibt, wie man sie erkennt und, vor allem, wie man sie vermeidet!

Was ist eine zirkuläre Abhängigkeit?

Eine zirkuläre Abhängigkeit tritt auf, wenn zwei oder mehr Module, Dienste oder Dateien voneinander abhängen, wodurch eine Schleife entsteht, die schwer aufzulösen ist. In NestJS gibt es drei Haupttypen von zirkulären Abhängigkeiten:

  • Zirkuläre Dateiimporte: Datei A importiert Datei B und Datei B importiert Datei A.
  • Zirkuläre Modulimporte: Modul A importiert Modul B und Modul B importiert Modul A.
  • Zirkuläre Konstruktorabhängigkeiten: Dienst A hängt von Dienst B ab und umgekehrt.

Zirkuläre Modulimporte

Sehen wir uns ein Beispiel an. Betrachten Sie die folgenden zwei Module:


// foo.module.ts
import { Module, forwardRef } from '@nestjs/common';
import { FooService } from './foo.service';
import { BarModule } from '../bar/bar.module';

@Module({
  imports: [forwardRef(() => BarModule)],
  providers: [FooService],
  exports: [FooService],
})
export class FooModule {}
    

// bar.module.ts
import { Module, forwardRef } from '@nestjs/common';
import { BarService } from './bar.service';
import { FooModule } from '../foo/foo.module';

@Module({
  imports: [forwardRef(() => FooModule)],
  providers: [BarService],
  exports: [BarService],
})
export class BarModule {}
    

Hier importieren beide Module einander, was zu einer zirkulären Abhängigkeit führt. Wir lösen dies, indem wir die Funktion forwardRef() verwenden, die die Importauflösung bis zur Laufzeit verzögert und so die zirkuläre Referenz aufhebt.

Zirkuläre Konstruktorabhängigkeiten

Ein weiteres häufiges Szenario sind zirkuläre Konstruktorabhängigkeiten. Zum Beispiel:


// foo.service.ts
import { Injectable, Inject, forwardRef } from '@nestjs/common';
import { BarService } from '../bar/bar.service';

@Injectable()
export class FooService {
  constructor(
    @Inject(forwardRef(() => BarService))
    private readonly barService: BarService,
  ) {}
}
    

// bar.service.ts
import { Injectable, Inject, forwardRef } from '@nestjs/common';
import { FooService } from '../foo/foo.service';

@Injectable()
export class BarService {
  constructor(
    @Inject(forwardRef(() => FooService))
    private readonly fooService: FooService,
  ) {}
}
    

In diesem Beispiel hängt FooService von BarService ab und umgekehrt. Ohne forwardRef() könnte NestJS diese Abhängigkeiten nicht auflösen. Diese Lösung funktioniert zwar, aber es ist besser, zirkuläre Abhängigkeiten zu vermeiden, wenn möglich.

Zirkuläre Abhängigkeiten erkennen

Nun, da Sie wissen, wie zirkuläre Abhängigkeiten entstehen, sehen wir uns an, wie man sie findet. Ein großartiges Tool dafür ist madge, das zirkuläre Abhängigkeiten in Ihrem Projekt erkennen kann:


npm install -D madge
npx madge --circular src/main.ts
    

Das Ergebnis könnte etwa so aussehen:

✖ 2 zirkuläre Abhängigkeiten gefunden!
1) bar/bar.service.ts > foo/foo.service.ts
2) bar/bar.module.ts > foo/foo.module.ts
    

Jetzt können Sie genau sehen, wo die zirkulären Abhängigkeiten in Ihrem Code sind und sie nach Bedarf umstrukturieren.

Warum man zirkuläre Abhängigkeiten vermeiden sollte

Zirkuläre Abhängigkeiten sind oft ein Zeichen für eng gekoppelten Code, was zu Wartungsproblemen und Leistungsproblemen führen kann. Insbesondere können sie verursachen:

  • Race Conditions: Abhängigkeiten werden parallel aufgelöst, sodass zirkuläre Abhängigkeiten zu undefinierten Werten führen können.
  • Fragile Architektur: Eng gekoppelte Codestrukturen sind schwerer zu refaktorisieren, zu testen und in verschiedenen Teilen der Anwendung wiederzuverwenden.

Refactoring, um zirkuläre Abhängigkeiten zu vermeiden

Anstatt sich auf forwardRef() zu verlassen, sollten Sie überlegen, ob Sie Ihre Module und Dienste so umstrukturieren können, dass zirkuläre Abhängigkeiten vollständig vermieden werden. Zum Beispiel:

Lösung 1: Gemeinsame Logik extrahieren

Wenn sowohl FooService als auch BarService dieselbe Funktionalität benötigen, extrahieren Sie diese in einen neuen Dienst oder ein Hilfsmodul, das beide importieren können:


// shared.service.ts
@Injectable()
export class SharedService {
  commonMethod() { /* Logik hier */ }
}
    

Jetzt können sowohl FooService als auch BarService SharedService injizieren, anstatt voneinander abhängig zu sein.

Lösung 2: Module entkoppeln

Wenn möglich, entkoppeln Sie Module, sodass sie nicht direkt voneinander abhängen. Zum Beispiel können Sie ein neues Vermittler- oder ereignisgesteuertes System erstellen, in dem die Module indirekt miteinander kommunizieren.

Fazit

Zirkuläre Abhängigkeiten können herausfordernd sein, bieten aber auch die Möglichkeit, die Architektur Ihrer Anwendung zu überdenken. Verwenden Sie Tools wie madge, um sie zu erkennen, und bevorzugen Sie immer Refactoring, bevor Sie forwardRef() verwenden, es sei denn, es ist absolut notwendig. Sauberer, entkoppelter Code ist einfacher zu warten, zu testen und zu skalieren!

LASSEN SIE UNS ETWAS NEUES BAUEN

Aus Ihrer Idee kann die nächste gute Softwarelösung entstehen.

Erzählen Sie uns, was Sie entwickeln, verbessern oder neu denken möchten.

Projekt besprechen