Dependency Injection

Wie Teams Abhängigkeiten sichtbar machen, Implementierungen austauschbar halten und Geschäftslogik ohne schwere Infrastruktur testen.

Ein Entwurfsmuster, bei dem Abhängigkeiten einer Klasse nicht intern erstellt, sondern von außen übergeben werden – für bessere Testbarkeit, Flexibilität und lose Kopplung.

Fortgeschritten2 Min Lesezeit

Erklärung starten

Auf einen Blick

3 Punkte

  1. Klassen erhalten ihre Abhängigkeiten von außen statt sie selbst zu erstellen
  2. Ermöglicht einfaches Austauschen von Abhängigkeiten – besonders für Unit Tests
  3. Fördert das Dependency-Inversion-Prinzip aus SOLID

Sofortantwort

Dependency Injection einfach erklärt

Abhängigkeiten werden von außen übergeben statt intern erstellt – für bessere Testbarkeit.

Kurz gesagt
Klassen erhalten ihre Abhängigkeiten von außen statt sie selbst zu erstellen
Typischer Einsatz
Unit Testing, Konfigurierbare KI-Pipelines, Plugin-Architekturen
Wichtig zu wissen
Fördert das Dependency-Inversion-Prinzip aus SOLID

Dependency Injection im Überblick

Stell dir vor, eine Klasse ReportGenerator braucht eine Datenbankverbindung. Ohne DI erstellt sie diese selbst:

class ReportGenerator:
    def __init__(self):
        self.db = PostgresDatabase("localhost:5432")  # Hardcodiert!

Das Problem: Für Tests braucht man eine echte Datenbank. Und wenn man die Datenbank wechselt, muss man den Code ändern. Die Klasse ist eng an eine konkrete Implementierung gekoppelt.

Mit Dependency Injection wird die Abhängigkeit von außen übergeben:

class ReportGenerator:
    def __init__(self, db: Database):  # Interface, nicht konkrete Klasse
        self.db = db

Jetzt kann man im Test eine Mock-Datenbank übergeben, in Production die echte – der ReportGenerator merkt keinen Unterschied.

Verbindung zu SOLID

DI ist die praktische Umsetzung von zwei SOLID-Prinzipien:

  • D – Dependency Inversion Principle: High-Level-Module sollen nicht von Low-Level-Modulen abhängen, beide sollen von Abstraktionen abhängen
  • O – Open/Closed Principle: Klassen sollen offen für Erweiterung, geschlossen für Modifikation sein – durch DI kann man Verhalten ändern, ohne Code zu ändern

Anti-Pattern: Service Locator

DI hat ein häufig verwechseltes Anti-Pattern:

# ❌ Service Locator: Abhängigkeiten werden aktiv geholt
class ReportGenerator:
    def __init__(self):
        self.db = ServiceLocator.get(Database)  # Versteckte Abhängigkeit!

# ✅ Dependency Injection: Abhängigkeiten werden übergeben
class ReportGenerator:
    def __init__(self, db: Database):  # Explizite Abhängigkeit
        self.db = db

Der Service Locator versteckt Abhängigkeiten – man sieht nicht am Konstruktor, was die Klasse braucht. DI macht Abhängigkeiten explizit und sichtbar.

Technisch betrachtet

Die drei Injektionsarten

# 1. Constructor Injection (empfohlen – alle Abhängigkeiten sofort sichtbar)
class LLMService:
    def __init__(self, llm_client: LLMClient, cache: Cache):
        self.client = llm_client
        self.cache = cache

# 2. Setter Injection (für optionale Abhängigkeiten)
class LLMService:
    def set_cache(self, cache: Cache) -> None:
        self.cache = cache

# 3. Property Injection (selten, nur wenn Konstruktor nicht kontrollierbar)
class LLMService:
    cache: Cache = InMemoryCache()  # Default-Implementierung

Testbarkeit durch DI

# Production: echte Implementierungen
service = LLMService(
    llm_client=OpenAIClient(api_key=settings.OPENAI_KEY),
    cache=RedisCache(host="redis:6379")
)

# Unit Test: Mock-Implementierungen – kein Netzwerk, kein Redis
service = LLMService(
    llm_client=MockLLMClient(responses={"Hallo": "Welt"}),
    cache=InMemoryCache()
)
result = service.generate("Hallo")
assert result == "Welt"

TypeScript mit InversifyJS

import { injectable, inject, Container } from 'inversify';

// Interfaces definieren
interface LLMClient { generate(prompt: string): Promise<string>; }
interface Cache { get(key: string): string | null; set(key: string, value: string): void; }

