14 Dependency Injection

14.1 Einführung

Dependency Injection (DI) ist ein fundamentales Designmuster und einer der Grundpfeiler des Angular-Frameworks. Angular hat sein DI-System über die Versionen hinweg stetig weiterentwickelt, um es leistungsfähiger, flexibler und entwicklerfreundlicher zu gestalten.

Das Grundprinzip von Dependency Injection ist einfach, aber mächtig: Eine Klasse sollte ihre Abhängigkeiten nicht selbst erstellen oder verwalten, sondern diese von außen erhalten. Dies führt zu:

14.2 Grundlegende Konzepte des DI-Systems in Angular

14.2.1 Constructor Injection

Die Constructor Injection ist die klassische und weit verbreitete Methode, um Abhängigkeiten in Angular-Klassen zu injizieren. Hierbei werden die benötigten Abhängigkeiten als Parameter im Konstruktor deklariert:

@Component({
  selector: 'app-user-list',
  templateUrl: './user-list.component.html'
})
export class UserListComponent implements OnInit {
  users: User[] = [];
  
  // Die Abhängigkeiten werden im Konstruktor deklariert
  constructor(
    private userService: UserService,
    private logger: LoggerService,
    @Inject(API_URL) private apiUrl: string
  ) { }
  
  ngOnInit(): void {
    this.logger.log(`Initialisiere User-Liste mit API: ${this.apiUrl}`);
    this.userService.getUsers().subscribe(users => {
      this.users = users;
      this.logger.log(`${users.length} Benutzer geladen`);
    });
  }
}

Die Vorteile der Constructor Injection sind:

Seit Angular 14 ist die Constructor Injection weiterhin vollständig unterstützt, obwohl für viele Anwendungsfälle die neue inject()-Funktion eine elegantere Alternative bietet.

14.2.2 Injektoren (Injectors)

Injektoren sind die Kern-Maschinerie hinter dem DI-System in Angular. Sie sind für das Erstellen von Instanzen, das Caching und das Auflösen von Abhängigkeiten verantwortlich.

Die Hierarchie der Injektoren ist klar strukturiert:

// Beispiel für den Zugriff auf den Environment Injector
import { EnvironmentInjector, inject } from '@angular/core';

// In einer Komponente oder einem Service
const environmentInjector = inject(EnvironmentInjector);

14.2.3 Provider

Provider sind Anweisungen für Angular, wie Abhängigkeiten erstellt und bereitgestellt werden sollen. Angular unterstützt verschiedene Arten von Providern:

14.2.3.1 Class Provider

// Typischer Class Provider
providers: [
  { provide: MyService, useClass: MyService }
]

// Oder verkürzt:
providers: [MyService]

14.2.3.2 Value Provider

// Einen konstanten Wert bereitstellen
providers: [
  { provide: 'API_URL', useValue: 'https://api.example.com/v2' }
]

14.2.3.3 Factory Provider

// Komplexe Erstellung durch eine Factory-Funktion
providers: [
  { 
    provide: ConfigService, 
    useFactory: (http) => {
      // Dynamische Entscheidung basierend auf Umgebung oder anderen Faktoren
      return environment.production 
        ? new ProductionConfigService(http) 
        : new DevelopmentConfigService(http);
    },
    deps: [HttpClient] // Abhängigkeiten für die Factory-Funktion
  }
]

14.2.3.4 Existing Provider

// Alias für einen bestehenden Provider
providers: [
  { provide: AbstractLoggerService, useExisting: ConsoleLoggerService }
]

14.2.4 Tokens

Tokens sind Schlüssel, die von Injektoren verwendet werden, um Abhängigkeiten zu identifizieren. In Angular können verschiedene Arten von Tokens verwendet werden:

14.2.4.1 Klassen als Tokens

@Injectable()
export class UserService {
  // ...
}

// Injection
constructor(private userService: UserService) {}

14.2.4.2 String Tokens

