11. Jänner 2021 von Harun Sevinc
Die SOLID-Design-Prinzipien
Was sind die SOLID-Prinzipien?
Bei den SOLID-Prinzipien handelt es sich um Designvorgaben, die in der Softwareentwicklung zum Einsatz kommen. Der Begriff wurde durch Robert C. Martin geprägt und beschreibt fünf Design-Prinzipien, die vorgeben, wie Funktionen und Datenstrukturen in Klassen angeordnet sind und wie diese Klassen miteinander verbunden sein sollen.
Ziel von SOLID ist die Erzeugung von Software, die Modifikationen toleriert, leicht nachvollziehbar ist und die eine Basis der Komponenten bildet, die in vielen Softwaresystemen eingesetzt werden.
Im Folgenden soll ein möglichst verständlicher Überblick der Prinzipien gegeben werden. Dabei werde ich nicht zu sehr ins Detail gehen, da weitaus umfangreichere Publikationen und Artikel zu den einzelnen Prinzipien existieren.
- SRP: Single-Responsibility-Prinzip
- OCP: Open-Closed-Prinzip
- LSP: Liskov’sche Substitutions-Prinzip
- ISP: Interface-Segregation-Prinzip
- DIP: Dependency-Inversion-Prinzip
SRP Single Responsibility-Prinzip
Beim Single-Responsibility-Prinzip (SRP), handelt es sich um das wohl am meisten missverstandene Prinzip der SOLID-Prinzipien. Softwareentwickelnde neigen laut Robert C. Martin dazu, anzunehmen, dass jedes Modul ausschließlich eine einzige Aufgabe erfüllen sollte. Das Prinzip Refactoring zu betreiben und bei Funktionen mit großem Umfang auf die niedrigste Ebene aufzuteilen existiert zwar, allerdings ist dies nicht mit SRP gemeint. Im Allgemeinen lautet die Beschreibung des SRP:
„Es sollte nie mehr als einen Grund geben, eine Klasse zu modifizieren.“
Da aber in den meisten Fällen Software für Kunden/Stakeholder oder User geschrieben wird und der Begriff der Klassen an dieser Stelle zu spezifisch ist, wird folgender Ausspruch von Robert C. Martin empfohlen:
„Ein Modul sollte für einen, und nur einen, Akteur verantwortlich sein“
Als Beispiel könnte man ein Modul nehmen, welches Funktionen für verschiedene Geschäftsbereiche zur Verfügung stellt. Sollte sich in einem Geschäftsbereich eine Änderung ergeben, sodass sich die Struktur der Klasse ändern muss, könnte dies Auswirkungen auf den Teil der Klasse haben, der von einem anderen Teil der Organisation benötigt wird. Das schafft Abhängigkeiten, die für die Softwareentwicklung blockierend sein können. Somit wäre es sinnvoll, Klassen und Methoden so zu gestalten, dass nur ein Akteur die Anforderungen und Anpassungen verantwortet.
Beim SRP geht es also hauptsächlich darum, Funktionen und Klassen und deren Verbindung zu Akteuren, die Anforderungen schaffen.
OCP Open-Closed-Prinzip
Das Open-Closed-Prinzip (OCP) wurde von Bertrand Meyer formuliert:
„Eine Softwareentität sollte offen für Erweiterungen, aber zugleich auch geschlossen gegenüber Modifikationen sein.“
Oder auch: Das Verhalten einer Softwareentität sollte so erweiterbar sein, dass sie nicht modifiziert werden muss. Man könnte nun sagen, führt eine Änderung an der Software dazu, dass massive Eingriffe in diese erforderlich sind, dann hat die Architektur der Software versagt.
Welche Möglichkeiten gibt es denn, Software zu erweitern, ohne diese zu modifizieren? Als Martin in den 90er Jahren das Prinzip von Meyer übernahm setzte er es technisch anders um. Für Meyer bestand die Lösung nämlich darin, die in der objektorientierten Welt bekannte Vererbung zu nutzen. Für die damalige Zeit war dies ein wichtiger Faktor hin zur Wartbarkeit und Erweiterbarkeit von Software. Ein Beispiel: Nehmen wir an, wir haben die beiden Klassen PKW und Sportwagen. Die Klasse des Sportwagens würde alle wichtigen Eigenschaften und Funktionalitäten der PKW-Klasse vererbt bekommen. Spezifische Funktionalitäten und Eigenschaften würden dann in der Klasse Sportwagen hinzugefügt werden. Die Abhängigkeit geht hier nur in eine Richtung und das ist gut so.
Um einen Schritt weiter zu gehen, kann man dieses Beispiel noch mit der Nutzung von Interfaces erweitern. Genau dies macht auch Martin in seinem Beispiel.
OCP ist somit in seiner erweiterten Version ein sinnvolles und heute weitverbreitetes Prinzip. Obwohl bei der Vererbung eine Aufteilung entsteht, gibt es beispielsweise in Java keine echte Mehrfachvererbung. Stattdessen werden Interfaces genutzt, die wiederum vermehrt in eine Klasse implementiert werden können. Ziel und Sinn sollte es immer sein, Klassen einer höheren Hierarchie vor Modifikationen in Klassen niedrigerer Hierarchie zu schützen.
LSP Liskov‘sches Substitutionsprinzip
LSP wurde durch Barbara Liskov wie folgt definiert:
„Wenn für jedes Objekt o1 vom Typ S ein Objekt o2 vom Typ T existiert, sodass für alle Programme P, die in T definiert sind, das Verhalten von P unverändert bleibt, wenn o1 für o2 substituiert wird, dann ist S ein Subtyp von T.“
Prinzipiell ist hiermit gemeint, dass bei der Vererbung die Unterklasse jederzeit alle Eigenschaften der Oberklasse enthalten muss, so dass diese von der Oberklasse verwendet werden können. Die Unterklasse darf keine Änderungen der Funktionalitäten der Oberklassen enthalten. Die Oberklasse darf jedoch durch neue Funktionalitäten erweitert werden.
Schauen wir uns wieder unser PKW-Beispiel an. Wir haben die Oberklasse PKW. Diese bietet bestimmte Funktionen – etwa Beschleunigen und Bremsen. Nun haben wir aber zwei Unterklassen, nämlich Sportwagen und Kleinwagen. Beide Unterklassen müssen die Methoden der Oberklassen jederzeit nutzen können. Jedoch darf die Unterklasse erweiterte Eigenschaften besitzen. Beispielsweise könnte der Sportwagen noch die Funktion „Sportmodus aktivieren“ enthalten, welche die Fahreigenschaften des Fahrzeugs manipuliert.
LSP geht somit einen Schritt weiter als OCP und stellt Bedingungen bei der Mehrfachvererbung an Unterklassen durch die Oberklasse.
ISP Interface-Segregation-Prinzip
Das ISP dient dazu, User nicht dazu zu zwingen, Teile von Schnittstellen zu implementieren, die nicht benötigt werden. Dies soll dazu führen, dass Schnittstellen nicht zu groß werden und das sie nicht auf einen speziellen Nutzen schrumpfen.
In einer grafischen Darstellung ist dies leicht zu erkennen. Die folgende Abbildung zeigt, dass die Oberklasse (Klasse) mehrere Operationen implementiert hat (Op1,Op2,Op3). User1 nutzt allerdings nur Op1 und User2 sowie User3 nur Op2 beziehungsweise Op3. In diesem Fall wäre User1, obwohl die Operation nicht aufgerufen wird, von Op2 und Op3 abhängig. Würde man beispielsweise an der Implementation von Op2 in der Oberklasse etwas ändern, dann müsste auch für User1 eine komplette neue Kompilierung sowie ein Deployment durchgeführt werden. Und das, obwohl es faktisch keine Änderung an den von User1 genutzten Modulen gibt.
Die Lösung für dieses Problem wäre die Aufspaltung der Operationen in Schnittstellen, wie in der nächsten Abbildung zu sehen ist. In einer statisch typisierten Sprache – etwa Java – wäre der Quellcode von User1 nur von den Klassen User1Op1 und der dazugehörigen Op1 abhängig und nicht mehr von Klasse.
Das ISP soll verhindern, dass Modulabhängigkeiten, die unnötige Last mit sich tragen, völlig unerwartete Probleme verursachen. Mit einer Minderung der Abhängigkeiten wird dafür gesorgt, dass Code-Änderungen nicht zu komplexen und umfangreichen Änderungen oder Problemen führen können. Der Mehraufwand durch eine weitere Schicht sorgt im Nachhinein dafür, dass die Architektur mit Modifikationen besser umgehen kann.
DIP Dependency-Inversion-Prinzip
Bei dem letzten der fünf SOLID-Prinzipien handelt es sich um das Dependency-Inversion-Prinzip (DIP). Das DIP soll deutlich machen, dass Systeme am flexibelsten sind, in denen sich Quellcodeabhängigkeiten ausschließlich auf Abstraktionen beziehen, statt auf Konkretionen.
In Java bedeutet es, dass Anweisungen bei der Nutzung von use, import und include nur auf Quellmodule bezogen werden sollten – etwa Schnittstellen, abstrakte Klassen oder Module, die jede andere Form der Abstraktion gewährleisten. Damit soll erzwungen werden, dass keine Abhängigkeiten zu konkreten Modulen entstehen.
Dieses Prinzip als Regel zu nutzen ist allerdings alles andere als realistisch, da Softwaresysteme auch von konkreten Entitäten abhängig sind. In Java ist beispielsweise die Klasse String sehr konkret gestaltet. Zu versuchen, sie abstrakt zu gestalteten wäre nicht sehr sinnvoll. Hier sollte auch nicht die Abhängigkeit zum String-Objekt vermieden werden.
Aufgrund dieses Argumentes sollte man sich bei DIP hauptsächlich auf die Teile der Software beziehen, an der gearbeitet wird und die offensichtlich für Modifikationen zugänglich sind.
Was lässt sich daraus ableiten?
Zusammenfassend lässt sich sagen, dass jedes der Prinzipien einen wesentlichen Einfluss auf die Entwicklung von guter Softwarearchitektur haben kann. Die Prinzipien müssen dafür richtig gedeutet werden und im Kontext der Software eingesetzt werden.
Ich würde die Prinzipien nicht als die Grundpfeiler der guten Softwarearchitektur bezeichnen, sondern eher als gute Basis, die jeder Software Developer und Architect verinnerlicht haben sollte.
Man erkennt, dass die Prinzipien teilweise rekursiv miteinander verbunden sind oder aufeinander aufbauen. SOLID-Prinzipien tauchen immer wieder bei höheren architektonischen Themen auf und sind somit wichtiges Know-how für alle angehenden Softwareentwickelnden und Architektinnen und Architekten.
Ihr möchtet mehr zu spannenden Themen aus der adesso-Welt erfahren? Dann werft doch auch einen Blick in unsere bisher erschienenen Blog-Beträge.