Prinzipien bei der Entwicklung von sauberem Code

Clean Code
Kommentare

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 anwachsen 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.

Die Clean-Code-Prinzipien beschreiben gute Strukturen tendenziell auf Quellcode-Ebene, während Architektur eher größere Strukturen im Blick hat. Das gemeinsame Ziel ist es, kontinuierlich für lose Kopplung und verstehbare 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 Buchs 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 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 Mehrdeutigkeiten 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. Boole’sche 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.

[ header = Seite 2: Kommentare sind überflüssig ]

Kommentare sind überflüssig

Kommentare sind beinahe 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 selbstverständlich 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).

Langatmige Kommentare Die bei vielen Entwicklern ebenfalls beliebten „Header“, also langatmige Kommentare am Beginn einer Datei, die erklären, was diese Klasse ist, worum es geht etc. sind ebenso überflüssig wie die Änderungshistorie als Kommentar am Beginn – sie ist deutlich informativer und besser in der Versionsverwaltung aufgehoben und heute auch nicht einmal mehr für Open-Source-Entwickler angebracht.

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 als 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 Headerdateien 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).

Entwickler bei der Arbeit Als Entwicklercoach sehe ich vielen Entwicklern bei der Arbeit zu und entdecke manchmal mit Entsetzen, wie häufig die Tastenkombination Ctrl-C und Ctrl-V zum Einsatz kommt. Ich rate dann schon auch mal dazu, die Key-Bindings in der IDE für Copy und Paste für einige Zeit zu entfernen, sodass man zur Maus greifen muss, um zu kopieren. Der Effekt ist absehbar, man dupliziert augenblicklich weniger Code, denn wenn man kopieren muss, dann muss es ein wenig „weh“ tun. So gewöhnt man sich das Kopieren schneller ab.

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 Manifest 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 vornherein aufgebläht und generisch entwickelt. Am Ende stellt sich dann 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 Munde 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 Test-driven-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].

[ header = Seite 3: Grundprinzip: Tell, dont ask (TDA) ]

Grundprinzip: Tell, dont 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: „Procedural 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 Kopplung 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 m einer Klasse C nur folgende Methoden aufrufen darf:

  • Methoden der eigenen Klasse (also von C selbst)
  • Methoden von in 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 dieses Gesetzes auf. Wenn man sich daran hält und den Code entsprechend oft refaktorisiert, dann wird die Kopplung 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 Objekts 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 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 (oder auch einer Funktion) 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 (bzw. sie ist nicht mehr sichtbar).

SOLID-Prinzip: Open Closed Principle (OCP)

Das zweite Prinzip besagt, dass Softwaremodule (Klassen, Funktionen etc.) offen für Erweiterung, jedoch geschlossen für Modifikation 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äuchlicheres Beispiel für das Open-Closed-Prinzip ist eine Schnittstelle, die die Java-Klassenbibliothek für Listen sortierter Objekte verwendet. Eine sortierte Liste 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.

[ header = Seite 5: SOLID-Prinzip: Liskov Substitution Principle (LSP) ]

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 gesprochen haben, trifft Annahmen ü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 Buches 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 manchmal 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 zusammen gehören. In der Regel ist es dann 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 unnötig hohe Kopplung zwischen den Klienten, also den Klassen, die diese verwenden. 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 persistent gemacht werden muss oder eine weiter abgeleitete 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 (zum Beispiel die Persistenz), jedoch nicht Teil der Schnittstelle ist. Wir lagern also den oben als „verschmutzend“ beschriebenen Teil der Schnittstelle in eine andere Klasse aus, die wir zur Laufzeit erzeugen und in unserer 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 ja klar definierte Schichten mit kohärenten Diensten anbieten. Sehr häufig kommt es dann dazu, dass wir Schichten mit unterschiedlich konkreten oder an der Geschäftslogik fokussierten Funktionen haben, bei denen die Klassen von höherwertigen Schichten von Klassen aus den unteren Schichten abhängig sind (Abb. 1).

Abb. 1: Schichten in einer Architektur, nicht konform zu DIP

Auf den ersten Blick mag das angemessen wirken, bei genauerer Betrachtung sieht man, dass eine Klasse in der Businesslogik-Schicht anfällig für jegliche Änderungen ist, die entlang des Weges bis runter zur Schicht der „allgemeinen Dienste“ (Utility) passieren, was klarerweise nicht gut ist, da damit wieder einmal eine hohe Kopplung zwischen den Klassen dieser Schichten geschaffen wird.

Wir können dieses Problem lösen, indem wir die Abhängigkeit 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. Interessant ist, dass damit auch der „Besitz“ der Schnittstelle von den Klienten der Schnittstelle zum Server, also in die nächst höhere Schicht wandert.

Das Hollywood-Prinzip Vermutlich wurden Schauspieler von Filmagenten angewiesen, sie nicht anzurufen, sondern umgekehrt – sofern ein Bedarf besteht, von den Agenten angerufen zu werden: „Don’t call us, we call you.“

Fazit

Im ersten Schritt zum „Clean Coder“ geht es zunächst 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 Grundregeln für Bezeichner, Funktionen etc. führt zu bereits deutlich besserem Code. Arbeitet am Produktivcode und refaktorisiert diesen, sobald ihr Unit Tests hierfür habt und macht den bestehenden Code sauberer. Dazu dienen Code-Reviews in der Gruppe oder im Pair.

Unsere Redaktion empfiehlt:

Relevante Beiträge

Meinungen zu diesem Beitrag

X
- Gib Deinen Standort ein -
- or -