// Definition
providers: [
  { provide: 'API_VERSION', useValue: 'v3' }
]

// Injection mit @Inject
constructor(@Inject('API_VERSION') private apiVersion: string) {}

14.2.4.3 InjectionTokens

Dies ist der bevorzugte Weg für nicht-Klassen-Token:

// Definition
export const API_VERSION = new InjectionToken<string>('API_VERSION', {
  providedIn: 'root',
  factory: () => 'v3'
});

// Injection
constructor(@Inject(API_VERSION) private apiVersion: string) {}
// ODER mit dem inject-Function (ab Angular 14)
const apiVersion = inject(API_VERSION);

14.2.5 Providebereiche (Provider Scopes)

Angular bietet verschiedene Bereiche, in denen Provider registriert werden können:

14.2.5.1 providedIn: ‘root’

Services sind für die gesamte Anwendung als Singleton verfügbar:

@Injectable({
  providedIn: 'root'
})
export class GlobalStateService {
  // ...
}

14.2.5.2 providedIn: ‘platform’

Für plattformübergreifende Singletons (selten verwendet):

@Injectable({
  providedIn: 'platform'
})
export class PlatformService {
  // ...
}

14.2.5.3 providedIn im eigenen Modul (oder Komponente bei Standalone Components)

// Bei NgModules
@Injectable({
  providedIn: UserModule
})
export class UserService {
  // ...
}

// Bei Standalone Components
@Injectable({
  providedIn: 'any' // Ab Angular 14: Pro Lazy-Load-Grenze ein neuer Singleton
})
export class FeatureService {
  // ...
}

14.2.5.4 Component-Level Provider

Provider, die nur für eine Komponente und ihre Kinder verfügbar sind:

@Component({
  selector: 'app-user-dashboard',
  templateUrl: './user-dashboard.component.html',
  providers: [UserDashboardService]  // Nur für diese Komponente und Kinder
})
export class UserDashboardComponent {
  // ...
}

14.3 Neuere Entwicklungen im Angular DI-System

14.3.1 Das inject() API

Die in Angular 14 eingeführte inject()-Funktion ist eine wichtige Erweiterung und wird für viele Anwendungsfälle empfohlen:

// Vorher (Constructor Injection)
constructor(
  private userService: UserService,
  private router: Router,
  @Inject(API_URL) private apiUrl: string
) { }

// Ab Angular 14 (inject API)
private userService = inject(UserService);
private router = inject(Router);
private apiUrl = inject(API_URL);

Diese Syntax ist besonders nützlich für: - Services in Standalone Components - Injection in Guards und Interceptors - Zugriff auf Abhängigkeiten außerhalb des Konstruktors

14.3.2 Hierarchisches DI-System mit besserer Tree-Shaking-Unterstützung

Das hierarchische DI-System wurde optimiert, um besseres Tree-Shaking zu ermöglichen:

// Service mit eigener Provider-Konfiguration
@Injectable({
  providedIn: 'root',
  useFactory: () => {
    return isPlatformBrowser(inject(PLATFORM_ID)) 
      ? new BrowserService() 
      : new ServerService();
  }
})
export class PlatformSpecificService {
  // ...
}

14.3.3 Self, SkipSelf, Optional und Host-Dekoratoren

Diese Dekoratoren wurden verbessert:

// Den eigenen Provider verwenden und nicht nach oben suchen
constructor(@Self() private logger: LoggerService) {}

// Den Provider auf höherer Ebene verwenden und den eigenen überspringen
constructor(@SkipSelf() private parentConfig: ConfigService) {}

// Eine optionale Abhängigkeit, die null sein kann
constructor(@Optional() private analytics: AnalyticsService) {}

// Nur bis zum Host-Element suchen (wichtig für Projektions-Szenarien)
constructor(@Host() private formGroup: FormGroupDirective) {}

14.4 Konstruktor Injection vs. inject()-Funktion

