
Die SOLID-Prinzipien zielen darauf ab, objektorientierte Software langfristig wartbarer zu machen. In meinem Blog widme ich jedem der fünf Prinzipien einen eigenen Beitrag.
- Single Responsibility Principle
- Open-Closed Principle
- Liskov-Substitution Principle
- Interface Segregation Principle
- Dependency Inversion Principle
Das Open-Closed Principle (OCP) ist ein Entwurfsprinzip, das ursprünglich von Bertrand Meyer eingeführt und später von Robert C. Martin wieder aufgegriffen und verfeinert wurde. Es bildet zusammen mit vier anderen Prinzipien das Akronym SOLID und steht für dessen zweiten Buchstaben. Es hat folgende Aussage:
Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.
Robert C. Martin
Die Kernaussage des Prinzips ist es, neue oder geänderte Funktionalität umzusetzen, ohne dabei den bestehenden Code zu verändern. In der objektorientierten Programmierung gibt es dafür verschiedene Ansätze, auf die ich im Folgenden eingehen werde.
Vererbung
Das OCP kann über Vererbung realisiert werden. Das ist aus meiner Sicht eher eine schlechte Idee, da es zu einer engen Kopplung zwischen der vererbenden und der erbenden Klasse führen wird. Grundsätzlich würde ich Vererbung nur dann einsetzen, wenn ich das Gesamtbild im Kopf habe und ich weiß, dass es gemeinsame Funktionalität gibt, die in einer zentralen Eltern-Klasse untergebracht und von allen Ableitungen genutzt werden kann. Da sich das zu einem späteren Zeitpunkt aber auch ändern kann, gehe ich mit dem Einsatz von gemeinsamer Funktionalität in einer (abstrakten) Basisklasse eher sparsam um.
Komposition
Eine Alternative zur Vererbung ist ggf. die Komposition. Eine Is-Beziehung (Vererbung) wird dabei zu einer Has-Beziehung. Das erleichtert nicht nur die Austauschbarkeit von Implementierungen, sondern ist auch näher an der echten Welt dran: Eine Tür hat eine Klinke und ein Schloss, sie ist es nicht.
Polymorphie
Interfaces sind eine weitere Möglichkeit das OCP zu implementieren. Grundsätzlich würde ich bei Klassen, die Prozesse repräsentieren, wie klassische „Service“-Klassen am ehesten zu einer reinen Interface-Lösung greifen, da es hier in der Regel darum geht, wie ein solcher Prozess angestoßen wird und was dort reingegeben wird. Wie der Service intern arbeitet ist der jeweiligen Implementierung überlassen, aber der Aufruf sollte immer auf die gleiche Art und Weise erfolgen.
Ein Beispiel
Nehmen wir an, dass wir das Programm einer Autowaschanlage in Code gießen müssen. Es wird absehbar verschiedene Waschprogramme geben, die Ausgangslage und das Ergebnis sollten aber immer gleich sein: Ein Fahrzeug durchläuft das Waschprogramm und ist am Ende sauber.
Es bietet sich daher an, das Waschprogramm durch ein Interface zu repräsentieren, für das es mehrere Implementierungen geben wird. Da der grundsätzliche Ablauf eines Waschprogramms vermutlich von vornherein feststeht und sich die einzelnen Waschprogramme nur durch optionale Schritte oder Faktoren wie Zeit und Intensität unterscheiden, kann man in Erwägung ziehen, statt eines Interfaces zu einer abstrakten Basisklasse zu greifen. Die Klasse sollte bewusst abstrakt sein und nicht etwa das Standard-Waschprogramm repräsentieren, da Ableitungen sowohl weitere Schritte hinzufügen (Premium-Waschgang) als auch Schritte entfernen könnten (Spar-Waschgang) und der Code sehr unübersichtlich werden könnte, wenn man krampfhaft versucht dies rein in den Ableitungen zu realisieren. Ich würde den Code in einer abstrakten Basisklasse daher auf das Nötigste reduzieren.
Bei Wahl einer abstrakten Basisklasse sollte man sich bewusst sein, dass spätere Anpassungen in der Basisklasse teuer werden können, da sie sich auf alle Waschprogramme auswirken und entsprechend getestet werden müssen – und dies dann eben auch gegen das OCP verstößt. Allerdings könnte man hier argumentieren, dass die Wahrscheinlichkeit für einen solchen Umbau zu einem späteren Zeitpunkt eher gering ist, da dabei ja auch die physische Waschanlage umgebaut werden müsste, was teuer ist und eher vermieden werden würde. Eine Waschanlage hat darüber hinaus auch eine endliche Komplexität und ist etwas, das es schon seit Jahren auf dem Markt gibt und das sich in den letzten Jahren nicht großartig weiterentwickelt hat, so dass sich zu Beginn vermutlich gut planen lässt, welche Programmausprägungen es in der Zukunft alle mal geben könnte.
Sollte auch eine Repräsentation des Fahrzeuges für das Waschprogramm erforderlich sein, so würde ich hier primär auf Komposition setzen, da ein Fahrzeug klar in verschiedene Bestandteile zerlegt werden kann und es unterschiedliche Komponenten geben wird, die im Rahmen eines Waschprogramms berücksichtigt werden müssten. Etwa die Reifen, die Karosserie, die Windschutzscheibe, Seitenscheiben und Heckscheibe sowie ggf. auch die Radioantenne. Auch könnten Fahrzeugtyp, Gewicht und die Abmessungen eine Rolle spielen. Da mir kein sinnvoller Basistyp einfällt, würde ich das Fahrzeug als Interface deklarieren. Die aufgezählten Komponenten würde ich als Has-Beziehung in Form von Interfaces in das Fahrzeug hängen.
Ich könnte mir vorstellen, für verschiedene Fahrzeugtypen unterschiedliche Implementierungen dieses Interfaces zu schreiben. Die Implementierungen der Komponenten, die per Komposition eingebunden werden, könnten z.B. als Konstruktor-Parameter in die jeweiligen Fahrzeug-Implementierung injiziert werden, so dass tatsächlich der Code einer Implementierung „Kleinwagen“ nicht angefasst werden müsste, wenn es diesen mit einer besonders empfindlichen Windschutzscheibe geben sollte, für die eine Anpassung des Waschprogramms erforderlich ist.
Beispiele für das OCP aus der Java-Welt
Vieles aus der Java-Welt befolgt das OCP: Die Methoden der Klasse Object
wie toString()
, equals()
und hashcode()
gehören dazu genauso wie die Interfaces Comparable
, Iterable
, Runnable
oder AutoCloseable
. Alle sind in ihrer Verwendung oder Bedeutung fest definiert und erhalten durch Ableitung oder Implementierung die gewünschte Funktionalität in der jeweiligen Klasse.
OCP in Design Patterns
Verhaltensmuster (behavioral design patterns) sind meist gute Beispiele für eine Umsetzung des OCP, da sie häufig Interfaces vorgeben, die von beliebigen Klassen implementiert werden können, damit sie zu einem gemeinsamen Zweck eingesetzt werden können. Beispiele:
- Strategy Pattern (Auswahl eines Algorithmus anhand eines Kontextes)
- State Pattern (unterschiedliche Reaktion auf Ereignisse je nach Zustand)
- Observer Pattern (Komponente registriert sich um auf ein Ereignis reagieren zu können)
- Visitor Pattern (ein Algorithmus wird auf die Elemente einer Datenstruktur angewandt)