SOLID-Prinzipien
Die fuenf Prinzipien fuer guten OOP-Code: Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, Dependency Inversion.
Inhaltsverzeichnis
SOLID-Prinzipien
SOLID steht fuer fรผnf Design-Prinzipien, die zusammen einen Standard fuer sauberen, wartbaren objektorientierten Code bilden. Vom Robert C. Martin (โUncle Bobโ) popularisiert und heute in jedem Senior-Interview Thema.
- S - Single Responsibility Principle
- O - Open/Closed Principle
- L - Liskov Substitution Principle
- I - Interface Segregation Principle
- D - Dependency Inversion Principle
S - Single Responsibility Principle
Eine Klasse sollte einen Grund haben, sich zu aendern.
Oder anders: Eine Klasse sollte eine Verantwortung haben.
Schlecht
class User:
def __init__(self, name, email):
self.name = name
self.email = email
def save_to_db(self):
# DB-Code
...
def send_welcome_email(self):
# E-Mail-Code
...
def export_to_pdf(self):
# PDF-Code
...
User macht drei Dinge: Daten halten, speichern, mailen, exportieren. Aenderungen an der DB, der E-Mail-Library oder der PDF-Library betreffen alle diese Klasse.
Besser
class User:
def __init__(self, name, email):
self.name = name
self.email = email
class UserRepository:
def save(self, user): ...
class EmailService:
def send_welcome(self, user): ...
class PdfExporter:
def export(self, user): ...
Jede Klasse hat eine klare Verantwortung. Aenderungen isoliert.
In der Praxis
Typisches Zeichen einer SRP-Verletzung: Die Klasse hat Methoden, die auf verschiedenen Levels arbeiten - Business-Logik, Persistenz, UI gemischt.
O - Open/Closed Principle
Software-Module sollen offen fuer Erweiterung, aber geschlossen fuer Modifikation sein.
Du willst neue Features hinzufuegen, ohne bestehenden Code zu aendern.
Schlecht
class Rabatt:
def berechne(self, kunde_typ, betrag):
if kunde_typ == "standard":
return betrag * 0.05
elif kunde_typ == "premium":
return betrag * 0.10
elif kunde_typ == "vip":
return betrag * 0.20
# Neuer Typ "business" โ bestehenden Code aendern
Jeder neue Kunden-Typ erfordert eine Aenderung dieser Methode. Bei Tests und Rollouts gefaehrlich.
Besser (mit Strategy)
from abc import ABC, abstractmethod
class RabattStrategie(ABC):
@abstractmethod
def berechne(self, betrag): ...
class StandardRabatt(RabattStrategie):
def berechne(self, betrag): return betrag * 0.05
class PremiumRabatt(RabattStrategie):
def berechne(self, betrag): return betrag * 0.10
class VipRabatt(RabattStrategie):
def berechne(self, betrag): return betrag * 0.20
# Neuer Typ: einfach neue Klasse dazu - kein Aendern von existing Code
class BusinessRabatt(RabattStrategie):
def berechne(self, betrag): return betrag * 0.15
Neuer Code kommt dazu, alter Code bleibt unveraendert.
In der Praxis
Plugin-Systeme, Strategy Pattern, und der Aufbau wie bei React-Components (Props steuern Verhalten) sind alle Open/Closed-orientiert.
L - Liskov Substitution Principle
Objekte einer Subklasse muessen ohne Aenderung durch ihre Basisklasse ersetzbar sein.
Wenn Quadrat von Rechteck erbt, sollte Code, der Rechteck erwartet, mit Quadrat genauso funktionieren.
Der klassische Fehler: Rechteck / Quadrat
class Rechteck:
def __init__(self, breite, hoehe):
self.breite = breite
self.hoehe = hoehe
def set_breite(self, b): self.breite = b
def set_hoehe(self, h): self.hoehe = h
class Quadrat(Rechteck):
def __init__(self, seite):
super().__init__(seite, seite)
def set_breite(self, b):
self.breite = b
self.hoehe = b # Quadrat braucht gleiche Seiten
def set_hoehe(self, h):
self.breite = h
self.hoehe = h
Problem:
def test(rechteck):
rechteck.set_breite(5)
rechteck.set_hoehe(10)
assert rechteck.breite * rechteck.hoehe == 50
test(Rechteck(1, 1)) # OK
test(Quadrat(1)) # Fehler! 10*10 = 100 statt 50
Das Quadrat ist zwar mathematisch ein Rechteck - aber im Code-Verhalten bricht es die Basis-Vertraege.
Loesung
Wenn Vererbung Probleme macht: Composition statt Vererbung:
class Form:
def flaeche(self): ...
class Rechteck(Form):
def __init__(self, breite, hoehe):
self.breite = breite
self.hoehe = hoehe
def flaeche(self): return self.breite * self.hoehe
class Quadrat(Form):
def __init__(self, seite):
self.seite = seite
def flaeche(self): return self.seite ** 2
Beide sind Formen, aber keine erbt von der anderen. Kein LSP-Problem.
In der Praxis
Subklassen sollten nie:
- Exceptions werfen, die die Basis nicht wirft
- Precondition verstaerken (striktere Eingabe-Anforderungen)
- Postcondition abschwaechen (weniger garantierte Outputs)
I - Interface Segregation Principle
Klienten sollen nicht von Interfaces abhaengen, die sie nicht nutzen.
Zu grosse Interfaces zwingen Implementierungen zu Methoden, die sie eigentlich nicht brauchen.
Schlecht
from abc import ABC, abstractmethod
class Mitarbeiter(ABC):
@abstractmethod
def arbeiten(self): ...
@abstractmethod
def gehalt_bekommen(self): ...
@abstractmethod
def essen(self): ...
class MenschlicherMitarbeiter(Mitarbeiter):
def arbeiten(self): ...
def gehalt_bekommen(self): ...
def essen(self): ...
class Roboter(Mitarbeiter):
def arbeiten(self): ...
def gehalt_bekommen(self):
raise Exception("Roboter bekommen kein Gehalt") # Problem!
def essen(self):
raise Exception("Roboter essen nicht") # Problem!
Der Roboter wird gezwungen, Methoden zu implementieren, die er nicht braucht.
Besser
class Arbeitend(ABC):
@abstractmethod
def arbeiten(self): ...
class Entlohnt(ABC):
@abstractmethod
def gehalt_bekommen(self): ...
class Essend(ABC):
@abstractmethod
def essen(self): ...
class MenschlicherMitarbeiter(Arbeitend, Entlohnt, Essend):
def arbeiten(self): ...
def gehalt_bekommen(self): ...
def essen(self): ...
class Roboter(Arbeitend):
def arbeiten(self): ...
Kleine, fokussierte Interfaces - jedes fuer einen Aspekt.
In der Praxis
Go hat dieses Prinzip sprachlich verankert: Interfaces werden implizit implementiert, und sind oft sehr klein (z.B. Reader, Writer, Closer - statt einem grossen IOObject).
D - Dependency Inversion Principle
High-Level-Module sollten nicht von Low-Level-Modulen abhaengen. Beide sollten von Abstraktionen abhaengen.
Schlecht
class MySqlDatabase:
def save(self, data): ...
class UserService:
def __init__(self):
self.db = MySqlDatabase() # fest verdrahtet
def register(self, user):
self.db.save(user)
UserService ist an MySqlDatabase gebunden. Testen geht nicht ohne echte DB. Austausch (z.B. PostgreSQL, SQLite fuer Tests) erfordert Aenderungen.
Besser
from abc import ABC, abstractmethod
class UserRepository(ABC):
@abstractmethod
def save(self, user): ...
class MySqlUserRepo(UserRepository):
def save(self, user): ...
class PostgresUserRepo(UserRepository):
def save(self, user): ...
class UserService:
def __init__(self, repo: UserRepository):
self.repo = repo
def register(self, user):
self.repo.save(user)
# Nutzen
service = UserService(MySqlUserRepo())
# oder fuer Tests:
service = UserService(FakeUserRepo())
UserService haengt jetzt nur von der Abstraktion ab - austauschbar, testbar, flexibel.
In der Praxis
Das ist das Prinzip hinter Dependency Injection (DI). Frameworks wie Spring (Java), .NET DI-Container oder InversifyJS (TypeScript) automatisieren das.
Selbst ohne Framework: Einfach Dependencies als Parameter uebergeben - nicht intern erstellen.
Alle fuenf zusammen
Gutes OOP-Design:
- Klassen mit klaren Verantwortungen (SRP)
- Erweiterbar ohne Aenderung (OCP)
- Subklassen sind echte Spezialisierungen (LSP)
- Kleine, fokussierte Interfaces (ISP)
- Abhaengigkeiten auf Abstraktionen (DIP)
Das Ergebnis: Code, der einfach testbar, einfach erweiterbar, einfach verstaendlich ist.
Kritik und Nuancen
Overengineering-Gefahr
SOLID konsequent anzuwenden fuehrt leicht zu vielen kleinen Klassen und Interfaces - das kann ueberdimensioniert sein.
Faustregel: Wende SOLID an, wenn du aktiv Schmerz spuerst, den die Prinzipien loesen. Nicht praeventiv.
Nicht alle Sprachen sind OOP
SOLID-Prinzipien sind OOP-zentrisch. In funktionalen Sprachen (Elixir, Haskell) oder bei data-oriented design gelten andere Prinzipien.
Beispielhafte Vereinfachungen
Der Quadrat/Rechteck-Fall ist didaktisch - in echten Codebases sind SOLID-Verletzungen oft subtiler.
YAGNI und KISS
Zwei wichtige Gegenprinzipien:
- YAGNI - โYou Arenโt Gonna Need Itโ - baue nicht fuer Features, die vielleicht kommen
- KISS - โKeep It Simple, Stupidโ - einfache Loesungen sind meistens die besten
SOLID ist Werkzeug, nicht Dogma. Einfacher Code, der funktioniert und verstaendlich ist, schlaegt โperfektenโ Over-Engineering.
Zusammenfassung
- S - Single Responsibility: eine Klasse, eine Verantwortung
- O - Open/Closed: offen fuer Erweiterung, geschlossen fuer Aenderung
- L - Liskov Substitution: Subklassen muessen Basis ersetzen koennen
- I - Interface Segregation: kleine, fokussierte Interfaces
- D - Dependency Inversion: abhaengig von Abstraktionen, nicht Konkretem
Im naechsten Kapitel: Singleton und Factory - die klassischen Creational Patterns.