SOLID

Aus Programmieren-Wiki
🚧 Diese Seite befindet sich in Bearbeitung 🚧

Grundprinzipien zum Software-Design: Schnittstellenprogrammierung und die SOLID-Kriterien Gegen Schnittstellen programmieren anstatt Implementierungen. Siehe dazu auch Seite: Interface statt konkreter Klasse

Mittlerweile sollten Interfaces (z. dt. Schnittstellen) bekannt und ggf. sogar mit ihnen gearbeitet worden sein.

Einer der Gründe, warum Interfaces benutzt werden sollten, ist das einfache Austauschen von Implementierungsdetails.

Eigentlich kann es egal sein, ob eine ArrayList oder eine LinkedList verwendet wird, die Methoden des Interface List funktionieren.

Um uns diesen Vorteil zu nutze zu machen programmieren wir gegen Schnittstellen.

Ein Beispiel:

ArrayList list = new ArrayList();

An dieser Zeile ist sowohl syntaktisch als auch semantisch nichts auszusetzen. Es wird eine neue ArrayList erzeugt, mit der alle Funktionen der ArrayList-Klasse verwendet werden können. Wir können eine ArrayList aber auch als Parameter einer Methode verwenden:

public void maxElement(ArrayList list) {...}

Auch an diesem Codeschnipsel ist nichts falsch.

Was passiert aber, wenn wir jetzt eine LinkedList verwenden möchten? In diesem Fall müssen alle relevanten Stellen des Projekts durchsucht werden und alle vorkommen der ArrayList manuell durch eine LinkedList ausgetauscht werden.

Um diesen Aufwand zu vermeiden können wir Java mitteilen, dass es uns ausreicht eine Liste zu verwenden, wir also irgendeine Implementierung des Interfaces List verwenden können:

List list = new ArrayList(); // oder auch LinkedList

public void maxElement(List list) {...}

Die SOLID-Kriterien

Was ist SOLID?

Je größer ein Softwareprojekt wird, desto wichtiger ist eine gute Vorausplanung. Unabhängig von speziellen Implementierungsdetails werden Module, Klassen und Schnittstellen festgelegt.

Ein sorgfältig durchdachter Entwurf kann die Produktionskosten der Software erheblich reduzieren, da Versprechen gemacht worden sind, an die sich das Entwicklerteam halten muss.

Beispielsweise kann sich Team A darauf verlassen, dass Team B eine Klasse mit Funktion x bereitstellt und diese auch genau das leistet, was Team A erwartet.

Auch Änderungen, die sich notwendigerweise während der Implementierung ergeben, können bei einem guten Entwurf weniger Aufwand mit sich bringen.

Eine Funktionalität, die in eine Klasse gekapselt wurde, kann leichter abgeändert werden, als dieselbe Funktionalität in 100 Klassen.

Ab wann aber ist ein Entwurf gut? Ab wann lohnt es sich einen Entwurf zu implementieren? Genau dafür gibt es die hier vorgestellten Prinzipien und das Acronym:

  • Single responsibility design
  • Open/closed principle
  • Liskov substitution principle
  • Interface segregation principle
  • Dependency inversion principle

S - Single responsibility design

Jede Klasse sollte genau eine Verantwortung haben. Nach dieser freien Übersetzung schauen wir uns an, was damit gemeint ist:

Wenn wir eine Klasse haben und überlegen, warum sich diese ändern kann, dann sollte uns nur ein Grund einfallen.

Ein Beispiel:

"[...] consider a module that compiles and prints a report. Such a module can be changed for two reasons.

First: The content of the report can change.

Second: The format of the report can change.

These two things change for very different causes, one substantive, and one cosmetic. The single responsibility principle says that these two aspects of the problem are really two separate responsibilities, and should therefore be in separate classes or modules. It would be a bad design to couple two things that change for different reasons at different times." - Quelle

Dieses Prinzip heißt nicht, dass jede Methode automatisch eine eigene Klasse erhalten muss, weil sie ja etwas anderes tut als alle anderen Methoden.

Es sagt lediglich aus, dass Methoden die nichts direkt mit einer Klasse zu tun hat (zum Beispiel eine Methode zum speichern eines Users in der User-Klasse statt der Server-Klasse) dann auch nicht in dieser Klasse sein sollte.

