SOLID-Prinzipien

Einführung

Montagmorgen im Entwicklerteam: Ein kleiner Bug im Warenkorb soll behoben werden. Beim Öffnen der betroffenen Klasse fällt auf, dass sie über 800 Zeilen Code umfasst.

Sie kümmert sich gleichzeitig um Validierung, Datenbankzugriffe, Zahlungslogik und Benachrichtigungen. Ein Kollege ändert ein paar Zeilen für den Bugfix - und plötzlich bricht das gesamte Checkout-System. Alle fragen sich: “Warum ist diese kleine Änderung so gefährlich?”

Solche Situationen entstehen, wenn Code zu stark gekoppelt ist und zu viele Verantwortlichkeiten trägt. Änderungen wirken sich dann wie Kettenreaktionen aus. Genau hier setzen die SOLID-Prinzipien an: Sie geben dir fünf klare Regeln, mit denen du deinen Code modular, verständlich und sicher erweiterbar gestaltest.

In dieser Lerneinheit erfährst du, was hinter SOLID steckt - und wie du sie praktisch anwendest, um solche Probleme zu vermeiden.

Lernziele

Nach dieser Einheit kannst du:

  1. SOLID-Prinzipien benennen und erklären Du kannst die fünf Prinzipien (SRP, OCP, LSP, ISP, DIP) in eigenen Worten definieren und deren Nutzen für wartbaren, klaren Code darstellen.

  2. Negativ- und Positivbeispiele erkennen und bewerten Du erkennst typische Verstöße gegen SOLID (z. B. God Classes, wachsende if-Ketten) und kannst sie mit verbesserten Lösungen vergleichen.

  3. Prinzipien praktisch anwenden Du bist in der Lage, einfache Klassen und Methoden so zu strukturieren, dass sie die SOLID-Regeln einhalten (z. B. klare Verantwortlichkeiten, Abhängigkeiten über Abstraktionen).

  4. Vorteile im Team- und Projektkontext erläutern Du kannst erklären, wie SOLID-Prinzipien Code-Reviews, Onboarding und langfristige Wartung erleichtern und warum sie für professionelle Softwareentwicklung relevant sind.

Überleitung

Die SOLID-Prinzipien sind fünf Regeln, die dir helfen, objektorientierten Code verständlich, erweiterbar und wartbar zu schreiben. Wenn du sie anwendest, bleiben Änderungen lokal, Tests werden einfacher und Abhängigkeiten überschaubar.

Schauen wir uns diese Prinzipien genauer an.

Was bedeutet SOLID?

  • Single Responsibility Principle (SRP):
    • Eine Klasse hat genau eine Verantwortlichkeit.
  • Open/Closed Principle (OCP):
    • Für Erweiterung offen, für Änderung geschlossen.
  • Liskov Substitution Principle (LSP):
    • Ein Untertyp muss den Basistyp ohne Überraschungen ersetzen können.
  • Interface Segregation Principle (ISP):
    • Kleine, passende Schnittstellen statt “Alles-in-einem”.
  • Dependency Inversion Principle (DIP):
    • High-Level-Code hängt von Abstraktionen, nicht von konkreten Klassen ab.

Single Responsibility Principle (SRP)

Eine Klasse oder Funktion sollte genau einen Grund für eine Änderung haben.

Negativbeispiel (Python)

class UserService:
    def register(self, data):
        # 1) Eingabe prüfen
        if "email" not in data or "@" not in data["email"]:
            raise ValueError("Ungültige E-Mail")
        # 2) Speichern
        print("Speichere Nutzer:", data)
        # 3) Bestätigungs-Mail senden
        print("Sende Mail an:", data["email"])

Erklärung:

Dieser Python-Code definiert eine Klasse UserService mit einer Funktion (Methode) register, die einen neuen Benutzer registriert.

  1. Validierung: Zuerst prüft die Funktion, ob die Eingabedaten (data) überhaupt einen Eintrag "email" enthalten und ob dieser Wert ein "@"-Zeichen besitzt.
  2. Fehlerfall: Wenn eines von beiden nicht zutrifft, bricht die Funktion sofort mit einem ValueError (Fehler wegen ungültiger Daten) ab.
  3. Erfolgsfall: Nur wenn die E-Mail gültig ist, werden die nächsten Schritte (hier als print-Befehle simuliert) ausgeführt: das “Speichern” des Nutzers und das “Senden” einer Bestätigungsmail.

