SOLID-Prinzipien
Fünf fundamentale Designprinzipien der objektorientierten Programmierung, die zu wartbarem, erweiterbarem und testbarem Code führen.
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.
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.
DI ist die praktische Umsetzung von zwei SOLID-Prinzipien:
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.
# 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
# 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"
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 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}")
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 |
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.
Klassen erhalten ihre Abhängigkeiten von außen statt sie selbst zu erstellen
Ermöglicht einfaches Austauschen von Abhängigkeiten – besonders für Unit Tests
Fördert das Dependency-Inversion-Prinzip aus SOLID
Unit Testing
Echte Datenbankverbindungen durch Mock-Objekte ersetzen, ohne den Code zu ändern
Konfigurierbare KI-Pipelines
LLM-Provider austauschen (OpenAI ↔ Claude) ohne Änderungen an der Business-Logik
Plugin-Architekturen
Verschiedene Implementierungen eines Interfaces zur Laufzeit einsetzen
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.
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.
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.