Ein weiteres, sehr gutes Beispiel wird hier erläutert.

O - Open/closed principle

Das open/closed principle stellt an Softwareelemente (Klassen, Module, Methoden) zwei Bedingungen:

  1. "Open"
    Das Element soll offen für Erweiterungen sein
  2. "Closed"
    Das Element soll in seiner Funktionalität abgeschlossen sein, also keine Modifikationen erlauben

Wie kann dieses Prinzip durchgesetzt werden? Durch Vererbung:

Allerdings streiten sich hierbei Experten, was genau "schöner" ist. Durch Erben der Eigenschaften einer Basisklasse ist zwar die Implementierung geschützt, allerdings kann sich die Schnittstelle ändern. Eine gleichbleibende Schnittstelle ist aber ebenso eine wünschenswerte Eigenschaft.

Die zweite Möglichkeit ist es, eine abstrakte Basisklasse zu verwenden. Auch hier ist die Schnittstelle (die abstrakte Basisklasse) geöffnet für Erweiterung(durch die Vererbung) aber geschlossen für Modifikationen.

L - Liskovsches Substitutionsprinzip

Sei u ein Objekt der Unterklasse U und O deren Oberklasse.

Das liskovsche Substitutionsprinzip besagt, wenn jedes Objekt o vom Typ O durch ein Exemplar u des Typs U ersetzt wird, muss das Programm weiterhin korrekt funktionieren.

Das bedingt unter anderen:

Alle Eigenschaften der Oberklasse müssen in der Unterklasse vorhanden sein.

Die Unterklasse darf zusätzliche Eigenschaften definieren, die sie spezieller machen.

I - Interface segregation principle (Schnittstellenaufteilungsprinzip)

Bei diesem Prinzip geht es um die Aufteilung von großen Schnittstellen (Interfaces) in kleinere spezifischere Schnittstellen, damit eine Klasse nur die für sie nötigen Methoden kennen muss und nicht alle Methoden, die eine Klasse implementiert.

Dieses Prinzip fördert lose Kopplung, da wir nicht von Details abhängen, sondern nur von den jeweils bekannten Methoden. Das vereinfacht auch eine einfachere Wartbarkeit.

Die einzelnen Schnittstellen sollten dabei eine hohe Kohäsion haben, d.h. es sollten nur Methoden enthalten sein, die eng zusammengehören.

D - Dependency inversion principle

Bei dem dependency inversion principle handelt es sich um eines, das sich mit der Abhängigkeit von Modulen in Software beschäftigt.

Generell lässt sich das Prinzip folgendermaßen beschreiben:

  • Module höherer Ebenen sollten nicht von Modulen niedrigerer Ebenen abhängen
  • Beide Module sollten von Abstraktionen abhängen
  • Abstraktionen sollten nicht von Details abhängen
  • Details sollten von Abstraktionen abhängen

Generell werden Entwürfe in Module strukturiert, die dort dann unterschiedliche Verantwortungen besitzen und umsetzen. Gängig ist es, Module in Ebenen so anzuordnen, dass Module in niedrigeren Ebenen, speziellere Vorgänge definiert. In niedrig liegenden Modulen werden also Abläufe definiert, die von allgemeineren Abläufen auf höher liegenden Modulen benutzt werden.

Wird diese Anordnung falsch umgesetzt, kann es sein, dass Änderungen in niedrigeren Ebenen Änderungen in höheren Ebenen mit sich führen.

Das widerspricht allerdings nicht nur dem eigentlichen Ansatz der Hierarchie, sondern führt auch zu zyklischen Abhängigkeiten. Dadurch wird die Kopplung der Module unnötig erhöht und damit Änderungen der Architektur und dem Design der Software unnötig verkompliziert.

Die Lösung dafür ist dann folgende:

Die Module der höheren Ebene definieren Schnittstellen, mit der diese dann arbeiten. Die Module auf niedrigeren Ebenen realisieren dann lediglich die hierarchisch gegebene Schnittstelle.

Mehr zu diesem Prinzip kann hier nachgelesen werden.