In modernem Angular stehen zwei Hauptmethoden für die Dependency Injection zur Verfügung: die klassische Constructor Injection und die neuere inject()-Funktion. Beide haben ihre spezifischen Vor- und Nachteile:

14.4.1 Constructor Injection

@Component({
  selector: 'app-product-details',
  templateUrl: './product-details.component.html'
})
export class ProductDetailsComponent implements OnInit {
  product: Product | null = null;
  isLoading = true;
  errorMessage = '';

  constructor(
    private productService: ProductService,
    private route: ActivatedRoute,
    private analyticsService: AnalyticsService,
    @Optional() private featureFlags?: FeatureFlagsService
  ) { }

  ngOnInit(): void {
    const productId = this.route.snapshot.paramMap.get('id');
    if (productId) {
      this.loadProduct(productId);
    }
  }

  private loadProduct(id: string): void {
    this.productService.getProduct(id).subscribe({
      next: (product) => {
        this.product = product;
        this.isLoading = false;
        this.analyticsService.logEvent('product_view', { productId: id });
        
        // Optional Service verwenden wenn verfügbar
        if (this.featureFlags?.isEnabled('new_product_layout')) {
          // Neue Features aktivieren
        }
      },
      error: (err) => {
        this.errorMessage = 'Produkt konnte nicht geladen werden';
        this.isLoading = false;
      }
    });
  }
}

Vorteile: - Klassischer Ansatz, der vielen Entwicklern vertraut ist - Gute IDE-Unterstützung und TypeScript-Integration - Klare Sichtbarkeit aller Abhängigkeiten - Gut für Klassen mit wenigen Abhängigkeiten

Nachteile: - Kann bei vielen Abhängigkeiten unübersichtlich werden - Nicht außerhalb des Konstruktors verwendbar - Mit Dekoratoren wie @Inject, @Optional etc. kann es langatmig werden

14.4.2 inject()-Funktion (ab Angular 14)

@Component({
  selector: 'app-product-details',
  templateUrl: './product-details.component.html'
})
export class ProductDetailsComponent implements OnInit {
  private productService = inject(ProductService);
  private route = inject(ActivatedRoute);
  private analyticsService = inject(AnalyticsService);
  private featureFlags = inject(FeatureFlagsService, { optional: true });
  
  product: Product | null = null;
  isLoading = true;
  errorMessage = '';

  ngOnInit(): void {
    const productId = this.route.snapshot.paramMap.get('id');
    if (productId) {
      this.loadProduct(productId);
    }
  }

  private loadProduct(id: string): void {
    this.productService.getProduct(id).subscribe({
      next: (product) => {
        this.product = product;
        this.isLoading = false;
        this.analyticsService.logEvent('product_view', { productId: id });
        
        if (this.featureFlags?.isEnabled('new_product_layout')) {
          // Neue Features aktivieren
        }
      },
      error: (err) => {
        this.errorMessage = 'Produkt konnte nicht geladen werden';
        this.isLoading = false;
      }
    });
  }
}

Vorteile: - Kürzere, prägnantere Syntax - Kann überall in der Klasse verwendet werden, nicht nur im Konstruktor - Optionale und andere spezielle Injektionen sind einfacher zu lesen - Besseres Tree-Shaking und Performance-Optimierungen

Nachteile: - Neuerer Ansatz, der für manche Teams ungewohnt sein könnte - Abhängigkeiten sind nicht gebündelt im Konstruktor sichtbar

14.4.3 Wann welche Methode verwenden?

In modernem Angular wird die inject()-Funktion für die meisten Anwendungsfälle empfohlen, besonders für:

Die Constructor Injection bleibt jedoch nützlich für: - Legacy-Code und bestehende Anwendungen - Klassen mit wenigen Abhängigkeiten - Einfachere Testbarkeit in bestimmten Szenarien - Teams, die einen konsistenten Stil im gesamten Codebase beibehalten möchten

