Was sind die SOLID-Prinzipien? Einsteiger-Leitfaden zu den 5 Grundprinzipien der objektorientierten Programmierung [Mit Python-Beispielen]

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

PrinzipKernaussage
Single ResponsibilityEine Klasse, eine Verantwortung
Open/ClosedOffen für Erweiterung, geschlossen für Änderung
Liskov-SubstitutionUnterklassen müssen Basisklassen ersetzen können
Interface SegregationKleine, spezifische Schnittstellen statt einer großen
Dependency InversionVon 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:

schlecht_srp.py
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:

gut_srp.py
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
💡 Tipp

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.

⚠️ Häufige Falle

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:

schlecht_ocp.py
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:

gut_ocp.py
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
💡 Tipp

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.

⚠️ Häufige Falle

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:

schlecht_lsp.py
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:

gut_lsp.py
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
💡 Tipp

Bevor Sie erben, fragen Sie: „Kann die Unterklasse überall dort eingesetzt werden, wo die Basisklasse erwartet wird?“ Wenn nicht, ist Vererbung falsch.

⚠️ Häufige Falle

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:

schlecht_isp.py
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:

gut_isp.py
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
💡 Tipp

In Python nutzen Sie Protocol aus typing für strukturelle Subtyping — noch flexibler als ABC.

⚠️ Häufige Falle

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:

schlecht_dip.py
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:

gut_dip.py
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")
💡 Tipp

Dependency Injection macht Tests einfacher — Sie können Mock-Notifier injizieren, ohne echte E-Mails zu versenden.

⚠️ Häufige Falle

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

PrinzipSchlechtGut
SRPEine Klasse macht allesEine Verantwortung pro Klasse
OCPif/elif für jeden neuen FallNeue Klassen für neue Fälle
LSPUnterklasse bricht VertragUnterklasse ersetzbar
ISPRiesiges InterfaceKleine, fokussierte Interfaces
DIPDirekte Abhängigkeit von KonkretemAbhä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.

Comments

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert