Objektorientierte Programmiersprachen dominieren seit langem den Markt. Ob Java, C#, Python und sogar JavaScript. Unter den beliebtesten Programmiersprachen lässt sich keine finden, die nicht zumindest mit Objekten umgehen kann. Ein Grund für die Popularität der Objektorientierung ist die einfache Erlernbarkeit und Verständlichkeit. Nichtsdestotrotz kann man bei der Entwicklung mit objektorientierten Architekturen auch viel falsch machen. Um Fehlern vorzubeugen wurden daher Prinzipien entwickelt, an die sich Entwickler halten sollten. Eine Gruppe dieser Prinzipien ist unter dem Namen SOLID bekannt die ich Ihnen nun vorstellen möchte.
Die SOLID-Prinzipien wurden durch den Softwareentwickler und langjährigen Author Robert Cecil Martin bekannt. SOLID ist dabei ein Akronym für fünf einfache Prinzipien:
- Single-Responsibility-Prinzip
- Open-Closed-Prinzip
- Liskovsches Substitutionsprinzip
- Interface-Segregation-Prinzip
- Dependency-Inversion-Prinzip
Alle diese Prinzipien dienen dazu den erstellten Code so zu strukturieren, dass er wartbar bleibt was dadurch zu einer längeren Lebensdauer der Applikation führt.
Single Responsibility
Das Single-Responsibility-Prinzip besagt, dass jede Klasse nur eine Aufgabe zu erfüllen hat. Alle Methoden dieser Klasse sollten für diese Aufgabe allein benötigt werden.
Dieses Prinzip lässt sich nicht nur auf Klassen anwenden, sondern zieht sich durch die gesamte Architektur. So sollten alle Bausteine von Modulen bis hin zu einzelnen Methoden eine klar definierte Funktion erfüllen. Der Entwickler sollte sich immer über diese Funktion bewusst sein und falls er Abweichungen feststellt die Funktionalität aufteilen. Die Einhaltung dieser Regel erleichtert die Wartbarkeit des Programms ungemein.
Softwarentwickler haben oft das Problem die richtigen Namen zu ihren Klassen, Variablen und Methoden zu finden. Wird das Single-Responsibility-Prinzip richtig umgesetzt, so ergibt sich der Name der Methode automatisch aus ihrer Funktionalität und die Wartung und Fehlersuche wird für alle Teammitglieder erleichtert.
Open closed
Eine der Grundlagen der Objektorientierten Programmierung ist Polymorphe, also das Erweitern von Klassen und Methoden. Das Open-Closed-Prinzip beschreibt wie diese Erweiterbarkeit auszusehen hat.
Open bedeutet dabei, dass eine Klasse offen sein muss für Änderungen. Sie soll, wie nach dem Single-Responsibility-Prinzip, so aufgebaut sein, dass sich einzelne Funktionalitäten abkapseln und erweitern lassen.
Im Bezug auf ihre Funktionalität soll die Klasse jedoch geschlossen sein. Dafür steht das Closed. Funktionalität soll durchaus erweiterbar, jedoch nicht veränderbar sein. Habe ich beispielsweise eine verkettete Liste und erweitere diese zu einer doppelt verketteten Liste, so bleibt ihre Funktionalität gleich.
Liskovsche Substitution
Das Liskovsche-Substitutionsprinzip ist eng mit dem Open-Closed-Prinzip verwandt. Es besagt, dass eine Unterklasse deren Oberklasse jederzeit ersetzten können müsste und dabei das gleiche Verhalten an den Tag legt. Der damit arbeitende weiß in diesem Fall nicht, und es ist ihm auch völlig egal, ob er mit der Unter- oder der Oberklasse arbeitet.
Barbara Liskov und Jeannette Wing habe dies so formuliert:
Barbara Liskov ; Jeannette Win: Behavioral Subtyping Using Invariants and Constraints
“Let be a property provable about objects of type . Then should be true for objects of type where is a subtype of .
Auch ist die verkettete Liste ein gutes Beispiel. Arbeitet man mit dieser Liste, so ist es völlig egal mit welcher Unterklasse gearbeitet wird oder um welche Funktionalität die Liste erweitert wurde.
Interface Segregation
Das Prinzip der Interface Segregation sagt aus, dass Interfaces möglichst klein gehalten werden sollten. Große Interfaces sollen möglichst in mehrere kleinere überführt werden, die jeweils für ihre Teilaufgaben benötigt werden.
Besonders wichtig ist dieses Prinzip, wenn Interfaces später durch Dritte implementiert werden müssen, wie dies häufig bei Event-Handler in GUI-Frameworks der Fall ist. Ein sehr bekannter Ableger eines schlanken Interfaces ist beispielsweise das Comparable-Interface von Java. Dies hätte man auch mit mehreren Methoden wie isBigger() oder isEqual() etc. lösen können. Die Java-Architekten haben sich jedoch für eine einzige Methode compareTo() entschieden, da diese alleine alle nötigen Informationen liefern kann.
Dependency Inversion
Bei der Dependency Inversion geht es darum Module höhere Ebene von Modulen niedriger Ebene zu entkoppeln. Damit wird vermieden, dass Änderungen in Modulen niedriger Ebene sich auf Module höherer Ebene auswirken. Dieses Prinzip lässt sich durch zwei Regeln, welche von Robert C. Martin stammen, umsetzen:
- Module höherer Ebene sollten nicht von Modulen niederer Ebene abhängen. Beide sollten von Abstraktionen abhängen.
- Abstraktionen sollten nicht von Details abhängen. Details sollten von Abstraktionen abhängen.
Das Umkehren der Abhängigkeiten bedeutet in diesem Fall nicht, dass niedrige Ebenen von höheren Ebenen abhängig sein sollten. Sie sollten beide von Interfaces abhängen.
Das ganze wird realisiert, indem niedere Services Interfaces Implementieren und diese Interfaces dem höheren Modul bereitstellen.
Ein Beispiel hierfür wäre das Lesen einer Datei. Ein höheres Modul hat dabei die Aufgabe eine Datei auszuwerten, während ein niederes Modul die Aufgabe hat die Datei als Stream bereit zu stellen. Das niedere Modul stellt ein Interface dafür bereit auf den das höhere zugreifen kann. Dem höheren Modul ist es dabei egal wie das niedere seine Aufgabe löst. Es greift nur auf das Interface zu. Das niedere Modul könnte die Datei beispielsweise über die Festplatte oder das Netzwerk bereitstellen ohne das dies einen Unterschied machen würde.
Die Abhängigkeit wurde in diesem Fall umgekehrt indem das niedere Modul die Aufgabe hat den Stream zu implementieren, welcher durch das höhere Modul gefordert wird und nicht wie üblich das höhere Modul davon abhängig mit welcher Methode Dateien gelesen werden müssen.
Gerade wenn ein Softwareprojekt über mehrere Teams verteilt ist funktioniert dies nur schwer. In den meisten Fällen wird das Team welches für das Lesen von Dateien beispielsweise verantwortlich ist die nutzbaren Funktionen selbst bestimmen und die API herausgeben. Das Team welches diese API nun benutzen möchte kann schwer Vorgaben an das andere Team machen wie das Interface auszusehen hat. Die Lösung um der Dependency Inversion gerecht zu werden ist das Adapter-Pattern. Dieses kann die API verschleiern und ein beliebiges Interface nach aussen hin anbieten.