Dies ist der zweite Blogpost zu den fünf Programmierprinzipien hinter SOLID. Heute geht es um O: Open-Closed Principle.
Vorwort
Ähnlich wie beim im letzten Blogpost behandelten Single Responsibility Principle gibt es auch beim Open-Closed Principle (OCP) kein unumstössliches "richtig" und "falsch" bei der Umsetzung des Prinzipes. Es braucht einige Programmiererfahrung, um dieses Prinzip strategisch sinnvoll einzusetzen. Es ist also auch bei diesem Post wichtig, sich eigene Gedanken zur Thematik zu machen, und allenfalls andere Meinungen und Interpretationen des Prinzipes zu studieren.
Ziele des Prinzipes
Die Absichten des Open-Closed Principles sind hauptsächlich die folgenden:
- Bestehender, funktionierender Quellcode soll vor Änderungen geschützt werden, welche bisher fehlerfreie Elemente mit Fehlern behaften könnten.
- Es soll helfen, bestehende Module (z.B. Klassen) um neue Funktionalitäten zu erweitern bzw. generell neue Spezifikationen einfach implementieren zu können.
- Ähnlich wie das Single-Responsibility Principle soll OCP dazu beitragen, dass Änderungen an bestehenden Klassen keine oder nur wenige Änderungen an anderen Klassen zur Folge haben.
Das Prinzip - theoretisch
Während sich die Best Practices für dieses Prinzip im Verlauf der Jahre stark änderte, ist der Leitsatz und auch die Definition seit jeher:
Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification
Meyer, Bertrand (1988). Object-Oriented Software Construction.
"Open for extension" meint, dass Module erweitert werden können, auch wenn die bestehenden Klassen nicht verändert werden dürfen. Dazu muss eine entsprechende Softwarestruktur gegeben sein. Erweiterungen können klassischerweise umgesetzt werden, indem eine neue Klasse erstellt wird, welche von der zu erweiternden Klasse erbt. Ein zeitgemässerer Ansatz ist jedoch Abhängigkeiten zu abstrahieren und Erweiterung mit dem Austauschen von Implementation umzusetzen.
"Closed for modification" meint, dass ein Stück Software, zum Beispiel eine Methode, aber typischerweise meint man ganze Klassen, nicht mehr verändert werden dürfen, wenn diese einmal Teil einer Version der Software ist, welche publiziert und damit abgesegnet wurde. Software wird naturgemäss stets verändert. Nach OCP sollen die Änderungen aber keine bestehenden Klassen betreffen.
Insbesondere der "closed" Teil des Prinzips wird häufig absolut definiert, also dass man absolut gar keinen bestehenden Quellcode verändern darf. Dies ist jedoch nicht möglich. Viel mehr soll "Strategic Closure" betrieben werden. Das bedeutet, dass Klassen so designt werden sollen, dass sie gegen bestimmte, zu erwartende künftige Änderungen geschützt sind. Wenn diese Änderungen an der Spezifikation tatsächlich eintreffen, sollen diese so umgesetzt werden können, dass die Klasse nicht geändert werden muss.
Das Prinzip - praktisch
Vererbung
Als das Prinzip erstmals definiert wurde, war es hauptsächlich Vererbung, welche bei dessen Umsetzung zum Zug kam. Ein einfaches Beispiel sei mit folgendem Klassendiagramm gegeben:
Hier wird OCP auf die Klasse Calculator angewendet. Sie soll um Funktionalitäten wie die trigonometrischen Funktionen (sin(), cos(), tan()) ergänzt werden. Zudem könnte der ScientificCalculator die Wurzel-Funktion verbessern, indem er auch komplexe Zahlen als Ergebnis liefern kann. Statt jedoch die Klasse zu verändern, deren Funktionen bereits funktionieren, wird eine neue Klasse namens ScientificCalculator definiert, welche von Calculator erbt. Somit wurden diese neuen Features hinzugefügt, ohne bestehenden Code zu verändern.
Abstraktion
Ein moderner Ansatz ist die Abstraktion. Diese kann in zwei Fällen verwendet werden:
- Eine Klasse muss mit einer Gruppe von Klassen arbeiten, welche sich ähnlich verhalten. Das am häufigsten genannte Beispiel ist eine GUI-Applikation, welche verschiedene geometrische Funktionen (Kreis, Rechteck) zeichnen kann. Mit OCP im Einsatz sollen weitere Formen hinzugefügt werden können, ohne die GUI-Klasse abändern zu müssen. Dazu wird entweder ein Interface oder eine abstrakte Klasse definiert, von welchem alle erben bzw. welches alle Form-Klassen implementieren.
- Eine Abhängigkeit, zum Beispiel eine Service-Klasse, eine Klasse für den Datenbank-Zugriff usw. kann mittels Interface-Segregation entkoppelt werden. Damit ist es möglich, diese Abhängigkeit durch eine neue Implementation, welche neue Features enthält, zu ersetzen, ohne die abhängige Klasse anzupassen.
Wie oben erwähnt, ist es ohnehin nicht möglich, alle bestehenden Klassen so zu entwerfen, dass bei egal welcher Änderung an den Spezifikationen kein bestehender Quellcode angepasst werden muss. Stattdessen muss strategisch vorgegangen werden: Als Programmierer muss man überlegen, an welchen Stellen im Programm welche Arten von Änderungen zu einem späteren Zeitpunkt nötig werden könnten. An diesen Stellen sollte OCP angewendet werden, sodass diese Änderungen ohne Anpassung der bestehenden Klassen möglich sein werden.
Ein weiterer Anhaltspunkt, ob eine Klasse dem OCP unterworfen werden soll, also als nie-mehr-zu-ändern erklärt werden soll, ist, wie gross die Folgeschäden wären, wenn sie verändert werden würde. Dies ist in der Regel gleichbedeutend mit wie oft die Klasse im gesamten Programm verwendet wird. Denn falls viele andere Klassen von dieser Klasse abhängen könnte eine Änderung einen (ungewollten) Effekt auf grosse Teile des Programms haben. Deshalb wäre es dort sehr sinnvoll, OCP umzusetzen. Andere Klassen, welche sich am "Rand" der Applikation befinden und nur an wenigen Stellen im Code zum Einsatz kommen können ohne schlechtes Gewissen von OCP ausgenommen werden, da die Folgeschäden einer Änderung überschaubar sind.