SOLID Prinzipien

SOLID in der Praxis

SOLID ist ein Akronym, das eine Reihe von Prinzipien beschreibt, die in der objektorientierten Programmierung verwendet werden, um Software zu entwickeln, die wartbar, erweiterbar und gut testbar ist. Diese Prinzipien sind besonders hilfreich, wenn es um die Strukturierung von Code geht, um das Risiko von Fehlern und technischen Schulden zu verringern. In dem Beitrag SOLID in der Praxis werden wir die 5 Prinzipien erläutern.

Hier eine detailliertere und umfassendere Erklärung der SOLID-Prinzipien, wie sie in der Praxis verwendet werden und warum sie so wichtig sind.

1. Single Responsibility Principle (SRP) – Prinzip der einzigen Verantwortung

Grundgedanke:

Das SRP besagt, dass jede Klasse nur eine einzige Verantwortung oder Aufgabe haben sollte. Wenn eine Klasse mehrere Verantwortlichkeiten übernimmt, wird es schwieriger, Änderungen vorzunehmen, weil jede Änderung an einer Verantwortung Auswirkungen auf die andere haben könnte. Dies führt zu unerwünschten Nebenwirkungen und zu einer schwer wartbaren Codebasis.

Tiefergehende Überlegungen:

  • Was bedeutet „Verantwortung“?
    Eine Verantwortung ist eine Funktionalität, die sich in einer Änderung oder in einer spezifischen Anforderung widerspiegeln lässt. Wenn du also eine Klasse ändern musst, um eine bestimmte Anforderung zu erfüllen, dann bezieht sich diese Änderung nur auf eine Verantwortung der Klasse.
  • Was passiert bei der Verletzung des SRP?
    Wenn eine Klasse mehrere Verantwortlichkeiten übernimmt, wird sie mit der Zeit komplexer und schwieriger zu verstehen. Diese Art von Klassen werden oft als „God Classes“ bezeichnet und sind oft der Ursprung von Fehlern und technischem Schuldenaufbau, da Änderungen an einer Funktionalität unbeabsichtigte Auswirkungen auf andere Teile des Systems haben können.

Beispiel:

Stell dir vor, du hast eine Klasse, die sowohl für die Geschäftslogik einer Bestellung als auch für das Senden von Bestellbestätigungs-E-Mails zuständig ist. Wenn du die Logik zur Generierung von Bestellbestätigungen ändern musst, kann dies die Geschäftslogik ungewollt beeinflussen.

Verletzung des SRP:

class Bestellung:
    def berechne_preis(self):
        pass
        
    def sende_bestellbestaetigung(self):
        # E-Mail senden
        pass

Ein SRP-konformes Design:

class Bestellung:
    def berechne_preis(self):
        pass

class Bestellbestaetigung:
    def sende_bestaetigung(self, bestellung):
        # E-Mail senden
        pass

Im zweiten Beispiel sind die Verantwortlichkeiten klar getrennt. Änderungen an der Art und Weise, wie eine Bestellbestätigung gesendet wird, beeinflussen nicht die Preisberechnung und umgekehrt.


2. Open/Closed Principle (OCP) – Offen/geschlossen-Prinzip

Grundgedanke:

Softwaremodule sollten offen für Erweiterungen, aber geschlossen für Änderungen sein. Dies bedeutet, dass du die Funktionalität eines Systems erweitern kannst, ohne bestehende Codebasis zu verändern, was die Stabilität des Systems wahrt.

Tiefergehende Überlegungen:

  • Warum ist das wichtig?
    Änderungen an bestehendem Code können unvorhergesehene Fehler verursachen. Wenn du den bestehenden Code ändern musst, um neue Anforderungen zu integrieren, riskierst du, die Software instabil zu machen. Das OCP fördert daher Erweiterungen durch die Schaffung von Erweiterungspunkten und vermeidet Modifikationen am bestehenden Code.
  • Abstraktionen nutzen:
    Das OCP wird häufig durch die Verwendung von Abstraktionen (wie Interfaces oder abstrakte Klassen) erreicht. So kannst du neue Implementierungen hinzufügen, die mit bestehenden Modulen interoperabel sind, ohne den bestehenden Code zu beeinflussen.