14.5 Anwendungsbeispiele

14.5.1 Beispiel 1: Globaler State Service mit providedIn: ‘root’

// state.service.ts
@Injectable({
  providedIn: 'root'
})
export class StateService {
  private state = new BehaviorSubject<AppState>({ user: null, theme: 'light' });
  
  state$ = this.state.asObservable();
  
  updateUser(user: User): void {
    const currentState = this.state.getValue();
    this.state.next({ ...currentState, user });
  }
  
  toggleTheme(): void {
    const currentState = this.state.getValue();
    const newTheme = currentState.theme === 'light' ? 'dark' : 'light';
    this.state.next({ ...currentState, theme: newTheme });
  }
}

// In einer Komponente
@Component({
  selector: 'app-header',
  template: `
    <div [class]="theme">
      <button (click)="toggleTheme()">Toggle Theme</button>
      <span *ngIf="user">Welcome, {{user.name}}</span>
    </div>
  `
})
export class HeaderComponent implements OnInit {
  user: User | null = null;
  theme = 'light';
  
  // Ab Angular 14 inject-Syntax
  private stateService = inject(StateService);
  
  ngOnInit() {
    this.stateService.state$.subscribe(state => {
      this.user = state.user;
      this.theme = state.theme;
    });
  }
  
  toggleTheme() {
    this.stateService.toggleTheme();
  }
}

14.5.2 Beispiel 2: Komponenten-spezifischer Service mit DI-Hierarchie

// user-dashboard.service.ts
@Injectable()
export class UserDashboardService {
  private userId = inject(ActivatedRoute).snapshot.paramMap.get('id');
  private http = inject(HttpClient);
  
  getUserDetails() {
    return this.http.get<UserDetails>(`/api/users/${this.userId}`);
  }
  
  getUserActivities() {
    return this.http.get<Activity[]>(`/api/users/${this.userId}/activities`);
  }
}

// user-dashboard.component.ts
@Component({
  selector: 'app-user-dashboard',
  templateUrl: './user-dashboard.component.html',
  providers: [UserDashboardService]  // Komponenten-lokaler Service
})
export class UserDashboardComponent implements OnInit {
  userDetails?: UserDetails;
  activities: Activity[] = [];
  
  private dashboardService = inject(UserDashboardService);
  
  ngOnInit() {
    this.dashboardService.getUserDetails().subscribe(
      details => this.userDetails = details
    );
    
    this.dashboardService.getUserActivities().subscribe(
      activities => this.activities = activities
    );
  }
}

// Ein Kind-Komponent nutzt denselben Service-Instance
@Component({
  selector: 'app-activity-list',
  template: `
    <div *ngFor="let activity of activities">
      {{activity.name}} - {{activity.date | date}}
    </div>
  `
})
export class ActivityListComponent {
  activities: Activity[] = [];
  
  // Verwendet denselben Service-Instance wie die Eltern-Komponente
  private dashboardService = inject(UserDashboardService);
  
  ngOnInit() {
    this.dashboardService.getUserActivities().subscribe(
      activities => this.activities = activities
    );
  }
}

14.5.3 Beispiel 3: Factory Provider mit Environment-Abhängigkeiten

// app.config.ts
export const API_CONFIG = new InjectionToken<ApiConfig>('API_CONFIG');

// app.module.ts (oder entsprechendes Standalone-Setup)
providers: [
  {
    provide: API_CONFIG,
    useFactory: () => {
      const platformId = inject(PLATFORM_ID);
      const environmentInjector = inject(EnvironmentInjector);
      
      // Basierend auf Umgebung unterschiedliche Konfiguration bereitstellen
      if (isPlatformBrowser(platformId)) {
        return {
          apiUrl: 'https://api.example.com/v3',
          timeout: 30000,
          retryAttempts: 3
        };
      } else {
        return {
          apiUrl: 'http://localhost:4000/api',
          timeout: 5000,
          retryAttempts: 1
        };
      }
    }
  }
]