Warum problematisch?
Eine Klasse macht Validierung, Speichern und Mailversand. Jede Änderung (z. B. Mail-Layout) betrifft dieselbe Klasse. Das fördert Seiteneffekte und erschwert Tests.

S - Single Responsibility Principle (SRP)

Positivbeispiel (Python)

class UserValidator:
    def is_valid(self, data):
        return "email" in data and "@" in data["email"]
 
class UserRepository:
    def save(self, data):
        print("Speichere Nutzer:", data)
 
class Mailer:
    def send_welcome(self, email):
        print("Sende Mail an:", email)
 
def register_user(data, validator, repo, mailer):
    if not validator.is_valid(data):
        raise ValueError("Ungültige E-Mail")
    repo.save(data)
    mailer.send_welcome(data["email"])

Erklärung:

Dieser Code verfolgt das Prinzip der Trennung der Verantwortlichkeiten (Separation of Concerns). Jede Hauptaufgabe (Validieren, Speichern, Mailen) wird in eine eigene, spezialisierte Klasse ausgelagert, anstatt alles in einer Funktion zu vermischen.

  1. Die “Spezialisten” (Klassen):

    • UserValidator: Ist nur für die Gültigkeitsprüfung der Daten zuständig. Die is_valid-Funktion gibt True (wahr) oder False (falsch) zurück.
    • UserRepository: Ist nur für das Speichern der Daten (z.B. in einer Datenbank) zuständig. Hier wird das Speichern durch print simuliert.
    • Mailer: Ist nur für das Versenden von E-Mails zuständig (ebenfalls simuliert).
  2. Der “Koordinator” (Funktion):

    • register_user: Diese Funktion ist der “Manager” des Prozesses. Sie führt keine Arbeit selbst aus, sondern delegiert sie an die Spezialisten (validator, repo, mailer), die ihr übergeben werden.
    • Ablauf:
      1. Sie fragt den validator, ob die Daten gültig sind.
      2. Wenn nicht (if not ...), bricht sie mit einem Fehler ab.
      3. Wenn die Daten gültig sind, befiehlt sie dem repo das Speichern.
      4. Anschließend befiehlt sie dem mailer das Senden der E-Mail.

Warum besser?
Jede Klasse hat eine Aufgabe. Du testest und änderst Teile unabhängig.

O – Open/Closed Principle (OCP)

Bestehender Code bleibt unangetastet, wenn du neue Fälle ergänzt.

Negativbeispiel (wachsende if/elif-Kette)

def shipping_cost(kind, weight):
    if kind == "standard":
        return 5 + 0.5 * weight
    elif kind == "express":
        return 10 + 0.7 * weight
    elif kind == "overnight":
        return 20 + 1.0 * weight
    # Beim nächsten Tarif wächst die Kette weiter ...

Erklärung:

Dieser Code definiert eine Funktion shipping_cost, die Versandkosten basierend auf der Versandart (kind) und dem Gewicht (weight) berechnet.

  1. Funktionsdefinition: def shipping_cost(kind, weight): startet die Funktion, die zwei Argumente erwartet: kind (z.B. “standard”) und weight (z.B. 10.5).
  2. Bedingte Preisberechnung: Die Funktion nutzt eine if/elif-Kette (else if), um zu entscheiden, welche Berechnungsformel angewendet wird:
    • if kind == "standard": Wenn die Art “standard” ist, beträgt der Preis 5 (Grundgebühr) plus 0.5 pro Gewichtseinheit.
    • elif kind == "express": Wenn es nicht “standard”, aber “express” ist, beträgt der Preis 10 (Grundgebühr) plus 0.7 pro Gewichtseinheit.
    • elif kind == "overnight": Wenn es weder “standard” noch “express”, aber “overnight” ist, beträgt der Preis 20 (Grundgebühr) plus 1.0 pro Gewichtseinheit.
  3. Rückgabe: Das return-Schlüsselwort beendet die Funktion sofort und gibt das berechnete Ergebnis zurück.

