S.O.L.I.D Design Prinzipien

Julian | Aug 31, 2025 min read

Hallo zusammen,

in der Welt der Softwareentwicklung stoßen wir immer wieder auf dieselben Herausforderungen: Wie schreiben wir Code, der einfach zu warten, zu erweitern und zu verstehen ist? Wie verhindern wir, dass unsere Codebasis mit der Zeit unübersichtlich und starr wird? Die Antwort auf diese Fragen liegt oft nicht in einem neuen Framework, sondern in den grundlegenden Design-Prinzipien.

Genau hier kommen die S.O.L.I.D Prinzipien ins Spiel. Als eine Sammlung von fünf Design-Prinzipien für die objektorientierte Programmierung, bieten sie einen klaren Leitfaden, um unseren Code robuster, flexibler und wartbarer zu machen. Entwickelt von Robert C. Martin (auch bekannt als “Uncle Bob”), bilden sie das Fundament für die Architektur moderner, nachhaltiger Software.

In diesem Artikel tauchen wir in jedes der fünf Prinzipien ein: Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation und Dependency Inversion. Wir werden uns ansehen, was jedes Prinzip bedeutet und wie wir es in unseren täglichen Code integrieren können.

TL;DR

  • S — Single Responsibility Principle (SRP)
    • Eine Klasse hat genau einen Job.
  • O — Open/Closed Principle (OCP)
    • Klassen sollen offen für Erweiterungen und geschlossen für Änderungen sein.
  • L — Liskov Substitution Principle (LSP)
    • Objekte einer Superklasse sollen austauschbar mit Objekten ihrer abgeleiteten Klassen sein, ohne die Applikation zu verändern.
  • I — Interface Segregation Principle (ISP)
    • Clients sollen nicht gezwungen werden, von Methoden abhängig zu sein, die sie nicht benutzen.
  • D — Dependency Inversion Principle (DIP)
    • Hochrangige Module sollen nicht von niedrigrangigen Modulen abhängig sein, sondern beide von Abstraktionen.

SOLID

Die Prinzipien helfen Entwicklern, Systeme zu entwerfen, die lose gekoppelt, hoch kohäsiv und einfach zu modifizieren sind, ohne bestehende Funktionalität zu zerstören.

Nachfolgend werde ich anhand von Code-Beispielen die einzelnen Prinzipien erklären. Und Interview Beispielfragen und Antworten zeigen.

Single Responsibility Principle (SRP)

Das Single Responsibility Principle (SRP) besagt, dass eine Klasse oder ein Modul nur einen einzigen Grund zum Ändern haben sollte. Mit anderen Worten: Jede Klasse sollte nur eine einzige Verantwortung besitzen, die klar definiert ist.

Bespiel

❌ Schlechtes Design:

class Invoice {
    public void calculateTotal() {
        // logic for calculating total
    }

    public void printInvoice() {
        // logic for printing invoice
    }
    public void saveToDatabase() {
        // logic for saving invoice
    }
}

Hier übernimmt die Klasse Invoice mehrere Aufgaben, die Berechnung der Gesamtsumme, das Drucken der Rechnung und das Speichern in der Datenbank.

✅ Gutes Design:

class Invoice {
    public void calculateTotal() {
        // logic for calculating total
    }
}

class InvoicePrinter {
    public void print(Invoice invoice) {
        // logic for printing invoice
    }
}
class InvoiceRepository {
    public void save(Invoice invoice) {
        // logic for saving invoice
    }
}

Jetzt hat jede Klasse eine Aufgabe.

Interview Fragen

Frage: Was ist der Hauptnachteil, gegen das SRP zu verstoßen?

Antwort: Wenn eine Klasse mehrere Aufgaben hat, können Änderungen an einer Aufgabe die anderen beeinflussen. So wird das Risiko für Bugs erhöht und die Wartbarkeit beeinträchtigt.

Open/Closed Principle (OCP)

Das Open Closed Principle (OCP) besagt, dass Software-Entitäten (wie Klassen, Module oder Funktionen) für die Erweiterung offen, aber für die Änderung geschlossen sein sollten.

Das bedeutet, dass du das Verhalten eines Moduls erweitern kannst, ohne den bestehenden Quellcode ändern zu müssen.

❌ Schlechtes Design

class AreaCalculator {
    public double calculateArea(Object shape) {
        if (shape instanceof Circle) {
            Circle c = (Circle) shape;
            return Math.PI * c.radius * c.radius;
        } else if (shape instanceof Rectangle) {
            Rectangle r = (Rectangle) shape;
            return r.length * r.width;
        }
        return 0;
    }
}

Wenn wir eine Form hinzufügen, müssen wir dir Methode calculateArea anpassen.

✅ Gutes Design

interface Shape {
    double area();
}

class Circle implements Shape {
    double radius;
    Circle(double radius) { this.radius = radius; }
    public double area() { return Math.PI * radius * radius; }
}
class Rectangle implements Shape {
    double length, width;
    Rectangle(double length, double width) { this.length = length; this.width = width; }
    public double area() { return length * width; }
}
class AreaCalculator {
    public double calculateArea(Shape shape) {
        return shape.area();
    }
}

Wir haben Polymorphie angewendet um das OCP einzuhalten und können nun beliebig viele Formen hinzufügen ohne, dass wir an dem bestehenden Code Änderungen vornehmen müssen.

Interview Fragen

Frage: Wie erreichst du das OCP in Java?

Antwort: Abstraktion (Interfaces und abstrakte Klassen) und Polymorphie (Vererbung), so kann Funktionalität hinzugefügt werden ohne bestehenden Code zu ändern.

Liskov Substitution Principle (LSP)

Das Liskov Substitution Principle (LSP) besagt, dass Objekte einer Superklasse mit Objekten ihrer abgeleiteten Klassen austauschbar sein sollen, ohne das Verhalten der Anwendung zu verändern.

Das klingt kompliziert, aber die Idee ist einfach: Eine Unterklasse darf das Verhalten ihrer Superklasse nicht so verändern, dass es für den Code, der die Superklasse verwendet, unerwartete Fehler verursacht.

Beispiel

❌ Schlechtes Design:

class Rectangle {
    protected int width;
    protected int height;

    public void setWidth(int width) { this.width = width; }
    public void setHeight(int height) { this.height = height; }
    public int getArea() { return width * height; }
}

class Square extends Rectangle {
    @Override
    public void setWidth(int width) {
        this.width = width;
        this.height = width;
    }
    @Override
    public void setHeight(int height) {
        this.width = height;
        this.height = height;
    }
}

// Beispiel-Anwendung
Rectangle r = new Square();
r.setWidth(5);
r.setHeight(4);
// Erwartet: Flaeche 20, Tatsaechlich: 16 (wegen der ueberschriebenen Methoden)
System.out.println(r.getArea());

Hier verstößt die Klasse Square gegen das LSP, weil sie das Verhalten der Rectangle-Methoden ändert. Ein Square ist kein Rectangle im Sinne der LSP, da es die Erwartungen des aufrufenden Codes nicht erfüllt.

✅ Gutes Design:

interface Shape {
    int getArea();
}

class Rectangle implements Shape {
    protected int width;
    protected int height;
    // ... Konstruktoren und Getter
    public int getArea() { return width * height; }
}

class Square implements Shape {
    protected int side;
    // ... Konstruktoren und Getter
    public int getArea() { return side * side; }
}

Indem wir ein gemeinsames Interface Shape verwenden, das nur die Methode getArea() definiert, stellen wir sicher, dass Rectangle und Square unabhängig voneinander ihre eigenen spezifischen Eigenschaften und Logiken implementieren, ohne die Verträge der jeweils anderen Klasse zu verletzen.

Interview Fragen

Frage: Wie kannst du Verstöße gegen das LSP erkennen?

Antwort: Ein klarer Hinweis ist, wenn man in der Unterklasse die Methoden der Superklasse so überschreiben muss, dass man eine Ausnahme wirft oder eine unerwartete Logik implementiert, die das grundlegende Verhalten ändert.


Interface Segregation Principle (ISP)

Das Interface Segregation Principle (ISP) besagt, dass Clients nicht gezwungen sein sollen, von Methoden abhängig zu sein, die sie nicht verwenden. Es ist besser, viele spezifische Interfaces zu haben als ein einziges großes, allgemeines Interface.

Das Ziel ist, die Kopplung zu reduzieren und die Wartbarkeit zu erhöhen, indem wir Interfaces erstellen, die genau das abbilden, was ein Client benötigt.

Beispiel

❌ Schlechtes Design:

interface Worker {
    void work();
    void eat();
    void sleep();
}

class HumanWorker implements Worker {
    public void work() { /* ... */ }
    public void eat() { /* ... */ }
    public void sleep() { /* ... */ }
}

class RobotWorker implements Worker {
    public void work() { /* ... */ }
    // Muss essen und schlafen implementieren, obwohl ein Roboter das nicht tut!
    public void eat() { /* ... */ }
    public void sleep() { /* ... */ }
}

