SOLID Prinzipien

Testbarkeit und SOLID

Die Testbarkeit ist ein wesentlicher Bestandteil moderner Softwareentwicklung. Gut getesteter Code ist robuster, wartbarer und verlässlicher. In Kombination mit den SOLID-Prinzipien kann die Testbarkeit signifikant verbessert werden. SOLID ist ein Akronym, das fünf Prinzipien umfasst, die helfen, Softwaredesign zu optimieren und die Testbarkeit zu steigern. In diesem Text wird untersucht, wie SOLID die Testbarkeit beeinflusst, einschließlich Beispielen in C++ und der Diskussion von Vorteilen und Nachteilen.

1. Single Responsibility Principle (SRP)

Das Single Responsibility Principle besagt, dass eine Klasse nur eine einzige Verantwortung haben sollte. Eine Klasse sollte nur für eine Aufgabe zuständig sein und diese effizient ausführen.

Beispiel in C++:

class Order {
public:
    void calculateTotal() { /* Berechnung des Gesamtbetrags */ }
};

class OrderPrinter {
public:
    void printOrder(const Order& order) { /* Drucke Bestellung */ }
};

Hier hat die Order-Klasse nur die Verantwortung für die Berechnung des Gesamtbetrags. Das Drucken der Bestellung ist der OrderPrinter-Klasse zugewiesen.

Vorteile:

  • Jede Klasse ist einfacher zu testen, da sie nur eine spezifische Aufgabe hat.
  • Änderungen an einer Klasse beeinflussen nur die Funktionalität, die sie direkt betrifft.

Nachteile:

  • Kann zu einer größeren Anzahl an Klassen führen, was den Code komplexer macht.
  • Zu viel Granularität kann den Code unübersichtlich machen.

2. Open/Closed Principle (OCP)

Das Open/Closed Principle besagt, dass eine Klasse offen für Erweiterungen, aber geschlossen für Modifikationen sein sollte. Das bedeutet, dass bestehender Code nicht geändert werden soll, sondern durch Erweiterungen angepasst wird.

Beispiel in C++:

class Shape {
public:
    virtual double area() const = 0;
};

class Circle : public Shape {
public:
    double area() const override { return 3.14 * radius * radius; }
private:
    double radius;
};

class Rectangle : public Shape {
public:
    double area() const override { return width * height; }
private:
    double width, height;
};

Neue Formen können hinzugefügt werden, ohne die bestehenden Formen zu ändern. Das ermöglicht es, die Klassen ohne umfangreiche Tests zu erweitern.

Vorteile:

  • Erweiterungen sind einfach hinzuzufügen, ohne bestehende Logik zu beeinflussen.
  • Verhindert das Überarbeiten von Code, was die Testbarkeit erhöht.

Nachteile:

  • Bei falscher Implementierung kann die Anzahl der Klassen schnell wachsen.
  • Fehlende Abwärtskompatibilität kann zu Problemen führen.

3. Liskov Substitution Principle (LSP)

Das Liskov Substitution Principle besagt, dass Objekte von abgeleiteten Klassen anstelle von Objekten der Basisklasse verwendet werden können, ohne das Verhalten des Programms zu verändern.

Beispiel in C++:

class Bird {
public:
    virtual void fly() = 0;
};

class Sparrow : public Bird {
public:
    void fly() override { /* Fliegen implementieren */ }
};

class Penguin : public Bird {
public:
    void fly() override { /* Penguin kann nicht fliegen, wir werfen eine Ausnahme */ }
};

In diesem Beispiel ist Penguin keine geeignete Unterklasse von Bird, da das fly()-Verhalten für Pinguine unpassend ist.

Vorteile:

  • Erhöht die Zuverlässigkeit des Codes und erleichtert das Testen, da jede Klasse die Basislogik korrekt erweitert.
  • Reduziert Seiteneffekte bei der Verwendung von Unterklassen.

Nachteile:

  • Verletzung des Prinzips kann dazu führen, dass abgeleitete Klassen unerwartete oder fehlerhafte Verhaltensweisen aufweisen.
  • Es kann schwierig sein, das Prinzip korrekt umzusetzen, wenn die Basisklasse zu allgemein ist.

4. Interface Segregation Principle (ISP)

Das Interface Segregation Principle besagt, dass eine Klasse nur die Methoden implementieren sollte, die sie wirklich benötigt. Ein Interface sollte nicht zu groß oder unübersichtlich sein.

Beispiel in C++:

class Printer {
public:
    virtual void print() = 0;
};

class Scanner {
public:
    virtual void scan() = 0;
};

class MultiFunctionPrinter : public Printer, public Scanner {
public:
    void print() override { /* Drucken */ }
    void scan() override { /* Scannen */ }
};

Das Interface ist auf die jeweiligen Aufgaben aufgeteilt, sodass Klassen nur die für sie relevanten Methoden implementieren müssen.

Vorteile:

  • Klassen sind spezialisierter und leichter zu testen.
  • Verhindert unnötige Abhängigkeiten und Implementierungen von Methoden, die nicht verwendet werden.

Nachteile:

  • Kann zu einer größeren Anzahl an Schnittstellen führen, die den Code komplexer machen.
  • Bei komplexeren Projekten kann die Verwaltung von vielen kleinen Interfaces schwierig werden.

5. Dependency Inversion Principle (DIP)

Das Dependency Inversion Principle besagt, dass Hochlevel-Module nicht von Lowlevel-Modulen abhängen sollten. Beide sollten von Abstraktionen abhängen. Auch die Abstraktionen sollten nicht von Details abhängen, sondern Details von Abstraktionen.

Beispiel in C++:

class IPrinter {
public:
    virtual void print() = 0;
};

class LaserPrinter : public IPrinter {
public:
    void print() override { /* Laser Drucken */ }
};

class Client {
private:
    IPrinter& printer;
public:
    Client(IPrinter& p) : printer(p) {}
    void printDocument() { printer.print(); }
};

Die Client-Klasse hängt nicht von einer konkreten Drucker-Implementierung ab, sondern nur von der Abstraktion IPrinter. Dies ermöglicht es, den Druckertyp einfach zu wechseln, ohne die Klasse zu ändern.

Vorteile:

  • Fördert lose Kopplung und vereinfacht das Testen, da Abhängigkeiten leicht gemockt werden können.
  • Erhöht die Flexibilität und Erweiterbarkeit des Systems.

Nachteile:

  • Abhängigkeiten und Abstraktionen können den Code komplizierter machen.
  • Das Mocken von Abhängigkeiten kann beim Testen mehr Aufwand erfordern.

Fazit: Testbarkeit durch SOLID

Die Anwendung der SOLID-Prinzipien führt zu gut strukturiertem, wartbarem und testbarem Code. Jede der Prinzipien trägt dazu bei, den Code in einer Weise zu gestalten, die das Testen vereinfacht und Fehler reduziert. Insbesondere die Trennung von Verantwortlichkeiten, die Unterstützung von Erweiterungen ohne Modifikationen und die Vermeidung unnötiger Abhängigkeiten machen den Code robuster und leichter verständlich. Es gibt jedoch auch Nachteile, wie die erhöhte Anzahl von Klassen und die Notwendigkeit, mehr Abstraktionen zu schaffen. Insgesamt trägt die Anwendung von SOLID jedoch signifikant dazu bei, die Testbarkeit und Qualität des Codes zu verbessern.

Einen Grundlagen-Artikel gibt es hier: SOLID Design Prinzipien​ und Software Engineering

VG WORT Pixel