Warum problematisch?
Jede neue Versandart zwingt dich, bestehenden Code zu ändern. Das erhöht Risiko und Merge-Konflikte.

O – Open/Closed Principle (OCP)

Positivbeispiel (Strategie über Objekte)

class ShippingMethod:
    def cost(self, weight):
        raise NotImplementedError
 
class Standard(ShippingMethod):
    def cost(self, weight):
        return 5 + 0.5 * weight
 
class Express(ShippingMethod):
    def cost(self, weight):
        return 10 + 0.7 * weight
 
def shipping_cost(method: ShippingMethod, weight):
    return method.cost(weight)
 
# Nutzung:
price = shipping_cost(Express(), 3)

Erklärung:

Dieser Code löst dasselbe Problem wie das vorherige if/elif-Beispiel, aber durch Vererbung (Object-Oriented Programming). Dies ist ein sehr gängiges Muster (genannt “Strategy Pattern”), um Code flexibler zu machen.

  1. Die “Vorlage” (Mutterklasse):

    • class ShippingMethod:: Dies ist eine abstrakte Vorlage (manchmal “Interface” genannt).
    • raise NotImplementedError: Sie legt einen “Vertrag” fest: Jede Versandart, die von ShippingMethod erbt, muss eine eigene cost()-Funktion implementieren, sonst stürzt das Programm ab.
  2. Die “konkreten” Versandarten (Kindklassen):

    • class Standard(ShippingMethod): und class Express(ShippingMethod):: Dies sind spezifische Versandarten, die von der ShippingMethod-Vorlage erben.
    • Beide “erfüllen den Vertrag”, indem sie die cost()-Funktion mit ihrer eigenen, spezifischen Logik überschreiben.
  3. Die Berechnungs-Funktion:

    • def shipping_cost(method: ShippingMethod, weight): Diese Funktion ist nun viel einfacher. Sie benötigt keine if/elif-Kette mehr.
    • Sie akzeptiert irgendein Objekt (method), solange es dem ShippingMethod-”Vertrag” entspricht (also eine .cost()-Funktion hat).
    • return method.cost(weight): Sie ruft einfach die .cost()-Funktion des Objekts auf, das ihr übergeben wurde.
  4. Nutzung:

    • shipping_cost(Express(), 3): Hier wird ein neues Express-Objekt (Express()) erstellt und an die Funktion übergeben. Die Funktion ruft Express().cost(3) auf, ohne wissen zu müssen, dass es “Express” ist.

Warum besser?
Neue Versandart = neue Klasse. shipping_cost bleibt unverändert.

L – Liskov Substitution Principle (LSP)

Ein Untertyp darf keine Regeln des Basistyps brechen. Er muss austauschbar sein.

Negativbeispiel (Untertyp kann geforderte Fähigkeit nicht erfüllen)

class Bird:
    def fly(self):
        print("Ich fliege")
 
class Penguin(Bird):
    def fly(self):
        raise NotImplementedError("Pinguine fliegen nicht")

Erklärung:

Dieser Code zeigt ein klassisches Problem bei der Vererbung, bekannt als das Liskov-Substitutionsprinzip (LSP).

  1. Die “Mutterklasse” (Basisklasse):

    • class Bird:: Definiert die allgemeine Vorlage für einen Vogel.
    • def fly(self):: Geht davon aus, dass die Standardaktion eines Vogels das Fliegen ist.
  2. Die “Kindklasse” (Abgeleitete Klasse):

    • class Penguin(Bird):: Ein Penguin (Pinguin) erbt von Bird. Das bedeutet, er “ist ein” Vogel und bekommt zunächst alle seine Fähigkeiten, einschließlich fly().
  3. Das Problem und die “Lösung” hier:

    • Pinguine sind Vögel, können aber nicht fliegen. Die geerbte fly()-Funktion ist für Pinguine falsch.
    • def fly(self): (im Pinguin): Der Pinguin überschreibt die geerbte Funktion.
    • raise NotImplementedError(...): Anstatt zu fliegen, löst die Funktion des Pinguins absichtlich einen Fehler aus. Dies signalisiert, dass die fly()-Funktion für ein Penguin-Objekt nicht verwendet werden darf.

Im Grunde korrigiert die Penguin-Klasse die falsche Annahme der Bird-Klasse (dass alle Vögel fliegen).

Warum problematisch?
Penguin verletzt die Erwartung: Jeder Bird soll fliegen können. Code, der Bird nutzt, stürzt bei Penguin ab.

L – Liskov Substitution Principle (LSP)

Positivbeispiel (passende Typen trennen)

class Bird:
    pass
 
class FlyableBird(Bird):
    def fly(self):
        print("Ich fliege")
 
class Penguin(Bird):
    def swim(self):
        print("Ich schwimme")

Erklärung:

Dieser Code löst das Problem aus dem vorherigen Beispiel (“Pinguine können nicht fliegen”) durch eine bessere Vererbungsstruktur.

  1. class Bird:: Dies ist jetzt eine sehr allgemeine “Basis”-Klasse. Sie dient nur als gemeinsamer Vorfahre (Marker). pass ist ein Platzhalter und bedeutet “tut nichts”.
  2. class FlyableBird(Bird):: Dies ist eine spezialisierte Unterklasse (Kindklasse) für flugfähige Vögel. Sie erbt von Bird (ist also ein Vogel) und fügt die Fähigkeit fly() hinzu.
  3. class Penguin(Bird):: Der Penguin erbt ebenfalls von Bird (ist ein Vogel), aber er erbt nicht von FlyableBird.
    • Ergebnis: Der Pinguin hat korrekterweise keine fly()-Funktion. Stattdessen hat er seine eigene swim()-Funktion.

Dieser Aufbau ist besser, da eine Klasse (Pinguin) nicht mehr gezwungen wird, eine geerbte Funktion (fliegen) absichtlich “kaputt” zu machen.

Warum besser?
Nur Vögel, die fliegen, erben von FlyableBird. Penguin bricht keine Erwartung mehr.

I - Interface Segregation (ISP)

Interface Segregation Principle (ISP)

Kein Code soll gezwungen sein, von Methoden abzuhängen, die er nicht nutzt. Zerlege große Interfaces in kleine, zweckmäßige Interfaces.

Nutzen: Weniger Kopplung, bessere Kohäsion, klarere Verträge. Änderungen bleiben lokal, Tests werden einfacher.

Negativbeispiel (zu großes Interface – unnötige Methoden)

class MultiFunctionDevice:
    def print(self, text: str) -> None:
        ...
    def scan(self) -> bytes:
        ...
    def fax(self, number: str) -> None:
        ...
 
class SimplePrinter(MultiFunctionDevice):
    def print(self, text: str) -> None:
        print(text)
    def scan(self) -> bytes:
        raise NotImplementedError  # braucht SimplePrinter nicht
    def fax(self, number: str) -> None:
        raise NotImplementedError  # braucht SimplePrinter nicht

Erklärung:

Dieser Code demonstriert ein Problem, das durch zu “breite” oder “fette” Vorlagen (Klassen/Interfaces) entsteht, bekannt als Verletzung des Interface Segregation Principle (ISP).

  1. Die “Alles-Könner”-Vorlage (Mutterklasse):

    • class MultiFunctionDevice: Definiert eine Vorlage für ein Multifunktionsgerät. Es setzt voraus, dass jedes Gerät dieser Art drucken, scannen und faxen kann. (Das ... ist ein Platzhalter).
  2. Die “Spezialisten”-Klasse (Kindklasse):

    • class SimplePrinter(MultiFunctionDevice): Ein einfacher Drucker erbt von der MultiFunctionDevice-Vorlage.
    • Dadurch “verspricht” der SimplePrinter, alles zu können, was ein MultiFunctionDevice kann (drucken, scannen, faxen).
  3. Das Problem:

    • Der SimplePrinter kann die print-Funktion erfüllen.
    • Er kann jedoch nicht scannen oder faxen. Er wird durch die Vererbung gezwungen, diese Funktionen zu implementieren, obwohl er sie nicht unterstützt.
    • Wie im Pinguin-Beispiel “löst” er dies, indem er bei Aufruf von scan() oder fax() einen Fehler (NotImplementedError) auslöst.

Das Kernproblem ist, dass SimplePrinter gezwungen wird, von Funktionen “abhängig” zu sein (scan, fax), die er gar nicht benötigt.

Warum ist das schlecht?
Der Klient hängt von unnötigen Methoden ab. Jede Änderung an scan/fax betrifft auch Geräte, die nur drucken.

I - Interface Segregation (ISP)

Positivbeispiel (kleine, fokusierte Interfaces)

from abc import ABC, abstractmethod
 
class Printer(ABC):
    @abstractmethod
    def print(self, text: str) -> None: ...
 
class Scanner(ABC):
    @abstractmethod
    def scan(self) -> bytes: ...
 
class SimplePrinter(Printer):
    def print(self, text: str) -> None:
        print(text)
 
class PrintScanDevice(Printer, Scanner):
    def print(self, text: str) -> None:
        print(text)
    def scan(self) -> bytes:
        return b"image"

Erklärung:

Dieser Code löst das Problem aus dem vorigen Beispiel (“fette” Vorlagen), indem er das Interface Segregation Principle (ISP) anwendet. Die Idee ist, “Alles-Könner”-Vorlagen in kleinere, spezifische “Rollen”-Vorlagen aufzuteilen.

  1. Import (abc): ABC (Abstract Base Class) und abstractmethod sind Werkzeuge, um formale “Vorlagen” oder “Verträge” in Python zu erstellen.

  2. Getrennte Vorlagen (Interfaces):

    • class Printer(ABC): Definiert einen “Vertrag” nur für das Drucken. @abstractmethod bedeutet: Jede Klasse, die von Printer erbt, muss eine print-Funktion haben.
    • class Scanner(ABC): Definiert einen separaten “Vertrag” nur für das Scannen.
  3. Spezialisierte Klasse:

    • class SimplePrinter(Printer): Diese Klasse erbt nur von Printer. Sie muss nur den Printer-Vertrag erfüllen (die print-Funktion bereitstellen). Sie wird nicht mehr gezwungen, scan() oder fax() zu implementieren.
  4. Kombinierte Klasse (Mehrfachvererbung):

    • class PrintScanDevice(Printer, Scanner): Dieses Gerät kann beides. Es erbt von Printer und Scanner.
    • Daher muss es beide Verträge erfüllen und sowohl eine print()- als auch eine scan()-Funktion implementieren.

Was ist besser?
SimplePrinter implementiert nur print(). Ein Kombigerät kann optional scan() ergänzen. Klienten sehen nur die Methoden, die sie wirklich brauchen.

D - Dependency Inversion Principle (DIP)

Hochrangige Module sollen nicht von konkreten, niederrangigen Modulen abhängen. Beide hängen von Abstraktionen ab.

Praxis: Nutze Dependency Injection (Übergabe im Konstruktor oder als Parameter), um Implementierungen zur Laufzeit auszutauschen.

Nutzen: Geringere Kopplung, besser testbar (Mocks/Stubs), einfacherer Technologiewechsel.

Negativbeispiel (harte Kopplung durch direkte Instanziierung)

class SmtpMailer:
    def send(self, to: str, body: str) -> None:
        print(f"SMTP -> {to}: {body}")
 
class NotificationService:
    def __init__(self):
        self.mailer = SmtpMailer()  # feste Bindung
    def notify(self, to: str, msg: str) -> None:
        self.mailer.send(to, msg)

Erklärung

Dieser Code demonstriert das Problem der starken Kopplung (Strong Coupling), oft auch als feste Bindung bezeichnet.

  1. SmtpMailer: Dies ist eine “konkrete” Implementierung, die E-Mails über SMTP versendet (hier simuliert durch print).
  2. NotificationService: Dies ist ein Dienst, der Benachrichtigungen versenden soll.
  3. Das Kernproblem (__init__): Im Konstruktor (der __init__-Funktion) erstellt der NotificationService seine Abhängigkeit (den SmtpMailer) selbst.
  4. Die Konsequenz: Der NotificationService ist untrennbar an die spezifische Klasse SmtpMailer gebunden. Man kann den Mailer nicht einfach austauschen (z.B. gegen einen ApiMailer oder einen TestMailer für Tests), ohne den Code des NotificationService ändern zu müssen.