Beispiel:

Angenommen, du hast eine Anwendung, die unterschiedliche Rabatte berechnet, je nach Art der Bestellung. Wenn du eine neue Rabattart hinzufügen möchtest, ohne den bestehenden Code zu ändern, erstellst du eine neue Klasse, die von einer abstrakten Klasse oder Schnittstelle erbt.

Verletzung des OCP:

class RabattBerechnung:
    def berechne_rabatt(self, bestellung, rabattart):
        if rabattart == "Saisonrabatt":
            return bestellung.betrag * 0.1
        elif rabattart == "Mengenrabatt":
            return bestellung.betrag * 0.2

Um eine neue Rabattart hinzuzufügen, müsste der Code angepasst werden – das ist eine Verletzung des OCP.

Ein OCP-konformes Design:

class RabattBerechnung:
    def berechne_rabatt(self, bestellung, rabatt):
        return rabatt.berechne(bestellung)

class SaisonRabatt:
    def berechne(self, bestellung):
        return bestellung.betrag * 0.1

class MengenRabatt:
    def berechne(self, bestellung):
        return bestellung.betrag * 0.2

Hier kannst du problemlos neue Rabattarten hinzufügen, ohne den bestehenden Code zu ändern. Du musst nur neue Rabatt-Klassen hinzufügen, die die berechne-Methode implementieren.


3. Liskov Substitution Principle (LSP)Liskovsches Substitutionsprinzip

Grundgedanke:

Objekte einer abgeleiteten Klasse sollten überall dort verwendet werden können, wo Objekte der Basisklasse erwartet werden, ohne das Verhalten des Programms zu ändern. Mit anderen Worten, eine abgeleitete Klasse muss sich so verhalten, dass sie die Erwartungen erfüllt, die durch die Basisklasse gesetzt werden.

Tiefergehende Überlegungen:

  • Warum ist das wichtig?
    Wenn eine abgeleitete Klasse sich anders verhält oder die Funktionalität der Basisklasse einschränkt, kann dies zu unerwarteten Fehlern führen, wenn eine Instanz der abgeleiteten Klasse an einem Ort verwendet wird, der die Basisklasse erwartet.
  • Verletzung des LSP:
    Wenn eine abgeleitete Klasse die Vertragsbedingungen der Basisklasse verletzt (z. B. eine Methode mit einer nicht unterstützten Funktionalität überschreibt), entsteht eine Diskrepanz, die zu Problemen führen kann.

Beispiel:

Stell dir vor, du hast eine Basisklasse „Vogel“, die eine Methode fliegen() enthält. Eine abgeleitete Klasse „Pinguin“ könnte fliegen() nicht sinnvoll überschreiben, da Pinguine nicht fliegen können.

Verletzung des LSP:

class Vogel:
    def fliegen(self):
        pass

class Pinguin(Vogel):
    def fliegen(self):
        raise NotImplementedError("Pinguine können nicht fliegen.")

Ein LSP-konformes Design:

class Vogel:
    def bewegen(self):
        pass

class Pinguin(Vogel):
    def bewegen(self):
        # Pinguine bewegen sich anders
        pass

Nun sind sowohl „Vogel“ als auch „Pinguin“ zu einer abstrakten Bewegungsschnittstelle vereint, wodurch Pinguine problemlos als „Vogel“-Instanz verwendet werden können, ohne dass das Verhalten von „fliegen“ unerwartet fehlschlägt.


4. Interface Segregation Principle (ISP) – Prinzip der Schnittstellenaufspaltung

Grundgedanke:

Eine Klasse sollte nicht gezwungen werden, Schnittstellen zu implementieren, die sie nicht benötigt. Statt eine große, monolithische Schnittstelle zu verwenden, sollte man kleine, spezialisierte Schnittstellen definieren.

Tiefergehende Überlegungen:

  • Warum ist das wichtig?
    Große Schnittstellen zwingen Klassen dazu, Methoden zu implementieren, die sie nicht brauchen, was zu unnötigem Overhead und schlechter Lesbarkeit führt. Kleine, fokussierte Schnittstellen machen den Code klarer und einfacher zu verstehen.
  • Verletzung des ISP:
    Wenn eine Klasse gezwungen ist, Methoden zu implementieren, die sie nicht benötigt, kann das zu unnötigen Abhängigkeiten und unnötigem Code führen.

Beispiel:

Stell dir vor, du hast eine Schnittstelle Gerät, die sowohl das Drucken als auch das Scannen definiert. Wenn du jedoch eine „Drucker“-Klasse hast, die nur drucken kann, wird diese Klasse gezwungen, auch scannen zu implementieren, was sie nicht braucht.

Verletzung des ISP:

class Geraet:
    def drucken(self):
        pass

    def scannen(self):
        pass

class Drucker(Geraet):
    def drucken(self):
        pass
    def scannen(self):
        raise NotImplementedError("Drucker können nicht scannen.")

Ein ISP-konformes Design:

class Druckbar:
    def drucken(self):
        pass

class Scannbar:
    def scannen(self):
        pass

class Drucker(Druckbar):
    def drucken(self):
        pass

class Scanner(Scannbar):
    def scannen(self):
        pass

Nun sind Drucker und Scanner jeweils auf die benötigte Funktionalität fokussiert und implementieren nur die für sie relevanten Schnittstellen.


5. Dependency Inversion Principle (DIP) – Prinzip der Abhängigkeitsumkehr

Grundgedanke:

Hochwertige Module sollten nicht von niederen Modulen abhängen, sondern beide sollten von Abstraktionen abhängen. Das bedeutet, dass nicht konkrete Implementierungen direkt verwendet werden sollten, sondern Interfaces oder abstrakte Klassen, die dann an die konkrete Implementierung gebunden werden.

Tiefergehende Überlegungen:

  • Warum ist das wichtig?
    Die direkte Abhängigkeit von konkreten Implementierungen führt zu starker Kopplung und macht Tests, Wartung und Erweiterungen schwieriger. Abstraktionen ermöglichen es dir, die konkrete Implementierung leicht zu ändern, ohne die hohe Ebene des Systems zu beeinflussen.

Beispiel:

Stell dir vor, du hast eine Klasse „Bestellung“, die direkt von einer Datenbankklasse abhängt. Das führt dazu, dass die Bestellung nicht leicht verändert oder in Tests isoliert werden kann.

Verletzung des DIP:

class Bestellung:
    def __init__(self, db: MySQLDB):
        self.db = db

Ein DIP-konformes Design:

class Datenbank:
    def speichern(self):
        pass

class MySQLDB(Datenbank):
    def speichern(self):
        pass  # MySQL-spezifische Logik

class Bestellung:
    def __init__(self, db: Datenbank):
        self.db = db

Hier verwendet „Bestellung“ die Abstraktion „Datenbank“ anstelle der konkreten MySQLDB-Klasse, sodass du die Datenbankimplementierung jederzeit ändern kannst, ohne die Bestellungsklasse zu beeinflussen.


Fazit

Die SOLID-Prinzipien sind nicht nur ein theoretisches Konzept, sondern eine bewährte Methode zur Gestaltung von robustem, wartbarem und erweiterbarem Code. Diese Prinzipien helfen dabei, langfristig stabile Software zu entwickeln, die sich auch in dynamischen und schnelllebigen Entwicklungsumgebungen bewährt. Sie sind insbesondere von Bedeutung, wenn es um große Systeme, Teamarbeit und agile Entwicklung geht. Hier sind einige erweiterte Gedanken dazu:

1. Erhöhte Wartbarkeit und Reduzierung der technischen Schulden

Wenn du dich konsequent an SOLID hältst, minimierst du die Entstehung von technischen Schulden. Technische Schulden entstehen häufig durch unsicheren Code, der schwer zu verstehen und zu warten ist. Indem du den Code in kleine, fokussierte Klassen und Module aufteilst, die jeweils nur eine Verantwortung tragen, wird der Code nicht nur für dich als Entwickler verständlicher, sondern auch für jedes Teammitglied, das in der Zukunft mit diesem Code arbeiten muss. In größeren Teams sind SOLID-Prinzipien sogar eine Art universelle „Sprache“, die allen Beteiligten hilft, den Code effizient zu verstehen und weiterzuentwickeln.

2. Erleichterte Zusammenarbeit und Teamarbeit

Wenn jedes Teammitglied den SOLID-Prinzipien folgt, sorgt dies für eine einheitliche Struktur im gesamten Codebase. Dies macht es leichter, bei der Arbeit an gemeinsamen Projekten zu kommunizieren und Aufgaben zu teilen, da der Code nach klaren und vorhersehbaren Mustern organisiert ist. Zum Beispiel, wenn du in einem Team an einem Softwareprodukt arbeitest, wird die Implementierung neuer Features durch das Prinzip der Offenheit für Erweiterungen (OCP) ermöglicht, ohne dass jemand befürchten muss, dass der Code durch Änderungen an anderen Stellen instabil wird. Zudem werden Abhängigkeiten minimiert, sodass verschiedene Teile des Systems unabhängig voneinander bearbeitet werden können, was den Entwicklungsprozess beschleunigt.

3. Erhöhte Flexibilität und Anpassungsfähigkeit

Softwareprodukte sind niemals „fertig“ – sie müssen ständig erweitert, angepasst oder optimiert werden, um mit neuen Anforderungen Schritt zu halten. SOLID stellt sicher, dass der Code flexibel bleibt und Änderungen möglichst wenig Eingriffe in bestehende Funktionen erfordern. So kannst du ohne Bedenken neue Features einführen, neue Technologien integrieren oder bestehende Funktionalitäten anpassen, ohne den gesamten Code umwerfen zu müssen. Die Prinzipien fördern eine klare Trennung von Anliegen, sodass Änderungen an einer Komponente die anderen nicht beeinträchtigen.

4. Förderung von Tests und Qualitätssicherung

Die Anwendung von SOLID-Prinzipien macht den Code testbarer. Das bedeutet, dass du leichter Unit-Tests und Integrationstests schreiben kannst. Insbesondere die Prinzipien der Single Responsibility und der Abhängigkeitsumkehr sorgen dafür, dass jede Klasse und jedes Modul eine klar definierte Aufgabe hat und von anderen Komponenten nur über Schnittstellen oder Abstraktionen abhängt. Dadurch kannst du Klassen isoliert testen, ohne dich um komplexe Wechselwirkungen kümmern zu müssen.

Auch der Umgang mit Fehlern und Ausnahmefällen wird einfacher, weil die Verantwortlichkeiten und Zuständigkeiten klar definiert sind. Eine gut strukturierte, SOLID-basierte Codebasis ist ein starkes Fundament, um automatisierte Tests effizient zu gestalten und die Codequalität kontinuierlich zu sichern.

5. Skalierbarkeit und zukünftige Erweiterungen

Ein weiterer wichtiger Aspekt ist, dass SOLID-Prinzipien besonders in großen Softwareprojekten und bei der Skalierung von Anwendungen von großer Bedeutung sind. Im Laufe der Zeit wird Software oft immer komplexer. Zu Beginn mag eine einfache Lösung ausreichen, aber mit zunehmendem Wachstum und zunehmenden Anforderungen wird der Code schwieriger zu handhaben. Wenn du von Anfang an SOLID befolgst, gibst du deinem Code eine Struktur, die Skalierbarkeit ermöglicht, ohne dass du den gesamten Code umbauen musst.