Der RobotWorker wird gezwungen, Methoden zu implementieren, die für ihn keinen Sinn ergeben. Das führt zu leerer oder sinnloser Logik und zu unnötigen Abhängigkeiten.

✅ Gutes Design:

interface Workable {
    void work();
}

interface Eatable {
    void eat();
}

interface Sleepable {
    void sleep();
}

class HumanWorker implements Workable, Eatable, Sleepable {
    public void work() { /* ... */ }
    public void eat() { /* ... */ }
    public void sleep() { /* ... */ }
}

class RobotWorker implements Workable {
    public void work() { /* ... */ }
}

Jetzt implementiert jede Klasse nur die Interfaces, die sie auch wirklich benötigt. Dies macht den Code sauberer und reduziert die Kopplung.

Interview Fragen

Frage: Was ist die “Client”-Seite bei der Anwendung des ISP?

Antwort: Der Client ist der Code, der das Interface benutzt. Er sollte nicht vom “dicken” Interface abhängig sein, sondern von einem Interface, das nur die Methoden enthält, die er auch wirklich braucht.

Dependency Inversion Principle (DIP)

Das Dependency Inversion Principle (DIP) besagt, dass hochrangige Module nicht von niedrigrangigen Modulen abhängen sollen, sondern beide von Abstraktionen.

Einfach ausgedrückt: Konkrete Implementierungen (wie eine MySqlDatabase-Klasse) sollten nicht direkt in eurem High-Level-Code (wie einem UserService) verwendet werden. Stattdessen sollten beide von einem Interface oder einer abstrakten Klasse abhängen.

Beispiel

❌ Schlechtes Design:

class MySqlDatabase {
    public void save(String data) {
        // Logik, um Daten in einer MySQL-Datenbank zu speichern
    }
}

class UserService {
    private MySqlDatabase database;

    public UserService() {
        this.database = new MySqlDatabase();
    }

    public void saveUser(String user) {
        database.save(user);
    }
}

Die UserService-Klasse ist ein High-Level-Modul, das die Kernlogik (Benutzerverwaltung) enthält. Sie hängt direkt von der Low-Level-Implementierung MySqlDatabase ab. Wenn wir die Datenbank ändern wollen (z.B. zu PostgreSQL), müssten wir die UserService-Klasse anpassen, was gegen das Open/Closed Principle verstößt.

✅ Gutes Design:

interface Database {
    void save(String data);
}

class MySqlDatabase implements Database {
    public void save(String data) {
        // Logik, um Daten in einer MySQL-Datenbank zu speichern
    }
}

class PostgresDatabase implements Database {
    public void save(String data) {
        // Logik, um Daten in einer PostgreSQL-Datenbank zu speichern
    }
}

class UserService {
    private Database database;

    // Abhaengigkeit wird ueber den Konstruktor injiziert
    public UserService(Database database) {
        this.database = database;
    }

    public void saveUser(String user) {
        database.save(user);
    }
}

Jetzt hängen sowohl das High-Level-Modul (UserService) als auch die Low-Level-Module (MySqlDatabase, PostgresDatabase) von der Abstraktion (Database) ab. Wir können die Implementierung der Datenbank einfach austauschen, ohne den UserService anzufassen.

Interview Fragen

Frage: Was ist der Zusammenhang zwischen dem DIP und dem Konzept der Dependency Injection?

Antwort: Dependency Injection (DI) ist eine Technik, um das Dependency Inversion Principle umzusetzen. Anstatt Abhängigkeiten im Code zu instanziieren, werden sie von außen (z.B. über den Konstruktor) in die Klasse “injiziert”, sodass die Klasse nur noch von Abstraktionen abhängt.

Fazit

Die S.O.L.I.D Prinzipien sind der Wegweiser für uns Entwickler, um aus gutem Code großartigen Code zu machen. Sie sind nicht nur theoretische Konzepte, sondern praktische Werkzeuge, die uns helfen, Systeme zu entwerfen, die leicht zu warten, zu erweitern und zu verstehen sind.

Indem ihr diese Prinzipien in euren täglichen Code-Reviews und Design-Diskussionen anwendet, werdet ihr feststellen, dass eure Codebasis robuster wird, die Zusammenarbeit im Team einfacher wird und ihr mit wachsender Komplexität besser umgehen könnt. Fangt mit einem Prinzip an und arbeitet euch langsam vor. Der Aufwand lohnt sich!