Zum Inhalt springen
Design Patterns Fortgeschritten 30 min

SOLID-Prinzipien

Die fuenf Prinzipien fuer guten OOP-Code: Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, Dependency Inversion.

Aktualisiert:
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:

  1. Klassen mit klaren Verantwortungen (SRP)
  2. Erweiterbar ohne Aenderung (OCP)
  3. Subklassen sind echte Spezialisierungen (LSP)
  4. Kleine, fokussierte Interfaces (ISP)
  5. 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.

Zurรผck zum Design Patterns Kurs