7.8 SOLIDe Modellierung
Wer gute objektorientierte Software schreiben möchte, sollte sich an einige Designprinzipien halten. Es sind Best-Practice-Methoden, die natürlich nicht zwingend sind, aber in der Regel das Design verbessern.
7.8.1 DRY, KISS und YAGNI
Die ersten drei Regeln sind:
DRY (Don’t Repeat Yourself): »Wiederhole dich nicht.« Codeduplizierung sollte vermieden und doppelter Code in Methoden ausgelagert werden. Das heißt auch, dass bestehender Code (aus etwa eigenen Bibliotheken, der Java SE oder quelloffenen Bibliotheken) verwendet werden soll.
KISS (Keep It Simple, Stupid): »Halte es einfach und idiotensicher«. Ein Problem soll einfach und leicht verständlich gelöst werden. Für Entwickler bedeutet dies: einfacher Code, wenige Zeilen Code, auf den ersten Blick verständlich.
YAGNI (You Ain’t Gonna Need It): Das Prinzip »Du wirst es nicht brauchen« soll uns daran erinnern, einfachen Code zu schreiben und nur das zu programmieren, was im Moment in der Anforderung auch erwartet wird. YAGNI ist ein zentraler Punkt im Extreme Programming (XP) und der Idee »Implementiere immer die einfachste mögliche Lösung, die funktioniert«, denn wenn etwas programmiert wird, was später nie produktiv wird, ist es Zeit- und Geldverschwendung, aber der Code muss dennoch dokumentiert, gewartet und getestet werden.
7.8.2 SOLID
Michael Feathers hat die Abkürzung SOLID eingeführt und fünf Punkte benannt, die einen guten objektorientierten Entwurf ausmachen. Die einzelnen Kriterien selbst stammen von unterschiedlichen Autoren.
S: Single Responsibility Principle (SRP)
Etwas flapsig ausgedrückt steht das Prinzip für: »Mache genau eine Sache, die aber richtig.« Ein Typ sollte genau eine Verantwortung (engl. responsibility) haben, sodass bei Änderungen im besten Fall auch nur eine Stelle angepasst werden muss und nicht viele Stellen. Das Gegenteil sind sogenannte Gott-Klassen, die alles können – ein Anti-Pattern. Robert Martin, der das SRP in seinem Buch »Agile Software Development: Principles, Patterns, and Practices« beschreibt, sagt auch: »Es sollte nie mehr als einen Grund geben, eine Klasse zu ändern.« Was heißt das nun praktisch?
Nehmen wir an, eine Person-Klasse speichert Namen, PLZ und Alter. An PLZ und Alter gibt es Anforderungen: Eine deutsche PLZ besteht nur aus Ziffern, ist 5 Stellen lang, und ein Alter ist sicherlich nicht negativ und nach oben beschränkt. Allerdings sind diese beiden Validierungen zwei unterschiedliche Dinge, also übernimmt die Person-Klasse Verantwortlichkeiten, die an sich mit einer Person nichts zu tun haben. Demnach gibt es zwei Gründe, warum die Klasse bei einer Änderung der Validierung angepasst werden muss; und zwei Gründe sind mehr als ein Grund und folglich ein Bruch des SRP.
Treibt die Modellierung das SRP ins Extrem, entstehen sehr viele kleine Typen. Damit ist auch dem Codeversteher nicht geholfen, wenn Verantwortlichkeiten wegen Unübersichtlichkeit nicht mehr zu verstehen sind.
O: Open/closed principle
Bertrand Meyer formuliert 1988 in seinem Buch »Object-Oriented Software Construction«, dass Module sowohl offen (für Erweiterungen) als auch verschlossen (für Modifikationen) sein müssen. Unter dem Begriff Modul müssen sich Java-Entwickler einen Typ vorstellen. Eine herkömmliche Klasse ist insbesondere mit privaten Zuständen geschlossen für Modifikationen, aber eine Unterklasse erlaubt ohne Codeänderungen der Oberklasse die Erweiterung um neue Zustände oder durch das Überschreiben von Methoden eine Anpassung einer Implementierung. Eine Unterklasse darf Methoden allerdings keine andere Semantik geben, sonst würde das die Geschlossenheit brechen.
L: Liskov Substitution Principle (LSP)
Barbara Liskov hielt 1987 den Vortrag »Data abstraction and hierarchy«, in dem es um die Tatsache ging, dass es möglich sein sollte, Objekte in Programmen durch Objekte eines Untertyps zu ersetzen, ohne dass die Korrektheit leidet. Damit der Austausch funktioniert, muss natürlich der Untertyp wissen, was »korrekt« ist, damit Methoden nicht eine falsche Implementierung realisieren, die das Verhalten brechen. Flapsig ausgedrückt: Kinder müssen das Verhalten der Eltern erben und respektieren. In Java ist das nicht einfach, denn syntaktische Konstrukte wie Preconditions, Postconditions und Invarianten gibt es nicht; Java-Entwickler müssen also rein aus dem Javadoc, der textuellen Information also, herausziehen, was ein korrektes Verhalten ist.
I: Interface Segregation Principle (ISP)
Das ISP wird Robert Cecil Martin zugeschrieben, als er für Xerox am Druckersystem arbeitete. Die zentrale Aussage ist: »Viele Client-spezifische Schnittstellen sind besser als eine allgemeine Schnittstelle.« Der Client ist der Nutzer eines Java-Typs, und mit Schnittstelle ist verallgemeinert das Angebot an Methoden gemeint. Praktisch heißt das Folgendes: Es gibt Typen, die sehr viele Methoden haben und dann ein »allgemeiner« Typ wären. Werden solche Objekte herumgereicht, dann bekommen die Programmstellen immer das gesamte Objekt mit allen Methoden. Das komplette Angebot an Methoden ist aber nicht immer nötig und vielleicht sogar gefährlich. Besser ist es, die API klein zu halten und damit nur den verschiedenen Stellen das zu ermöglichen, was auch benötigt wird.
D: Dependency Inversion Principle (DIP)
»Hänge nur von Abstraktionen ab, nicht von Spezialisierungen.« So hat es Robert Cecil Martin formuliert, ursprünglich etwas länger.[ 178 ](Die erste Fassung lautet: »A. High-level modules should not depend on low-level modules. Both should depend on abstractions. B. Abstractions should not depend on details. Details should depend on abstractions.« ) Gut zu erkennen ist das Prinzip in der Schichtenarchitektur: Eine obere Schicht greift auf Dienste einer tieferen Schicht zurück. Die obere Schicht sollte sich aber nicht an konkrete Typen klammern, sondern nur von Basistypen wie Java-Schnittstellen abhängen. In diesem Zusammenhang passt gut das Prinzip Programmieren gegen Schnittstellen.
Das große Ganze
Wie in einem Quentin-Tarantino-Film hängt alles irgendwie in einem großen Designuniversum zusammen. Jedoch haben die Entwurfspraktiken Schwerpunkte: Das SRP nimmt sich Typen vor und die Architektur im Großen. Das Open-Closed-Prinzip handelt von Typen und ihren Erweiterungen. LSP handelt von Vererbung und Untertypen. Und das ISP handelt von Geschäftslogik und Abhängigkeiten der Typen.
7.8.3 Sei nicht STUPID
Jedem »tue« in SOLID steht ein »lass es« in STUPID gegenüber. Das Akronym steht für die die dunkle Seite:
Singleton: Ein Singleton ist ein Objekt, das es im System nur einmal geben kann. Solche Objekte gibt es immer wieder, und sie sind an sich nichts Schlimmes. Problematisch ist jedoch, dass viele Entwickler das Singleton selbst als Klasse schreiben, und dann entsteht schnell eine Implementierung, die sich durch den globalen Zustand schlecht testen lässt. Besser ist es, Frameworks zu nutzen, die für uns dann ein Exemplar bereitstellen.
Tight Coupling (enge Kopplung): Das Ziel guten Entwurfs ist die Reduktion von Abhängigkeiten; auf je weniger Module/Pakete/Typen ein Stück Code zurückgreift, desto besser. Konkret: Je weniger import-Deklarationen es gibt, umso besser.
Untestability (Nicht-Testbarkeit): Wird erst nach dem Design und der Programmierung über das Testen nachgedacht, ist es oft schon zu spät – schnell entsteht schwer zu testender Code, besonders wenn die Kopplung zu eng ist. Besser ist der Ansatz der testgetriebenen Entwicklung, bei der die Testbarkeit das Design beeinflusst. Am besten überlegen sich Designer und Entwickler im Vorfeld, wie eine bestimmte Klasse und Funktionalität getestet werden kann, bevor es an die intensive Implementierung geht.
Premature Optimization (voreilige Optimierung): Entwickler meinen, ein Gefühl dafür zu haben, welche Programmteile Performance verschlingen und welche Teile schnell sind. Oft irren sie sich, verschenken aber viel Zeit bei der Optimierung dieser vermeindlich langsamen Stellen. Der beste Ansatz ist, nach KISS eine einfache Lösung zu realisieren und dann über einen Profiler sich genau die Stellen aufzeigen zu lassen, die Nacharbeit erfordern.
Indescriptive Naming (nichtbeschreibende Benennung): Variablennamen wie one, z, l, myvariable, var1, val10, theInt, aDouble, _1bool, button123 sind wenig sprechend und müssen vermieden werden. Der Programmleser sollte sofort wissen, worum es sich bei der Variablen handelt.
Duplikationen: Code, der mit kleinen Änderungen 1:1 kopiert wurde, ist zu vermeiden. Codeduplikate lassen sich mit Werkzeugen und IDE-Plugins relativ gut finden.