// Implementierungen
@injectable()
class OpenAIClient implements LLMClient {
  async generate(prompt: string) { /* ... */ }
}

@injectable()
class RedisCache implements Cache {
  get(key: string) { /* ... */ }
  set(key: string, value: string) { /* ... */ }
}

// Service mit injizierten Abhängigkeiten
@injectable()
class LLMService {
  constructor(
    @inject('LLMClient') private client: LLMClient,
    @inject('Cache') private cache: Cache,
  ) {}
}

// Container konfigurieren
const container = new Container();
container.bind<LLMClient>('LLMClient').to(OpenAIClient);
container.bind<Cache>('Cache').to(RedisCache);
container.bind(LLMService).toSelf();

const service = container.get(LLMService);

FastAPI Dependency Injection

FastAPI hat DI eingebaut – besonders nützlich für Datenbankverbindungen und Auth:

from fastapi import Depends, HTTPException
from functools import lru_cache

# Dependency-Funktionen
def get_llm_client() -> LLMClient:
    return OpenAIClient(api_key=settings.OPENAI_KEY)

def get_db() -> Generator:
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

async def get_current_user(
    token: str = Depends(oauth2_scheme),
    db: Session = Depends(get_db)
) -> User:
    user = db.query(User).filter(User.token == token).first()
    if not user:
        raise HTTPException(status_code=401)
    return user

# Endpunkt mit mehreren Abhängigkeiten
@app.post("/generate")
async def generate(
    prompt: str,
    llm: LLMClient = Depends(get_llm_client),
    user: User = Depends(get_current_user),
):
    return await llm.generate(f"User {user.name}: {prompt}")

Scopes in DI-Containern

DI-Container unterstützen verschiedene Lebenszyklen:

ScopeBedeutungTypischer Use Case
TransientNeue Instanz bei jeder AnfrageStateless Services
ScopedEine Instanz pro RequestDatenbankverbindung
SingletonEine Instanz für die gesamte AppKonfiguration, HTTP-Client

Schritt für Schritt

Dependency Injection bewusst einsetzen

DI schafft nicht automatisch gute Architektur. Sie hilft, wenn eine Komponente wirklich mit austauschbaren oder externen Abhängigkeiten arbeiten soll.

  1. Konkrete Abhängigkeiten sichtbar machen

    01

    Prüfe, welche Datenquellen, Dienste oder Konfigurationen eine Komponente tatsächlich benötigt.

    Komponente → Datenzugriff | Client | Speicher | Zeit
  2. Stabilen Vertrag definieren

    02

    Beschreibe nur die Fähigkeiten, die die Fachlogik braucht, statt eine konkrete Bibliothek oder Infrastruktur durchzureichen.

    Interface → benötigte Operationen → klare Grenzen
  3. Implementierung am Rand zusammenbauen

    03

    Die Anwendung entscheidet an einer zentralen Stelle, welche reale Implementierung in welcher Umgebung verwendet wird.

    Composition Root → konkrete Implementierungen → Anwendung
  4. Fachlogik unabhängig testen

    04

    Tests übergeben einfache, kontrollierte Test-Doubles und prüfen Verhalten ohne Netzwerk oder Datenbank.

    Test → Fake / Stub → Fachlogik → erwartetes Ergebnis
  5. Abhängigkeiten klein halten

    05

    Wenn Konstruktoren zu viele Dienste brauchen, ist das ein Signal für zu breite Verantwortung oder fehlende Zwischengrenzen.

    viele Dependencies → Verantwortung prüfen → Schnitt neu schneiden

Konkretes Beispiel

Beispiel: Austauschbarer Modellanbieter

Eine Fachfunktion braucht Texte zusammenzufassen, soll aber nicht direkt an einen einzelnen Anbieter gebunden sein.

Abhängigkeit

Die Business-Logik benötigt nur die Fähigkeit, einen Text mit definierten Grenzen zu verarbeiten – nicht die Details eines SDKs.

Entkoppelter Entwurf

Ein kleiner Vertrag für die Zusammenfassung, konkrete Adapter für Anbieter oder lokale Varianten und einfache Testimplementierungen für die Fachlogik.

DI trennt die Frage, was eine Komponente benötigt, von der Frage, wie diese Fähigkeit technisch bereitgestellt wird.

Einordnung

Vorteile und Grenzen

