Die SOLID-Prinzipien sind fünf Designregeln der objektorientierten Programmierung, die Robert C. Martin (Uncle Bob) zusammengefasst hat. Sie helfen dabei, Code zu schreiben, der wartbar, erweiterbar und testbar bleibt — auch wenn Anforderungen sich ändern.
In diesem Leitfaden lernen Sie alle fünf Prinzipien mit Python-Beispielen kennen: von schlechten zu guten Varianten, mit praktischen Tipps und typischen Fallstricken. Keine Vorkenntnisse in OOP-Design nötig — nur grundlegendes Python.
Übersicht: Die fünf SOLID-Prinzipien
| Prinzip | Kernaussage |
|---|---|
| Single Responsibility | Eine Klasse, eine Verantwortung |
| Open/Closed | Offen für Erweiterung, geschlossen für Änderung |
| Liskov-Substitution | Unterklassen müssen Basisklassen ersetzen können |
| Interface Segregation | Kleine, spezifische Schnittstellen statt einer großen |
| Dependency Inversion | Von Abstraktionen abhängen, nicht von Konkretem |
1. Single Responsibility Principle (SRP)
Eine Klasse soll nur eine einzige Verantwortung haben — einen Grund, warum sie geändert werden muss. Wenn Sie mehrere Gründe finden, die Klasse anzupassen, verletzen Sie das SRP.
Schlecht: Eine Klasse, die sowohl Benutzerdaten speichert als auch E-Mails versendet und PDFs erstellt:
class User:
def __init__(self, name, email):
self.name = name
self.email = email
def save_to_database(self):
# Speichert in DB
pass
def send_email(self, subject, body):
# Versendet E-Mail
pass
def export_to_pdf(self):
# Erstellt PDF
pass
Gut: Verantwortlichkeiten trennen — jede Klasse macht genau eine Sache:
class User:
def __init__(self, name, email):
self.name = name
self.email = email
class UserRepository:
def save(self, user):
pass
class EmailService:
def send(self, to, subject, body):
pass
class PdfExporter:
def export(self, user):
pass
Fragen Sie sich: „Wenn sich die Anforderungen an X ändern, muss ich diese Klasse anfassen?“ Wenn ja und X nicht die Hauptverantwortung ist, ziehen Sie X in eine eigene Klasse aus.
Zu früh zu granular zu werden. Eine Klasse mit drei eng zusammenhängenden Methoden ist oft in Ordnung. Erst wenn Änderungen an verschiedenen Stellen unterschiedliche Gründe haben, lohnt sich die Aufteilung.
2. Open/Closed Principle (OCP)
Software-Einheiten sollen offen für Erweiterung, aber geschlossen für Änderung sein. Neue Funktionalität fügen Sie durch neue Klassen hinzu, nicht durch Änderung bestehenden Codes.
Schlecht: Eine Funktion mit vielen if/elif für jeden neuen Typ:
def calculate_area(shape):
if shape["type"] == "circle":
return 3.14 * shape["radius"] ** 2
elif shape["type"] == "rectangle":
return shape["width"] * shape["height"]
elif shape["type"] == "triangle":
return 0.5 * shape["base"] * shape["height"]
# Jeder neue Typ = Codeänderung!
Gut: Polymorphismus nutzen — neue Formen durch neue Klassen, ohne bestehenden Code zu ändern:
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self):
pass
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
return 3.14 * self.radius ** 2
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
# Neuer Typ = neue Klasse, kein Ändern von calculate_area
Wenn Sie beim Hinzufügen eines neuen Falls eine bestehende Funktion oder Klasse anpassen müssen, prüfen Sie, ob Abstraktion (ABC, Protocol) die Lösung ist.
OCP zu früh anzuwenden. Für zwei oder drei Fälle reicht ein einfaches if/elif. Erst bei vier oder mehr Varianten lohnt sich die Abstraktion.
3. Liskov Substitution Principle (LSP)
Unterklassen müssen ihre Basisklassen ohne Überraschungen ersetzen können. Wer eine Basisklasse erwartet, darf eine Unterklasse übergeben bekommen und das Programm muss korrekt weiterlaufen.
Schlecht: Eine Unterklasse, die das Verhalten der Basisklasse bricht:
class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
class Square(Rectangle):
def __init__(self, side):
super().__init__(side, side)
def set_width(self, w):
self.width = self.height = w # Bricht Erwartung!
def set_height(self, h):
self.width = self.height = h # Bricht Erwartung!
Gut: Unterklassen erfüllen die Verträge der Basisklasse. Square sollte nicht von Rectangle erben, wenn sich das Verhalten unterscheidet:
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self):
pass
class Rectangle(Shape):
def __init__(self, width, height):
self._width = width
self._height = height
def area(self):
return self._width * self._height
class Square(Shape):
def __init__(self, side):
self._side = side
def area(self):
return self._side ** 2
# Beide sind Shapes, austauschbar wo area() gebraucht wird
Bevor Sie erben, fragen Sie: „Kann die Unterklasse überall dort eingesetzt werden, wo die Basisklasse erwartet wird?“ Wenn nicht, ist Vererbung falsch.
Vererbung nur wegen Code-Wiederverwendung. „Hat-a“-Beziehungen (Komposition) sind oft besser als „Ist-ein“ (Vererbung), wenn das Verhalten abweicht.
4. Interface Segregation Principle (ISP)
Klienten sollen nicht von Schnittstellen abhängen, die sie nicht nutzen. Lieber viele kleine, spezifische Interfaces als ein großes, das alles kann.
Schlecht: Ein riesiges Interface, das alle Methoden erzwingt:
from abc import ABC, abstractmethod
class Worker(ABC):
@abstractmethod
def work(self): pass
@abstractmethod
def eat(self): pass
@abstractmethod
def sleep(self): pass
class Robot(Worker):
def work(self): return "working"
def eat(self): pass # Robot braucht das nicht!
def sleep(self): pass # Robot braucht das nicht!
Gut: Interfaces nach Bedarf aufteilen:
from abc import ABC, abstractmethod
class Workable(ABC):
@abstractmethod
def work(self): pass
class Eatable(ABC):
@abstractmethod
def eat(self): pass
class Human(Workable, Eatable):
def work(self): return "working"
def eat(self): return "eating"
class Robot(Workable):
def work(self): return "working"
# Kein eat/sleep nötig
In Python nutzen Sie Protocol aus typing für strukturelle Subtyping — noch flexibler als ABC.
Zu viele Mini-Interfaces. Jedes Interface sollte einen klaren Zweck haben. Drei Methoden pro Interface sind oft ein guter Richtwert.
5. Dependency Inversion Principle (DIP)
Abstraktionen sollen von Details abhängen — nicht umgekehrt. High-Level-Module sollen nicht von Low-Level-Modulen abhängen; beide sollen von Abstraktionen abhängen.
Schlecht: Direkte Abhängigkeit von konkreten Implementierungen:
class EmailNotifier:
def send(self, msg):
print("Email:", msg)
class OrderService:
def __init__(self):
self.notifier = EmailNotifier() # Fest verdrahtet!
def place_order(self, order):
# ... Bestellung verarbeiten ...
self.notifier.send("Bestellung aufgegeben")
Gut: Abhängigkeit von einer Abstraktion, Implementierung wird von außen injiziert:
from abc import ABC, abstractmethod
class Notifier(ABC):
@abstractmethod
def send(self, msg): pass
class EmailNotifier(Notifier):
def send(self, msg):
print("Email:", msg)
class SmsNotifier(Notifier):
def send(self, msg):
print("SMS:", msg)
class OrderService:
def __init__(self, notifier: Notifier):
self.notifier = notifier # Abstraktion, austauschbar!
def place_order(self, order):
self.notifier.send("Bestellung aufgegeben")
Dependency Injection macht Tests einfacher — Sie können Mock-Notifier injizieren, ohne echte E-Mails zu versenden.
Für jede kleine Klasse sofort Abstraktionen einzuführen. Bei einfachen Hilfsklassen (z.B. StringUtils) ist direkte Nutzung völlig in Ordnung.
Zusammenfassung: Schlecht vs. Gut
| Prinzip | Schlecht | Gut |
|---|---|---|
| SRP | Eine Klasse macht alles | Eine Verantwortung pro Klasse |
| OCP | if/elif für jeden neuen Fall | Neue Klassen für neue Fälle |
| LSP | Unterklasse bricht Vertrag | Unterklasse ersetzbar |
| ISP | Riesiges Interface | Kleine, fokussierte Interfaces |
| DIP | Direkte Abhängigkeit von Konkretem | Abhängigkeit von Abstraktion |
Praxisanwendung
SOLID ist kein Dogma. Wenden Sie die Prinzipien an, wenn sie Ihnen helfen — nicht, um Code künstlich „sauber“ zu machen. Ein 50-Zeilen-Skript braucht oft keine Abstraktionen. Ein Projekt mit mehreren Entwicklern und langer Laufzeit profitiert stark.
Weitere Themen, die gut zu SOLID passen: Clean Code, Design Patterns und Test-Driven Development.
Fazit
Die SOLID-Prinzipien sind Leitlinien für wartbaren, erweiterbaren Code. Mit Python können Sie sie durch ABCs, Protocols und Dependency Injection umsetzen. Beginnen Sie mit SRP und DIP — sie bringen in der Praxis den größten Nutzen.
Wann SOLID anwenden?
Kleine Skripte und Prototypen brauchen oft keine SOLID-Struktur. Sobald aber mehrere Entwickler am Code arbeiten, Tests geschrieben werden oder das Projekt länger lebt, zahlen sich die Prinzipien aus. 💡 Tipp: Beginnen Sie mit SRP — es ist am einfachsten umzusetzen und bringt sofort Nutzen.
Python-spezifische Umsetzung
Python hat keine klassischen Interfaces wie Java. Nutzen Sie abc.ABC und @abstractmethod für formale Verträge, oder typing.Protocol für strukturelles Subtyping (Duck Typing mit Typ-Prüfung). Dependency Injection können Sie per Konstruktor, Setter oder Frameworks wie dependency-injector umsetzen.
Weiterführende Ressourcen
Für vertiefende Lektüre: Robert C. Martins „Clean Architecture“, das Python-Design-Patterns-Artikel und unser Einstieg in Python für die Grundlagen.
Häufige Fragen
Muss ich alle fünf Prinzipien immer einhalten? Nein. Sie sind Leitlinien. Bei Trade-offs (z.B. Einfachheit vs. strikte Einhaltung) entscheidet der Kontext.
Gilt SOLID auch für funktionale Programmierung? Die Ideen übertragen sich: SRP (eine Funktion, eine Sache), DIP (Funktionen als Parameter). OCP, LSP und ISP sind stärker OOP-geprägt.
Kurzüberblick
SOLID = fünf Prinzipien für wartbaren Code. SRP und DIP bringen in Python-Projekten den größten praktischen Nutzen. Wenden Sie sie an, wenn der Code wächst — nicht von Anfang an in jedem Skript.

Schreibe einen Kommentar