Warum ist das schlecht? Tests werden schwerer (kein einfacher Austausch), und Technologie-Änderungen greifen in High-Level-Logik ein.

D - Dependency Inversion Principle (DIP)

Positivbeispiel (Abhängigkeit injizieren, auf Abstraktion zielen)

from abc import ABC, abstractmethod
 
class Mailer(ABC):
    @abstractmethod
    def send(self, to: str, body: str) -> None: ...
 
class SmtpMailer(Mailer):
    def send(self, to: str, body: str) -> None:
        print(f"SMTP -> {to}: {body}")
 
class ApiMailer(Mailer):
    def send(self, to: str, body: str) -> None:
        print(f"API -> {to}: {body}")
 
class NotificationService:
    def __init__(self, mailer: Mailer) -> None:
        self.mailer = mailer  # Abhängigkeit wird übergeben
    def notify(self, to: str, msg: str) -> None:
        self.mailer.send(to, msg)
 
# Nutzung
service = NotificationService(SmtpMailer())  # oder NotificationService(ApiMailer())

Erklärung

Dieser Code löst das Problem der “festen Bindung” aus dem vorigen Beispiel durch Dependency Inversion (Umkehrung der Abhängigkeit), umgesetzt durch Dependency Injection (Einreichen der Abhängigkeit).

  1. Die Abstraktion (Der “Vertrag”):

    • class Mailer(ABC): Dies ist eine abstrakte Vorlage (ein “Interface”). Sie definiert, dass jeder Mailer eine send()-Funktion haben muss, sagt aber nicht, wie diese funktioniert.
  2. Die Konkreten Implementierungen (Die “Arbeiter”):

    • SmtpMailer(Mailer) und ApiMailer(Mailer): Dies sind spezifische Mailer-Typen, die beide den Mailer-Vertrag erfüllen (sie implementieren send()).
  3. Die Umkehrung (Der Kernpunkt):

    • class NotificationService:: Diese Klasse erstellt den Mailer nicht mehr selbst.
    • def __init__(self, mailer: Mailer) -> None:: Stattdessen verlangt der Konstruktor (__init__), dass ihm ein fertiges Mailer-Objekt von außen übergeben wird. Dies nennt man Dependency Injection (Einreichen der Abhängigkeit).
    • self.mailer = mailer: Der Service speichert nur die Referenz auf den übergebenen Mailer.
  4. Nutzung (Die Flexibilität):

    • service = NotificationService(SmtpMailer()): Beim Erstellen des NotificationService wird von außen entschieden, welcher Mailer verwendet wird (hier SmtpMailer).
    • Der NotificationService ist nun “entkoppelt”. Er funktioniert mit jedem Objekt, das den Mailer-Vertrag erfüllt, egal ob es ein SmtpMailer, ApiMailer oder ein TestMailer ist.

Was ist besser?
NotificationService hängt von der Abstraktion Mailer ab. Die konkrete Implementierung kommt von außen. Du tauschst sie, ohne den Service zu ändern.

Vorteile und Probleme

Die folgende Tabelle fasst die wichtigsten Vorteile und typische Probleme bei Missachtung:

Vorteile im AlltagHäufige Probleme ohne SOLID
Lesbarkeit & Wartbarkeit: Verantwortlichkeiten sind klar, Abhängigkeiten laufen über AbstraktionenGod Classes: zu viele Gründe zur Änderung (SRP-Verstoß)
Weniger Seiteneffekte: Änderungen bleiben lokalHohe Kopplung: direkte Abhängigkeiten auf konkrete Klassen (DIP-Verstoß)
Bessere Tests: einfache Mocks/Stubs dank kleiner Interfaces und InjektionAufgeblähte Interfaces: Klienten müssen unnötige Methoden mitziehen (ISP-Verstoß)
⏳ Lädt Dataview-Inhalt...

Anwendungstipps und Kontext

Anwendungstipps

  • Klein anfangen: Beim nächsten Feature große Interfaces aufteilen (ISP) und Abhängigkeiten injizieren (DIP).
  • Schrittweise refactoren: Erst eine Stelle entkoppeln, Tests ergänzen, dann weitermachen.
  • Team-Vokabular nutzen: „Bitte Interface aufteilen (ISP)“ / „Abstraktion + Injection (DIP)“ macht Reviews schneller.

