Clean Code
Prinzipien bei der
Entwicklung von sauberem Code
Entwickler wissen, wie es
sich anfühlt, wenn man sich durch den unordentlichen Code arbeiten muss, der
sich im Lauf der Zeit angesammelt hat. Man wird gebremst, allmählich sinkt
dadurch die Produktivität immer mehr. Wenn diese „Unordnung im Code“ einmal
angefangen hat, wird sie oft nachwachsen und durch eine wachsende Unlust der
Entwickler zum „Aufräumen“ verstärkt. Der Begriff „Unordnung“ ist hier
vermutlich noch harmlos, in schweren Fällen entsteht eine unüberschaubare
Codewüste, die nicht mehr wartbar ist.
Von Andreas Wintersteiger
und Christoph Mathis
Die Clean-Code-Prinzipien
beschreiben gute Strukturen tendenziell auf Quellcodeebene, während Architektur
eher größere Strukturen im Blick hat. Das gemeinsame Ziel ist es, kontinuierlich für lose Kopplung und verständliche Strukturen zu
sorgen bzw. sie wieder herzustellen. Clean Code ist aber mehr als nur eine
Sammlung von Entwurfsmustern – es beschreibt eine Haltung und eine eigene
Bewegung. Dave Thomas und Andy Hunt [1] nennen sie das „Broken Window Syndrome“
im Zusammenhang mit verrottender Software. In ihrem Buch geben sie Entwicklern
den guten Rat:„Don’t live with broken windows!“ – übersetzt in unsere Domäne
also nicht mit schlechten Designs, falschen Entscheidungen, oder miesem Code zu
leben, sondern dagegen vorzugehen. Dave Thomas und Andy Hunt haben mit ihrem
oben genannten Buch eine Bewegung in Gang gesetzt, die Prinzipien und Praktiken
für Entwickler festhält, um sauberen Code zu schreiben [1]. Sie benennen die
Entwickler als die Träger dieser Codequalität. Freeman Dyson trieb das weiter
und bezeichnet sie als Erben der traditionellen europäischen Handwerkskunst [2]
Robert Martin hat mit seinem
Buch „Clean Code“ einen gewissen Standard geschaffen und beschreibt viele
Hinweise zur Strukturierung des Code. Er entwickelt und fördert diese Gedanken
in der „Software-Craftsmanship“ -Bewegung [3]. Robert Martin widmet gut ein
Viertel seines Buches der Darstellung elementarer Ideen zu sauberem Code.
Dieser Aspekt scheint auf den ersten Blick trivial, mit einfachen Mitteln
lassen sich hier aber schnell große Fortschritte erzielen.
Elementare Sauberkeit durch sinnvolle Bezeichner
Der schnelle Gewinn lässt
sich mit einer durchdachten Auswahl an Bezeichnern im Code machen. Entwickler
lesen Code zehnmal öfter als sie den Code schreiben. Es macht daher Sinn,
gründlich darüber nachzudenken, wie man eine Methode, eine Klasse, ja sogar
eine Variable benennt. Die Zeit, die ein anderer Entwickler beim Lesen und
damit dem Versuch, den Code zu verstehen, verbringt, multipliziert sich. Es ist
klar, dass gut leserlicher und verständlicher Code deutlich schneller
verstanden werden kann als schlechter Code. Entwickler sollten daher Bezeichner
verwenden, die die Absichten hinter einer Methode oder Variable erklären und
somit viele Fragen zum Code vorwegnehmen. Die gute alte Regel, Hauptwörter für
Klassen und Verben für Methoden zu verwenden, hat nach wie vor Gültigkeit.
Bezeichner sollten auch keine Fehlinformation oder Mehrdeutigkeit enthalten.
Ein Relikt aus alten Zeiten sind auch Präfixe oder Ergänzungen von Namen mit
redundanter Information, wie zum Beispiel die Typinformation als Teil des Namens,
oder dass es sich um ein Feld einer Klasse handelt. Manch bekanntes Framework
und viele Bibliotheken der 90er Jahre prolongierten diesen Code Smell sogar.
Hat man sich einmal für
einen Namen für ein Konzept entschieden, sollte man ihn beibehalten. Schlechter
Code verwendet an verschiedenen Stellen andere Bezeichnungen für dasselbe
Konzept – zum Löschen von etwas, wie zum Beispiel clear, delete, destroy oder
Ähnliches. Unklare Namen sind auch solche, die der Leser zunächst „übersetzen“
muss (in seinen Sprachgebrauch oder eine andere Domäne) oder die zu humorvoll
gewählt wurden und auf den ersten Blick unverständlich sind.
Elementare Sauberkeit durch verständliche Funktionen
Häufig hört man die Regel
von 4-6 Zeilen Code als gute Länge für eine Methode oder Funktion. Wir denken,
es sollte hier nicht dogmatisch eine Anzahl von Zeilen als Regel genannt
werden, sondern der Grund – und der ist Verständlichkeit. Ich möchte in der
Lage sein, die Methode gänzlich verstanden zu haben, wenn ich sie von oben nach
unten in nur einer Sequenz durchlese. Dazu reicht es die Abstraktionsebenen zu
verstehen, also die weiteren Funktionen, die hier aufgerufen werden. Wichtig
hierfür ist aus unserer Sicht, in einer Funktion nur gleiche Abstraktionsebenen
zu haben. Bertrand Meyer beschreibt in seiner Arbeit über Eiffel [4] ein
Prinzip für Funktionen, das wir heute unter „Command-Query-Separation“ kennen.
Dieses Prinzip besagt, dass eine Funktion entweder eine Aktion durchführt
(Command) oder Werte abfragt, also Daten an den Aufrufer zurückgibt, und
niemals beides in einer Funktion tun sollte.
Lange Parameterlisten sind
ebenfalls ein Code Smell, der in vielen Büchern umfassend beschrieben wird.
Clean Code geht hier einen Schritt weiter und besagt, dass wir zunächst
versuchen sollten, „niladische“ Funktionen, also solche ohne Parameter, zu
schreiben, gefolgt von monadischen und dyadischen (mit ein oder zwei
Parametern) und es zum Zwecke der Verständlichkeit vermeiden sollten, mehr als
zwei Parameter in einer Funktion zu verwenden. Ausgabeparameter sind aus
demselben Grund verpönt. Boolesche Parameter sind beinahe immer ein schneller
Indikator dafür, dass eine Methode mehr als eine Sache macht.
Die Verwendung von
Exception-Handling-Mechanismen in modernen Sprachen ist gegenüber Fehlercodes
oder kodierten Rückgabewerten immer zu bevorzugen. Die Verständlichkeit des
Codes wird dadurch deutlich erhöht.
Funktionen sollten das tun,
was wir von ihnen erwarten, wenn wir ihren Namen lesen oder tun, was wir von
Funktionen oder Methoden desselben Musters (Pattern) gewöhnt sind. Darunter
fallen zum Beispiel implizite Konventionen über Namen oder Ähnliches. Das
Prinzip der geringsten Überraschung („principle of least surprise“) besagt,
dass wir den Code deutlich schneller verstehen können, wenn wir uns darauf
verlassen können, dass wir nicht überrascht werden, was eine Funktion noch so
alles tut. Wenn wir zum Beispiel eine Funktion getAddressDetails aufrufen, erwarten wir, dass sie eben genau das
liefert, nicht mehr und nicht weniger. Wir erwarten dabei auch keine
Seiteneffekte. Sind wir hingegen erstaunt, was eine Funktion noch so alles
macht, während wir ihren Code studieren, wird uns das deutlich mehr Zeit
kosten, um alle Details herauszufinden.
Kommentare sind überflüssig
Kommentare sind fast immer
überflüssig. Ein Kommentar ist ein deutliches Anzeichen dafür, dass es uns
nicht gelungen ist, lesbaren und verständlichen Code zu schreiben. Sie sind
daher redundant und meistens veraltet (Robert Martin nennt sie sogar „Lügen“ [3]).
Aus diesem Grund sind sie überflüssig.
Kommentare sind ein Hinweis,
genau den Code zu refaktorisieren, wo der Kommentar steht. Es reicht meistens
aus, eine Methode oder Variablen umzubenennen. Andere Kommentare sind
Markierungen, dass ab hier „etwas anderes losgeht“, die Methode also mehrere
Dinge tut oder zu lang ist. Wiederum andere Kommentare versuchen sich als Entschuldigung
für Faulheit oder Zeitmangel, die Funktion ordentlich zu entwickeln, oder auch
fertig zu machen. Ein darunter fallendes Antipattern ist der „TODO“ -Kommentar.
Es gibt dennoch einige
wenige Ausnahmen, wo Kommentare angebracht sind. Darunter fallen die
rechtlichen Hinweise und Copyright-Vermerke. In wenigen Fällen ist es
notwendig, die Absichten hinter einer Funktion in einem Kommentar zu klären
oder vor Konsequenzen zu warnen sowie Verweise auf andere Quellen zu geben.
Dokumentationselemente wie JavaDoc für (und nur für) öffentliche APIs sind als
Kommentar ebenfalls zulässig, obwohl sie die Lesbarkeit massiv stören (Kasten:
„Langatmige Kommentare“)
Grundprinzip: Don’t Repeat
Yourself (DRY)
Das DRY-Prinzip besagt, dass
jedes Stück an Wissen oder Information eine singuläre, unmissverständliche und
originäre Repräsentation an einem einzigen Ort im System hat. Was zunächst sehr
einfach klingt, ist in der Praxis eine schwierige Aufgabe, denn es betrifft
meistens sehr kleine Häppchen an Informationen, die davon stark betroffen sind.
Immer wieder wird Information an mehreren Stellen im Code ausgedrückt, mit der
Folge, dass eine Änderung dieser Information an vielen Stellen geändert werden
muss. Neben mehrfacher Wissensrepräsentation zählen hierzu auch Kommentare,
Dokumentation und sogar von Programmiersprachen erzwungene Wiederholungen, wie
zum Beispiel die Header-Dateien in C. In vielen Fällen ist auch Faulheit die
Ursache von Duplikationen, wenn Entwickler schnell mal ein Stück Code kopieren
und es dann den lokalen Gegebenheiten anpassen – die hier gefragte Technik wäre
das Refaktorisieren (Kasten: „Entwickler bei der Arbeit“).
In den meisten Fällen jedoch
wird Duplikation unabsichtlich gemacht, es fällt gar nicht auf. Kann sein, dass
Copy/Paste schon so in Fleisch und Blut übergegangen ist und man unterbewusst
Code dupliziert, es kann aber auch sein, dass man vergisst, dass man gewisse
Informationen schon mal anderswo kodiert hat.
Auch beim eigenen Entwurf
von Softwaresystemen spielt DRY eine wichtige Rolle. Man entwickelt ja schnell
auch mal eine Bibliothek, ein System oder auch eine domänenspezifische Sprache
zur Lösung eines spezifischen Problems und sollte nicht vergessen, darauf zu
achten, dass man damit die eigenen Anwender nicht zur Duplikation von
Informationen zwingt. In XML kodierte Konfigurationen, die sich in Teilen im
Code widerspiegeln, sind zum Beispiel typische Fallen. Aber auch auf der Ebene
der Benutzerschnittstelle passiert es immer wieder, dass ein und dieselbe
Information mehrmals auftaucht. Zu den üblichen Verdächtigen zählt hier die Validierungslogik,
die sich auch mal auf allen drei Ebenen, im User Interface, in der
Businesslogik und in der Persistenzschicht wiederfindet.
Dave Thomas und Andy Hunt schreiben in ihrem „Pragmatic Programmer“[1]:
„It isn’t a question whether you’ll remember, it is only a question when you
forget!“. Codeduplikation ist also
vielmehr erst dann eine Frage, wenn man vergisst sie wegzuräumen.
Grundprinzip: Keep it Simple,
Stupid! (KISS)
Das zehnte Prinzip des agilen Manifests lautet: „Simplicity – the art of
maximizing the amount of work not done – is essential.“ Auf den ersten Blick wird die „Maximierung von nicht
getaner Arbeit“ vielleicht als „Faulheit“ missverstanden. Bei genauerer
Betrachtung ist vielen Entwicklern klar, dass es darum geht, die einfachste
Lösung zu entwickeln und nicht mehr. Das Antipattern dazu ist „Goldplating“
also das Vergolden von Features: Man schreibt das Stück Funktionalität zu generisch
oder auch mit viel zu viel Extras rundherum, die keiner braucht. Wie schon bei
TDD erwähnt, sagt uns dieses Prinzip das gleiche auf einer höheren Ebene: Es
sollte die zunächst einfachste Lösung umgesetzt werden, die funktioniert – und
dann auf der Basis von Feedback, neuen Erkenntnissen und neuen Anforderungen
darauf aufbauend weiter gearbeitet werden.
Systeme werden unter
Missachtung des KISS-Prinzips („Halte es einfach, Dummkopf!“ und nicht „Halte
es einfach und dumm!“) von vornhinein aufgebläht und generisch entwickelt. Am
Ende stellt sich heraus, dass die Dinge, die man versucht hat zu antizipieren
entweder gar nicht benötigt werden und wenn, dann in einer deutlich anderen Art
und Weise als ursprünglich angenommen. Die essenzielle Frage, die sich Teams
immer wieder stellen müssen, ist: „Mit wie wenig kann ich davonkommen, um alle
bekannten und tatsächlich geforderten Anforderungen umzusetzen?“ Die zweite
Frage, die sich ein Team immer wieder stellt, ist die, ob sie sich bei einer
Entscheidung auf Tatsachen oder Annahmen stützen. Oft freue ich mich dann, wenn
ich dann aus ihrem Mund höre „Keep it simple, stupid!“
Eine noch stärkere
Konsequenz erschließt sich mit dem Kürzel „YAGNI – you ain’t gonna need it“.
Mit der gleichen Intensität, mit der wir hinterfragen, ob wir die einfachste
Lösung gewählt haben, sollten wir auch fragen, ob wir ein gegebenes
Softwarefeature überhaupt brauchen.
Grundprinzip: Verfrühte Optimierung vermeiden
Donald Erwin Knuth, einer
der Pioniere in der algorithmischen Informatik, hat (in Anerkennung einer früheren
Aussage von CAR Hoare ) einen wichtigen Satz geprägt: „Premature Optimization
Is the root of all evil!“ [5]. Unzählige Fehler und „Verbrechen“ am Code sind
im Namen der Optimierung begangen worden. Die Vorsicht vor Optimierungen ist
mehr als angebracht, denn in der Regel haben wir Entwickler es ja schon schwer,
es beim ersten Mal richtig zu machen -
was einer der Gründe für Testdriven Development und Refaktorisierung
ist. Wie sollen wir es dann beim ersten Mal optimiert hinbekommen?
Ein weiteres Standardwerk
der Informatik, das nicht zuletzt aufgrund seines Alters vermutlich von
hunderttausenden von Entwicklern gelesen wurde, griff das Thema ebenfalls auf.
Brian Kernighan und Dennis Ritchie gaben Entwicklern in ihrem Buch „The C Programming
Language“ bereits 1977 den Rat, eine Funktion zunächst mal zum Laufen zu
bringen und sie dann erst zu optimieren [6].
Grundprinzip: Tell, Don’t Ask (TDA)
In ihrem Artikel „Tell,
don’t ask“ beschreiben die „Pragmatic Programmers“ [7] Dave Thomas und Andy
Hunt ein Prinzip, das zu besserem Code führt: „Procedual code gets information
then makes decisions. Object-oriented code tells objects to do things,“ Objekte
sollten also etwas tun und nicht nach ihrem (internen) Zustand gefragt werden.
Ein Aufrufer interessiert sich für die Details eines Objektes – und die Logik
und die Entscheidungen, die dieser Aufrufer außerhalb des Objekts
implementiert, sind vermutlich eher in der Verantwortung des Objekts, dem das
Interesse gilt. Das hier beschriebene Aufbrechen der Kapselung, erhöht die
Koppelung zwischen Klassen. Bei TDA geht es jedoch darum, dass Objekte so
entworfen werden, dass sie sich um ihre eigenen Verantwortungen kümmern, also
„etwas tun sollen“ und nicht bloß Auskunft über ihre Variablen erteilen. Anders
ausgedrückt sollten Objekte so wenig wie möglich ihres Zustands nach außen
exponieren (nur wenige „Getter“-Methoden haben).
Grundprinzip: Gesetz von Demeter
Es gibt eine unter dem
Gesetz von Demeter bekannte Heuristik, die besagt, dass ein Modul nichts über
die inneren Gegebenheiten der Objekte wissen soll, die es manipuliert, oder
etwas einfacher ausgedrückt, eine Methode sollte nur mit „Freunden“ und nicht
mit „Fremden“ sprechen. Ein Kundenobjekt, das zum Beispiel seine sortierte
Liste von Aufträgen über einen Getter exponiert, erlaubt es, diese Liste über
CustomerObject.getContracts().add(…)
zu manipulieren. Das ist
eine Verletzung dieses Gesetzes und erhöht die Kopplung. Die richtige Lösung
wäre es, die zum Hinzufügen von Elementen nötige Funktion als Methode in der
Kundenklasse anzubieten. Konkret besagt das Gesetz von Demeter, dass eine
Methode in einer Klasse C nur folgende Methoden aufrufen darf:
- Methoden der eigenen Klasse (also von C selbst)
- Methoden von m selbst erzeugten Objekten
- Methoden von Objekten, die
als Parameter in m übergeben wurden
- Methoden von Objekten, die
als Instanzvariablen in C gehalten werden
Es sollte keine Methode von
Objekten aufgerufen werden, die von irgendeinem der erlaubten Aufrufe als
Rückgabewert zurückgegeben werden. In der Praxis bewährt sich dieses Gesetz zur
Vermeidung von hoher Kopplung. Im durchschnittlichen Code einer Klasse taucht
an vielen Stellen eine Verletzung des Gesetzes auf. Wenn man sich daran hält
und den Code entsprechend oft refaktoriesert, dann wird die Koppelung zwischen
Klassen deutlich sinken, was auch die Änderbarkeit des Codes deutlich erhöht.
Grundprinzip: Separation of Concerns
Ein weiteres Prinzip, das
uns dem architektonischen Ziel, lose gekoppelte Systeme zu haben, näher bringt,
ist es, die „Angelegenheiten“ oder „Belange“ von Objekten bzw. Klassen zu
trennen. Solche „Concerns“ sind Absichten oder Zwecke, die oft orthogonal
zueinander oder zum Hauptzweck, der Geschäftslogik eines Objektes stehen.
Belange, die oft mit der Hauptfunktion
einer Einheit vermischt werden, sind Logging, Tracing, Persistenz, Caching,
Transaktionsbehandlung etc. In der aspektorientierten Programmierung kennt man
sie auch als „Aspekte“. Klassen, die nur einen Belang verfolgen, weisen eine
deutlich höhere Kohäsion auf, sind also deutlich fokussierter und wirken
„zusammengehöriger“. Sie sind dadurch wartbarer. Zum anderen weisen solche
Klassen auch niedrigere Kopplung auf. Kohäsion und Kopplung sind in der Regel
gegenläufig.
SOLID-Prinzip: Single Responsibility
Principle (SRP)
Robert Martin hat in seinem
Buch “Agile Software Development: Principles, Patterns and Practices” [8] fünf
Prinzipien beschrieben, deren Einhaltung für deutlich besseren Code sorgt. Sie
sind für Clean Code zu einem wichtigen Wissensbestandteil von agilen
Entwicklern geworden.
Die Grundidee des
Single-Responsibility-Prinzips ist sehr einfach. Es besagt, dass eine Klasse
nur eine Verantwortung haben soll, sich also nur um eine Sache „kümmern“ soll.
Tut sie das nicht und trägt sie mehrere Verantwortungen, entsteht zwischen den
einzelnen Verantwortungen, oder besser gesagt, den zugehörigen Codeteilen, eine
hohe Kopplung. Wenn der Code der einen Verantwortung geändert wird, betrifft
das auch den Code der anderen Verantwortungen, sodass hier auch Änderungen
notwendig werden, oder es verletzt darin sogar Funktionalitäten.
Handelt es sich hier bei
einer Verantwortung um eine allgemeinere, häufig verwendete, so würde es
helfen, eine Verantwortung in eine Basisklasse zu verlegen und für die zweite
Verantwortung eine davon abgeleitete Klasse zu nehmen. Ein Beispiel hierfür ist
eine Klasse, die geometrische Figuren, wie Rechteck, Dreieck etc. zeichnen
(erste Verantwortung) und für sie auch mathematische Berechnungen, wie Fläche
oder Umfang, anstellen kann (zweite Verantwortung). Durch diese hohe Kopplung
entsteht ein zerbrechliches Design, das auf unterschiedlichste und unerwartete
Weise „zerfällt“, wenn Änderungen an der einen Verantwortung notwendig werden.
Man erkennt diese hohe Kopplung auch daran, dass es in der Regel mehrere Gründe
gibt, an solch einer Klasse Änderungen vorzunehmen.
Das
Single-Responsibility-Prinzip hilft uns, hohe Kopplung zu entdecken und diesen
Code Smell zu beheben, indem wir die Verantwortungen einer Klasse in mehrere aufteilen.
Der erste Weg führt uns zu „Extract Class“. Eine weitere Möglichkeit, diesen
Code Smell los zu werden, wäre es, eine Schnittstelle für jede Verantwortung
einzuführen („Extract Interface“), wenn es aus bestimmten Gründen nicht
gelingt, die Implementierung aufzutrennen. In diesem Fall ist die Kopplung in
der Implementierung zwar noch vorhanden, es hängen jedoch keine anderen Klassen
von dieser Implementierung ab.
SOLID-Prinzip: Open Closed Principle
(OCP)
Das zweite Prinzip besagt,
dass Softwaremodule (Klassen, Funktionen etc.) offen für Erweiterungen, jedoch
geschlossen für Modifikationen sein sollen. Offenheit bedeutet dabei, dass das
Verhalten eines Softwaremoduls erweitert werden kann. Der zweite Teil, die
Geschlossenheit, bedeutet, dass das Verhalten in abgeleiteten Klassen nicht mit
einer anderen Semantik belegt werden darf. Solche Abstraktionen sind
normalerweise Schnittstellen oder Vererbung. Java-EE-Programmierer kennen das
Problem – wenn ein Client eine bestimmte Funktionalität eines Servers benutzt,
muss er die Schnittstelle des Servers kennen. Es reicht aus, sie zu kennen,
eine Benutzungsrelation zwischen den beiden wäre zu viel „Intimität“ und eine
Verletzung des OCP bei der Serverklasse (der Implementierung der
Schnittstelle). Durch die Schnittstelle wird es möglich, die Serverklasse
auszutauschen, ohne die Clients zu beeinträchtigen.
Ein anderes, deutlich
gebräuchliches Beispiel für das Open-Closed-Prinzip ist eine Schnittstelle, die
die Java Klassenbibliothek für Listen benötigt Vergleichsoperatoren um für das
Sortieren festzustellen, ob ein Objekt kleiner gleich oder größer zu einem
anderen ist. Die Art und Weise, wie dies von der Java-Bibliothek gelöst wird,
ist es, eine Schnittstelle „Comparable“ einzuführen, die genau diesen Vergleichsoperator
definiert. Die Elemente die in so einer Liste Sortiert werden sollen, müssen sie
implementieren. Somit müssen in der Liste die Elemente, die darin sortiert
werden sollen, bzw. deren Typ, nicht bekannt sein. Dadurch ist die Kopplung
zwischen diesen Objekten bzw. Klassen deutlich geringer als würde die Liste
alle Typen „kennen“ müssen.
Auch wenn diese Beispiele
offensichtlich klingen, die allgemeine Anwendung dieses Prinzips auf alle
Klassen einer Anwendung ist schwer. Nur wenn man sich die Zeit nimmt und das
Design zum Beispiel im Code-Review im Hinblick auf diese Prinzipien untersucht,
eröffnen sich viele Möglichkeiten, den Code zu refaktorisieren. Oft fällt eine
Verletzung des OCP selbst während einer TDD-Sitzung nicht unmittelbar auf. Es
wird offensichtlich, dass es sinnvoll ist, den Code laufend explizit
hinsichtlich der Einhaltung der SOLID-Prinzipien zu untersuchen.
SOLID-Prinzip: Liskov
Substitution Principle (LSP)
Das nächste Prinzip schließt
nahtlos an. Barbara Liskov hat in einem Artikel [9] gefordert, dass Subklassen ihre
Superklassen immer ersetzen können. Was zunächst als Selbstverständlichkeit
klingt, ist ein oft missachtetes Prinzip: Wenn eine Methode m einer Klasse einen Typ A (zum Beispiel
als Parameter) erwartet, um ihre Verarbeitung zu machen, dann wird damit
erwartet, dass diese Methode m auch
auf Subtypen von A unverändert reagiert. Vielmehr noch sollte m nichts über die möglichen Subtypen
wissen und schon gar nicht in speziellen Fällen „zerbrechen“.
Dahinter steckt in Wahrheit
ein viel tieferes Problem: Abgeleitete Typen („Subtypen“) verändern das
Verhalten einer Klasse (Robert Martin nennt dies „IS-A is about behavior“ [8]
und eine Methode m, von der wir
soeben gesprichen haben, trifft über das Verhalten der von ihr verarbeiteten
Typen. Wird diese aber häufig gebrochen, dann sprechen wir von einer Verletzung
dieses Prinzips. Ein einfaches Beispiel ist die Berechnung der Summenfläche in
einer Liste von geometrischen Figuren. Die Methode m würde hierzu eine Funktion getArea
() über alle Elemente der Liste aufrufen und ggf. eine Ausnahme (Exception)
erwarten, wenn etwas schief läuft. Würde ein neuer Subtyp zum Beispiel nun eine
neue Exception werfen, so würde die Funktion fehlschlagen und das Prinzip
hiermit verletzt werden.
Es geht also um die
Annahmen, die ein Entwickler über die Verwendung seiner Klasse trifft, wenn er
sie entwirft – „Designentscheidungen“, die auch für die zukünftige Verwendung
einer Klasse oder Bibliothek wichtig sind. Da wir jedoch nicht zu viel
antizipieren wollen und können, macht es hier mehr Sinn, die
Designentscheidungen und damit verbundenen Rahmenbedingungen für deren
Verwendung zu dokumentieren. Es gibt eine Technik die solche Annahmen für
Clients einer klasse oder Methode explizit macht und damit das LSP im Grunde
erzwingt: „Design by Contract“ (DBC), eine von Bertrand Meyer [4] beschriebene
Technik, bei der jede Klasse den „Vertrag“ explizit in Form von Vor- und
Nachbedingungen für jede Methode festschreibt. In der Zeit vor Unit Tests
wurden dazu die „Assert“ -Funktionen einer objektorientierten
Programmiersprache verwendet. Wenige Sprachen, wie zum Beispiel das von
Bertrand Meyer selbst entwickelte „Eiffel“, hatten auch eingebaute
Unterstützung für Pre- und Post-Conditions in der Laufzeitumgebung. Diese Art
hatte sich, zumindest der Meinung der Autoren dieses Artikels nach, nicht
besonders durchgesetzt.
Unit Tests sind hingegen
eine hervorragende Art und Weise, die Verwendung und die Annahmen,
Einschränkungen sowie Vor- und Nachbedingungen einer Klasse klarzustellen. Die
korrekte Verwendung, im Zuge von TDD zum Beispiel, ist ein gleichwertiges
Instrument, um das Liskov-Prinzip zu erzwingen und die Designentscheidungen
mittels Test Fixtures zu dokumentieren.
SOLID-Prinzip: Interface
Segregation Principle (ISP)
Bei diesem Prinzip geht es
um zu “fette” Schnittstellen, die oft im Zuge von wachsenden Anwendungen
entstehen. Konkret ist es ein Problem, wenn Clients gezwungen werden, von
Methoden einer Schnittstelle abzuhängen, die sie nicht benötigen. Das erkennt
man dann, wenn die Schnittstelle so lang wird, dass sich „Gruppen von Methoden“
bilden, die offensichtlich zusammengehören. In der Regel ist es auch so, dass
einige Klienten die eine Gruppe von Methoden benutzt und andere Methodengruppen
von anderen, meist verschiedenen Klienten verwendet werden.
Diese fetten Schnittstellen
erzeugen eine hohe Kopplung zwischen den Klienten. Wenn ein Klient eine
Änderung bei der „fetten“ Klasse verursacht, sind in der Regel alle anderen
Klienten ebenso betroffen. Das Prinzip sagt somit auch aus, dass Klienten nur
von den Methoden abhängen sollen, die sie auch verwenden.
Diese fetten Klassen weisen
eine geringe Kohäsion auf. In vielen Fällen reicht es aus, dass die
Schnittstellen, und damit die Klassen, auf mehrere aufgeteilt werden. Manchmal
jedoch sind Objekte mit nichtkohäsiven Schnittstellen notwendig – eine saubere
Schnittstelle muss dann mit anderen notwendigen Methoden „verschmutzt“ werden,
zum Beispiel, wenn eine Klasse eine bestimmte abstrakte Basis einfordert. In
der klassischen objektorientierten Programmierung wurde hier dann häufig die
Mehrfachvererbung verwendet.
In der Regel kann
Mehrfachvererbung auch mittels „Delegation“ ersetzt werden, sodass Zweck und
Sauberkeit in gleichem Maße bedient werden können. Bei der Delegation handelt
es sich um ein weiteres Objekt, das die zum Zweck der abgeleiteten Klasse
notwendige Basis implementiert (z.B. die Persistenz), jedoch nicht Teil der
Schnittstelle ist. Wir lagern den oben als „verschmutzend“ bezeichnenden Teil der
Schnittstelle in eine andere Klasse aus, die wir zur Laufzeit erzeugen und in
der abgeleiteten Klasse zu diesem Zweck verwenden. Dieses Verwendungsmuster ist
auch als „Adapter“ bekannt [10].
SOLID-Prinzip: Dependency
Inversion Principle (DIP)
Um die Abhängigkeit von
Klassen untereinander geht es auch beim letzten der fünf SOLID-Prinzipien.
Objektorientierte Architekturen sollten klar definierte Schichten mit
kohärenten Diensten anbieten. Häufig kommt es dazu, dass wir Schichten mit
unterschiedlich konkreten oder an der Geschäftslogik fokussierten Funktion
haben, bei denen die Klassen von höherwertigen Schichten von Klassen aus den
unteren Schichten abhängig sind (Abb. 1).
Bei genauerer Betrachtung
sieht man, dass eine Klasse in der Bsuinesslogik-Schicht anfällig für jegliche
Änderungen ist, die entlang des Weges bis runter zur Schicht der „allgemeinen
Dienste“ (Utility) passieren, was natürlich nicht gut ist, da damit wieder
einmal eine hohe Kopplung zwischen den Klassen dieser Schichten geschaffen wird.
Wir können die Abhängigkeit
in diesem Fall umkehren („Dependency Inversion“): jede Abhängigkeit einer
Klasse von einer unterhalb angesiedelten Klasse wird durch eine Schnittstelle
ersetzt, die das erwartete Verhalten beschreibt. Die nächste Schicht implementiert
Klassen, die diese Schnittstellen implementieren und somit loslösen. Diese
Dependency Inversion wird manchmal auch als Hollywood-Prinzip (Kasten: „das
Hollywood-Prinzip“) bezeichnet. In Abbildung 2 ist die Inversion dieser
Abhängigkeiten dargestellt, bei der auch der „Besitz“ der Schnittstelle von den
Klienten der Schnittstelle in die nächst höhere Schicht zum Server wandert.
Fazit
Im ersten Schritt zum „Clean
Code“ geht es darum, die eigene Einstellung zu sauberem Code als
„Handwerkskunst“ zu manifestieren. In weiterer Folge ist es auch wichtig, dass
sich in Teams ein Regelwerk hierzu bildet. Die Einhaltung von Grundregelen für
Bezeichner, Funktionen etc. führt zu bereits deutlich besseren Code. Arbeitet
am Produktivcode und refaktorisiert diesen, sobald ihr Unit Tests hierfür habt
und macht den bestehenden Code sauberer durch Code-Reviews in der Gruppe oder
im Pair.