// api.service.ts
@Injectable({
  providedIn: 'root'
})
export class ApiService {
  private config = inject(API_CONFIG);
  private http = inject(HttpClient);
  
  getData<T>(endpoint: string): Observable<T> {
    return this.http.get<T>(`${this.config.apiUrl}/${endpoint}`)
      .pipe(
        timeout(this.config.timeout),
        retry(this.config.retryAttempts)
      );
  }
}

14.6 Die Angular Injector-Debugging API

Um das Debugging der Dependency Injection zu erleichtern, bietet Angular eine API zum Untersuchen des Injector-Status:

// In einer Komponente oder Service
import { Injector, inject } from '@angular/core';

// Zugriff auf den eigenen Injector
const injector = inject(Injector);

// Verfügbare Provider im aktuellen Injector anzeigen
console.log('Verfügbare Provider:', injector.get(ProviderToken));

// Injector-Baum navigieren
const parentInjector = injector.get(Injector);
console.log('Parent Provider:', parentInjector.get(ProviderToken));

// In Tests kann der Injector direkt abgefragt werden
it('should have the correct providers', () => {
  const fixture = TestBed.createComponent(MyComponent);
  const componentInjector = fixture.debugElement.injector;
  
  // Provider prüfen
  const myService = componentInjector.get(MyService);
  expect(myService).toBeInstanceOf(CustomMyServiceImplementation);
});

14.7 Best Practices für Dependency Injection in Angular

  1. Verwende inject() statt Constructor Injection für neuere Angular-Projekte: Die inject()-Funktion macht den Code übersichtlicher und ermöglicht besseres Tree-Shaking.

  2. Nutze providedIn wo möglich: Statt Providers in Modulen zu deklarieren, verwende die providedIn-Option für bessere Tree-Shaking-Unterstützung.

  3. Verwende InjectionToken für nicht-Klassen-Werte: Für Konfigurationen, Strings oder andere Primitives, nutze immer InjectionToken statt String-Tokens.

  4. Beachte die Hierarchie der Injektoren: Verstehe, auf welcher Ebene Services bereitgestellt werden, um unerwartetes Verhalten zu vermeiden.

  5. Nutze lazy loading mit separaten Injektoren: Für Features, die eigene Service-Instanzen benötigen, verwende Lazy-Loading in Kombination mit ‘providedIn: any’.

  6. Verwende Factory-Provider für komplexe Szenarien: Wenn die Service-Erstellung von Bedingungen oder anderen Diensten abhängt, nutze Factory-Provider.

  7. Implementiere OnDestroy für Aufräumarbeiten: Für Services mit Ressourcen, die freigegeben werden müssen, implementiere das OnDestroy-Interface.

14.8 Debugging des DI-Systems

Angular bietet verschiedene Debugging-Möglichkeiten für das DI-System:

// Injector-Debugging in Development-Mode
import { DebugElement } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';

describe('Component DI', () => {
  let fixture: ComponentFixture<TestComponent>;
  let debugElement: DebugElement;
  
  beforeEach(() => {
    fixture = TestBed.createComponent(TestComponent);
    debugElement = fixture.debugElement;
  });
  
  it('should provide the correct services', () => {
    // Injector des Elements untersuchen
    const injector = debugElement.injector;
    const service = injector.get(TestService);
    expect(service).toBeInstanceOf(TestService);
    
    // Provider-Hierarchie prüfen
    const parentInjector = injector.get(Injector);
    expect(parentInjector.get(ParentService)).toBeInstanceOf(ParentService);
  });
});

Dependency Injection ist ein fundamentales Konzept in Angular, das über die Versionen hinweg stetig verbessert wurde. Die Hauptvorteile sind:

Durch ein tiefes Verständnis des DI-Systems können Angular-Entwickler sauberen, modularen und gut testbaren Code schreiben, der optimal für Leistung und Wartbarkeit ist.