Einleitung: SOLID im Kontext von C++
SOLID ist ein Akronym für fünf fundamentale Prinzipien des objektorientierten Designs, formuliert von Robert Martin. Für C++, eine Sprache mit starker statischer Typisierung, Mehrfachvererbung und RAII, gewinnen diese Prinzipien eine besondere Bedeutung. Auf fortgeschrittenem Niveau ermöglicht das Verständnis von SOLID die Erstellung flexibler, testbarer und erweiterbarer Systeme und vermeidet Antipatterns wie „gottgleiche Klassen“ oder enge Kopplung.
In diesem Artikel analysieren wir jedes Prinzip anhand praktischer C++-Codebeispiele (17/20) und konzentrieren uns auf die Nuancen der Implementierung: die Verwendung von std::unique_ptr, std::function, CRTP, virtuellen Destruktoren und modernen Redewendungen.
1. Single Responsibility Principle (SRP) – Prinzip der einzigen Verantwortung
Formulierung: Eine Klasse sollte nur einen einzigen Grund für eine Änderung haben.
In der Praxis bedeutet dies, dass eine Klasse für eine Geschäftseinheit oder eine einzige Operation verantwortlich ist. Ein Verstoß gegen SRP führt zu einer „Verkettung“ von Verantwortlichkeiten, was das Testen und die Modifikation erschwert.
Beispiel für einen Verstoß
class Report {public: void generate() { /* Daten erstellen */ } void saveToFile(const std::string& path) { /* in Datei schreiben */ } void sendEmail(const std::string& to) { /* senden */ }};Hier ist Report sowohl für die Generierung als auch für das Speichern und Senden verantwortlich – drei Gründe für eine Änderung.
Refactoring mit SRP
class Report {public: void generate() { /* Daten erstellen */ }};
class ReportSaver {public: void save(const Report& report, const std::string& path);};
class EmailSender {public: void send(const Report& report, const std::string& to);};Jetzt hat jede Klasse eine einzige Verantwortung. Dies vereinfacht Modultests und die Wiederverwendung.
2. Open/Closed Principle (OCP) – Prinzip der Offenheit/Geschlossenheit
Formulierung: Software-Entitäten sollten für Erweiterungen offen, aber für Modifikationen geschlossen sein.
In C++ wird dies oft durch Polymorphie (virtuelle Funktionen) oder durch Templates (CRTP) implementiert. Für das fortgeschrittene Niveau betrachten wir beide Techniken.
Polymorpher Ansatz (virtuelle Funktionen)
class Shape {public: virtual double area() const = 0; virtual ~Shape() = default;};
class Circle : public Shape { double r;public: Circle(double r) : r(r) {} double area() const override { return 3.14 * r * r; }};
class Rectangle : public Shape { double w, h;public: Rectangle(double w, double h) : w(w), h(h) {} double area() const override { return w * h; }};Das Hinzufügen einer neuen Form (z. B. Triangle) erfordert keine Änderung vorhandener Klassen – nur die Erstellung eines neuen Erben.
Template-Ansatz (CRTP – Curiously Recurring Template Pattern)
template<typename Derived>class ShapeBase {public: double area() const { return static_cast<const Derived*>(this)->area_impl(); }};
class Circle : public ShapeBase<Circle> { double r;public: Circle(double r) : r(r) {} double area_impl() const { return 3.14 * r * r; }};CRTP bietet statische Polymorphie ohne den Overhead virtueller Aufrufe, was für High-Performance-C++ entscheidend ist.
3. Liskov Substitution Principle (LSP) – Prinzip der Substitution von Barbara Liskov
Formulierung: Objekte einer Basisklasse sollten durch Objekte abgeleiteter Klassen ersetzt werden können, ohne die Korrektheit des Programms zu beeinträchtigen.
Ein klassischer Verstoß gegen LSP in C++ ist das Überschreiben einer virtuellen Methode mit abgeschwächten Vorbedingungen oder verstärkten Nachbedingungen. Betrachten wir ein Beispiel mit der Hierarchie „Rechteck – Quadrat“.
Antipattern
class Rectangle {protected:\