Nehmen wir zum Beispiel eine E-Commerce-Anwendung, die zunächst nur eine kleine Anzahl von Produkten verwaltet. Mit der Zeit steigt die Produktanzahl, und es kommen neue Funktionen hinzu (z. B. Benutzerverwaltung, Rabatte, Bewertungen). Anstatt den bestehenden Code ständig zu ändern, kannst du durch das Offen/geschlossen-Prinzip (OCP) und das Prinzip der Abhängigkeitsumkehr (DIP) neue Funktionen und Erweiterungen hinzufügen, ohne die alte Funktionalität zu gefährden. So wird deine Anwendung zukunftssicher.

6. Reduzierte Komplexität und einfachere Fehlerbehebung

Ein oft übersehener Vorteil von SOLID ist die Vereinfachung der Fehlerbehebung. Wenn der Code modular, flexibel und gut strukturiert ist, werden Probleme leichter zu isolieren und zu diagnostizieren. Komplexe Abhängigkeiten und stark gekoppelter Code erschweren die Fehleranalyse erheblich. Wenn jedoch jedes Modul eine einzelne Verantwortung hat, ist es einfach, den Ursprung eines Problems schnell zu finden und zu beheben.

Beispiel: Wenn du bei einer Bestellungsbearbeitung auf ein Problem stößt, das mit der Berechnung des Preises zusammenhängt, kannst du dich auf den Bereich konzentrieren, der für die Preisberechnung verantwortlich ist – ohne sich um den Druckvorgang oder den Versand kümmern zu müssen.

7. Kosteneffizienz und langfristige Investition

Letztlich bieten SOLID-Prinzipien eine langfristige Kosteneffizienz. Auch wenn es zu Beginn mehr Zeit kosten kann, sich an diese Prinzipien zu halten und den Code sorgfältig zu strukturieren, zahlst du die Investition auf lange Sicht aus. Der Aufwand, den du in die gute Architektur und sauberen Code zu Beginn steckst, zahlt sich aus, wenn du zukünftige Features entwickelst, bestehende Funktionen wartest oder das System an neue Anforderungen anpasst.

Wenn du an ein Softwareprojekt als langfristige Investition denkst, wird der Code umso wertvoller, je wiederverwendbarer, erweiterbarer und skalierbar er ist. SOLID hilft dabei, dass die Software den Anforderungen von morgen standhält und mit minimalem Aufwand anpassbar bleibt. So schaffst du eine solide Grundlage, die auch zukünftige Veränderungen problemlos aufnehmen kann, ohne das gesamte System grundlegend zu überarbeiten.


Schlussgedanken:

SOLID in der Praxis ist nicht nur eine Sammlung von Best Practices – es ist ein Werkzeugset, das dir hilft, Software so zu entwickeln, dass sie langfristig tragfähig, pflegeleicht und anpassbar bleibt. Wenn du diese Prinzipien konsequent anwendest, schaffst du nicht nur eine saubere und wartbare Architektur, sondern auch ein Teamklima, das von Klarheit und Effizienz geprägt ist. Deine Software wird nicht nur den heutigen Anforderungen gerecht, sondern ist auch bereit, in der Zukunft zu wachsen – ohne in unübersichtliche oder schwer verständliche Strukturen zu verfallen.

Gerade in einer Zeit, in der Softwareentwicklungszyklen immer schneller werden und die Anforderungen an Systeme immer dynamischer sind, bieten die SOLID-Prinzipien einen verlässlichen Kompass, der dir hilft, auch in komplexen und herausfordernden Projekten die richtige Richtung einzuschlagen.

Zur Liste der Pattern: Liste der Design-Pattern

VG WORT Pixel

Newsletter Anmeldung

Bleiben Sie informiert! Wir informieren Sie über alle neuen Beiträge (max. 1 Mail pro Woche – versprochen)