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:
| Scope | Bedeutung | Typischer Use Case |
|---|---|---|
| Transient | Neue Instanz bei jeder Anfrage | Stateless Services |
| Scoped | Eine Instanz pro Request | Datenbankverbindung |
| Singleton | Eine Instanz für die gesamte App | Konfiguration, 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.
Konkrete Abhängigkeiten sichtbar machen
01
Prüfe, welche Datenquellen, Dienste oder Konfigurationen eine Komponente tatsächlich benötigt.
Komponente → Datenzugriff | Client | Speicher | ZeitStabilen Vertrag definieren
02
Beschreibe nur die Fähigkeiten, die die Fachlogik braucht, statt eine konkrete Bibliothek oder Infrastruktur durchzureichen.
Interface → benötigte Operationen → klare GrenzenImplementierung am Rand zusammenbauen
03
Die Anwendung entscheidet an einer zentralen Stelle, welche reale Implementierung in welcher Umgebung verwendet wird.
Composition Root → konkrete Implementierungen → AnwendungFachlogik unabhängig testen
04
Tests übergeben einfache, kontrollierte Test-Doubles und prüfen Verhalten ohne Netzwerk oder Datenbank.
Test → Fake / Stub → Fachlogik → erwartetes ErgebnisAbhä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 FortgeschritteneAbhängigkeiten am Systemrand
Fachlogik arbeitet gegen kleine Verträge; die konkrete Infrastruktur wird zentral verbunden.
Eine Komponente kennt benötigte Fähigkeiten, nicht deren konkrete technische Umsetzung.