Das spricht dafür

  • Bessere Testbarkeit: Fachlogik lässt sich ohne schwergewichtige externe Systeme prüfen.
  • Austauschbarkeit: Infrastruktur oder Anbieter können hinter klaren Verträgen gewechselt werden.
  • Sichtbare Verantwortung: Konstruktoren zeigen, welche Abhängigkeiten eine Komponente wirklich hat.

Das solltest du beachten

  • Mehr Abstraktion: Für einfache, lokale Helfer kann DI unnötigen Code und Indirektion erzeugen.
  • Schlechte Verträge helfen nicht: Zu breite Interfaces verstecken Kopplung nur hinter einem Namen.
  • Zusammensetzen bleibt Aufgabe: Die richtige Konfiguration der realen Implementierungen muss klar gepflegt werden.
Vertiefung · für Fortgeschrittene

Abhängigkeiten am Systemrand

Fachlogik arbeitet gegen kleine Verträge; die konkrete Infrastruktur wird zentral verbunden.

Wissenskarte

Eine Komponente kennt benötigte Fähigkeiten, nicht deren konkrete technische Umsetzung.

Dependency Injection reduziert Kopplung, wenn die Schnittstellen aus tatsächlichen fachlichen Bedürfnissen entstehen. Fachlogik gliedert sich in: Interface, Adapter, Datenbank, API-Client, Test Double, Composition Root.

01Einsatzbereiche

Wann ist Dependency Injection sinnvoll?

Geeignet für

  • Unit TestingEchte Datenbankverbindungen durch Mock-Objekte ersetzen, ohne den Code zu ändern
  • Konfigurierbare KI-PipelinesLLM-Provider austauschen (OpenAI ↔ Claude) ohne Änderungen an der Business-Logik
  • Plugin-ArchitekturenVerschiedene Implementierungen eines Interfaces zur Laufzeit einsetzen

↑ Inhalt

02Werkzeuge

Womit Dependency Injection umgesetzt wird

↑ Inhalt

Merksatz

Dependency Injection ist wie ein Restaurant, das seine Zutaten geliefert bekommt, statt selbst anzubauen

Der Koch (Klasse) kocht das Gericht, aber er pflanzt das Gemüse nicht selbst an. Jemand anderes (der Injector) liefert die Zutaten. So kann man dem Koch für Tests auch Kunstgemüse geben – er merkt keinen Unterschied.

  1. Klassen erhalten ihre Abhängigkeiten von außen statt sie selbst zu erstellen
  2. Ermöglicht einfaches Austauschen von Abhängigkeiten – besonders für Unit Tests
  3. Fördert das Dependency-Inversion-Prinzip aus SOLID

04Redaktion

Herkunft und Stand

Redaktion und Aktualität

Ebenex RedaktionRedaktion

Veröffentlicht
Aktualisiert

Dieses Feld entwickelt sich schnell. Oben stehen Veröffentlichung und letzte Änderung; ein Prüfdatum kommt dazu, sobald die Erklärung nach ihrer letzten Änderung geprüft wurde.

↑ Inhalt

05FAQ

Häufige Fragen zu Dependency Injection

Was ist der Unterschied zwischen Dependency Injection und Dependency Inversion?

Dependency Inversion ist ein Prinzip (aus SOLID): High-Level-Module sollen nicht von Low-Level-Modulen abhängen, beide sollen von Abstraktionen abhängen. Dependency Injection ist eine Technik, um dieses Prinzip umzusetzen: Abhängigkeiten werden über Interfaces definiert und von außen injiziert.

Welche Arten von Dependency Injection gibt es?

Constructor Injection: Abhängigkeiten werden im Konstruktor übergeben (empfohlen). Setter Injection: Abhängigkeiten werden über Setter-Methoden gesetzt. Property Injection: Direkte Zuweisung an Properties. Constructor Injection ist am klarsten, weil alle Abhängigkeiten sofort sichtbar sind.

Brauche ich einen DI-Container?

Nicht unbedingt. In kleinen Projekten reicht manuelles DI: Objekte im Hauptprogramm erstellen und übergeben. DI-Container (wie Spring oder InversifyJS) automatisieren das für große Projekte mit vielen Abhängigkeiten.

↑ Inhalt

06Weiterlernen

Was möchtest du als Nächstes verstehen?

  • Grundlage nachholen

    SOLID-Prinzipien

    Fünf Prinzipien, die die Wartbarkeit und Erweiterbarkeit von Code fördern.

  • Technisch vertiefen

    Design Patterns

    Bewährte Lösungen für häufige Probleme in der Softwareentwicklung.

↑ Inhalt