Kontext / Herkunft

Die SOLID-Prinzipien wurden von Robert C. Martin konsolidiert; das Akronym „SOLID“ prägte Michael Feathers. Das Liskov Substitution Principle geht auf Barbara Liskov (1987) zurück.

⏳ Lädt Dataview-Inhalt...

Zusammenfassung

Zusammenfassung:

Single Responsibility Principle (SRP)

Dieses Prinzip fordert, dass eine Klasse nur eine Verantwortung haben darf.

  • Negativbeispiel: Eine Klasse validiert Nutzerdaten und speichert sie zugleich → viele Gründe zur Änderung.
  • Positivbeispiel: Eine UserValidator-Klasse prüft Daten, eine UserRepository-Klasse speichert sie.
  • Nutzen: Klarere Verantwortlichkeiten, weniger Seiteneffekte, bessere Testbarkeit.

Open/Closed Principle (OCP)

Eine Softwareeinheit soll für Erweiterungen offen, aber für Änderungen geschlossen sein.

  • Negativbeispiel: Neue Zahlungsarten erfordern Anpassungen in bestehenden Methoden.
  • Positivbeispiel: Ein PaymentMethod-Interface erlaubt neue Zahlungsarten, ohne die Checkout-Logik zu ändern.
  • Nutzen: Stabilität des Codes bei Erweiterungen, weniger Fehler durch Änderungen.

Liskov Substitution Principle (LSP)

Subtypen müssen sich so verhalten, dass sie Objekte ihres Basistyps problemlos ersetzen können.

  • Negativbeispiel: Ein Square bricht Erwartungen an Rectangle, weil Setter-Methoden unerwartet wirken.
  • Positivbeispiel: Korrekte Modellierung mit getrennten Klassen vermeidet Verletzungen von Erwartungen.
  • Nutzen: Austauschbarkeit von Klassen ohne unerwartete Effekte.

Interface Segregation Principle (ISP)

Klassen sollen nur von Methoden abhängen, die sie wirklich brauchen.

  • Negativbeispiel: SimplePrinter muss unnötige Methoden wie scan oder fax implementieren.
  • Positivbeispiel: Kleine, spezifische Interfaces wie Printer und Scanner erlauben passgenaue Implementierungen.
  • Nutzen: Weniger Kopplung, schlankere Verträge, einfachere Tests.

Dependency Inversion Principle (DIP)

Hochrangige Module sollen nicht von konkreten Klassen, sondern von Abstraktionen abhängen.

  • Negativbeispiel: NotificationService erzeugt selbst ein SmtpMailer-Objekt → feste Bindung.
  • Positivbeispiel: Ein Mailer-Interface erlaubt die Übergabe beliebiger Implementierungen (z. B. SMTP oder API-Mailer).
  • Nutzen: Austauschbarkeit, einfache Tests mit Stubs/Mocks, geringere Abhängigkeiten.

Gesamtvorteile von SOLID

  • Lesbarkeit und Wartbarkeit steigen.
  • Änderungen verursachen weniger Seiteneffekte.
  • Erweiterungen sind planbarer.
  • Teams nutzen ein gemeinsames Vokabular (SRP, OCP, LSP, ISP, DIP), was Code-Reviews und Onboarding erleichtert.

Häufige Probleme ohne SOLID

  • „God Classes“ mit zu vielen Verantwortlichkeiten.
  • Harte Kopplung an konkrete Implementierungen.
  • Interfaces mit unnötigen Methoden.
  • Zunehmende Softwarefäulnis durch unstrukturierte Änderungen.

Ausblick:

In der nächsten Lerneinheit geht es um das Model-View-Controller (MVC)-Prinzip – ein grundlegendes Architekturkonzept, das für saubere Trennung von Logik, Darstellung und Daten sorgt. Du wirst sehen, wie MVC hilft, Anwendungen übersichtlicher zu strukturieren, Änderungen einfacher umzusetzen und Teams effizienter zusammenarbeiten zu lassen.