Warum verbessern die SOLID-Prinzipien die Softwarearchitektur?
Vom funktionierenden zum wartbaren Code
Aus den Prinzipien für sauberen Code kennst du bereits Konzepte wie DRY (Don't Repeat Yourself) und KISS (Keep It Simple, Stupid). Während diese dir helfen, einzelne Methoden und Funktionen übersichtlich zu schreiben, setzen die SOLID-Prinzipien eine Ebene höher an: bei der Architektur deiner gesamten objektorientierten Anwendung.
Selbst wenn dein Code fehlerfrei läuft, kann ein schlechtes Design dazu führen, dass eine kleine Änderung an einer Stelle unerwartete Fehler an ganz anderen Stellen verursacht. Die SOLID-Prinzipien sind fünf bewährte Leitlinien, um Software so zu entwerfen, dass sie leicht verständlich, flexibel erweiterbar und einfach zu warten ist. Sie helfen dir, starren "Spaghetti-Code" zu vermeiden.
Die fünf Säulen der Softwarequalität
Das Akronym SOLID fasst fünf spezifische Entwurfsprinzipien zusammen. Ihr gemeinsames Ziel ist es, die Kohäsion (den logischen Zusammenhalt innerhalb eines Moduls) zu maximieren und die Kopplung (die Abhängigkeit zwischen verschiedenen Modulen) zu minimieren:
- Single Responsibility Principle (SRP)
- Open/Closed Principle (OCP)
- Liskov Substitution Principle (LSP)
- Interface Segregation Principle (ISP)
- Dependency Inversion Principle (DIP)
Wenn du diese Prinzipien verinnerlichst, schaffst du unabhängige Module, die dein Team auch Jahre später noch problemlos testen, anpassen und austauschen kann.
Wie sorgen wir für klare Verantwortung und Erweiterbarkeit?
Single Responsibility Principle (SRP)
Stell dir eine Klasse Rechnung vor. Sie berechnet die Gesamtsumme, speichert die Daten in der Datenbank und generiert ein PDF für den E-Mail-Versand. Ändert sich das Datenbank-Passwort, musst du die Klasse anpassen. Ändert das Finanzamt die Vorgaben für das PDF-Format, musst du dieselbe Klasse anfassen. Diese Klasse hat eine geringe Kohäsion und eine zu hohe Kopplung an verschiedene externe Systeme.
Das Single Responsibility Principle (SRP) löst dieses Problem mit einer klaren Regel: "Eine Klasse sollte nur einen einzigen Grund haben, sich zu ändern."
Sie darf also nur für einen einzigen fachlichen oder technischen Verantwortungsbereich zuständig sein. Die Lösung für unser Beispiel ist die Aufteilung in spezialisierte Klassen:
RechnungsBerechner: Kümmert sich ausschließlich um die Mathematik.RechnungsRepository: Übernimmt nur das Speichern in der Datenbank.RechnungsDrucker: Ist nur für das Erstellen des PDFs verantwortlich.
Open/Closed Principle (OCP)
Stell dir vor, du programmierst eine Gehaltsabrechnung für Festangestellte. Nun kommen Freelancer hinzu. Wenn du jetzt in deine Hauptklasse gehst und überall if (typ == Freelancer) einbaust, veränderst du bestehenden, bereits getesteten Code. Das birgt das Risiko, neue Fehler einzubauen.
Das Open/Closed Principle (OCP) fordert: "Software-Entitäten sollten offen für Erweiterung, aber geschlossen für Modifikation sein."
Du erreichst dies durch Polymorphie und Abstraktionen. Die Hauptklasse nutzt nur ein Interface Mitarbeitende. Um Freelancer zu unterstützen, erstellst du eine neue Klasse, die dieses Interface implementiert. Der Code der Abrechnungsmaschine bleibt unangetastet ("geschlossen"), kann aber neue Typen verarbeiten ("offen").
class Mitarbeitende:
def berechne_gehalt(self) -> float:
pass
class Festangestellte(Mitarbeitende):
def berechne_gehalt(self) -> float:
return 3000.0
class Freelancer(Mitarbeitende):
def berechne_gehalt(self) -> float:
return 2000.0
class AbrechnungsMaschine:
# Diese Methode muss nie wieder geändert werden,
# auch wenn neue Mitarbeiter-Typen hinzukommen!
def generiere_abrechnung(self, person: Mitarbeitende):
print(f"Gehalt: {person.berechne_gehalt()}")Wie gestalten wir flexible Schnittstellen und Beziehungen?
Liskov Substitution Principle (LSP)
Stell dir vor, du hast eine Basisklasse Vogel mit der Methode fliegen(). Nun leitest du die Klasse Pinguin davon ab. Da Pinguine nicht fliegen, wirft die Methode beim Pinguin einen Fehler. Ein Programmteil, der eine Liste von Vögeln durchgeht und alle fliegen lassen will, wird beim Pinguin abstürzen.
Das Liskov Substitution Principle (LSP) besagt: "Objekte einer Basisklasse müssen durch Objekte ihrer abgeleiteten Klassen ersetzt werden können, ohne die Korrektheit des Programms zu beeinträchtigen."
Eine Unterklasse darf die Erwartungen an die Elternklasse nicht brechen oder deren Verhalten verfälschen. Die Lösung: Die Vererbungshierarchie war falsch. fliegen() gehört in ein separates Interface Flugfaehig, das nur von der Taube oder dem Adler, aber nicht vom Pinguin implementiert wird.
Interface Segregation Principle (ISP)
Stell dir ein Interface Multifunktionsgeraet mit den Methoden drucken(), scannen() und faxen() vor. Ein einfacher Drucker, der dieses Interface implementiert, müsste auch scannen() und faxen() als leere Methoden oder mit einer Fehlermeldung implementieren.
Das Interface Segregation Principle (ISP) fordert: "Clients sollten nicht gezwungen werden, von Interfaces abhängig zu sein, die sie nicht nutzen."
Große, allgemeine Schnittstellen führen zu unnötigen Abhängigkeiten. Es ist besser, viele kleine, spezifische Schnittstellen zu definieren. Teile das Interface auf in Druckbar, Scannbar und Faxbar. Der einfache Drucker implementiert nun nur noch Druckbar.
class Druckbar:
def drucken(self): pass
class Scannbar:
def scannen(self): pass
# Der einfache Drucker wird nicht mit ungenutzten Methoden belastet
class EinfacherDrucker(Druckbar):
def drucken(self):
print("Druckt Dokument...")
# Das Multifunktionsgerät kombiniert die kleinen Interfaces
class Multifunktionsgeraet(Druckbar, Scannbar):
def drucken(self):
print("Druckt Dokument...")
def scannen(self):
print("Scannt Dokument...")Dependency Inversion Principle (DIP)
Wenn eine Klasse OnlineShop (hochrangige Geschäftslogik) in ihrem Code direkt ein konkretes Objekt MySQLDatenbank (niederrangiges Modul) erzeugt, sind beide stark gekoppelt. Willst du die Datenbank wechseln, musst du den Code des Shops umschreiben.
Das Dependency Inversion Principle (DIP) besagt: "Abhängigkeiten sollten auf Abstraktionen beruhen, nicht auf konkreten Implementierungen."
Der OnlineShop sollte nur ein abstraktes Interface IDatenbank kennen. Welche konkrete Datenbank genutzt wird, wird von außen an den Shop übergeben (Dependency Injection). Dadurch verringerst du die Kopplung massiv und erhöhst die Flexibilität: Dem Shop ist es nun völlig egal, ob die Daten in MySQL, PostgreSQL oder einer Textdatei liegen.
# FALSCH: Starke Kopplung an eine konkrete Implementierung
class SchlechterShop:
def __init__(self):
self.db = MySQLDatenbank()
# RICHTIG: Abhängigkeit von einer Abstraktion (Interface)
class GuterShop:
def __init__(self, datenbank: IDatenbank):
self.db = datenbankTeste dein Wissen
Du diskutierst mit einer Kolleg:in über Code-Qualität. Warum sollt ihr bei der Entwicklung neben DRY und KISS auch noch die SOLID-Prinzipien anwenden?