>>
Wissensdatenbank
/
Clean Code
Funktionen
- Klein
Funktionen sollten klein sein.
Funktionen sollten noch kleiner sein.
Die Einrückungstiefe einer Funktion sollte nicht größer als eine oder zwei Ebenen sein.
- Eine Aufgabe erfüllen
Funktionen sollten eine Aufgabe erledigen. Sie sollten sie gut erledigen. Sie sollten nur diese Aufgabe erledigen.
Wird der Code einer Funktion in Abschnitte unterteilt, ist dies offensichtlich ein Symptom dafür, dass sie mehr als
eine Aufgabe erfüllt.
- Eine Abstraktionsebene pro Funktion
Wenn eine Funktion nur eine Aufgabe erledigen soll, müssen sich die Anweisungen innerhalb der Funktion alle auf denselben
Abstraktionsebenen befinden.
- Switch-Anweisungen
Beispiel: In einer Funktion soll per Switch-Anweisung nach Mitarbeitertypen entschieden werden, welche Funktion zur
Gehaltsabrechnung aufgerufen wird.
Diese Funktion mit Switch-Anweisungen hat folgende Probleme:
- Die Funktion ist zu groß.
- Sie erfüllt mehrere Aufgaben.
- Verstoß gegen Single-Responsibility-Prinzip.
- Verstoß gegen das Open-Closed-Prinzip.
- Es kann möglicherweise weitere Funktionen mit derselben Struktur geben, da dieselbe Verzweigung an anderen
Stellen auch benötigt wird.
Die Lösung für dieses Problem:
Die switch-Anweisung wird in den Keller einer Abstract Factory verbannt. Die Factory erstellt anhand der switch-Anweisung
die entsprechenden Instanzen der abgeleiteten Klassen von Mitarbeiter. Die verschiedenen Funktionen wie calculatePay werden
polymorph von dem Mitarbeiter-Interface an die richtige Stelle dirigiert.
- Beschreibende Namen verwenden
Ward Cunningham: "Sie erkennen, dass Sie mit sauberem Code arbeiten, wenn jede Routine im Wesentlichen das tut, was
Sie erwartet haben."
Die halbe Schlacht im Kampf um die Realisierung dieses Prinzips besteht darin, gute Namen für kleine Funktionen zu finden,
die eine Aufgabe erledigen.
- Funktionsargumente
Die ideale Anzahl von Argumenten für eine Funktion ist null (niladisch). Als Nächstes kommt eins
(monadisch), dicht gefolgt von zwei (dyadisch). Drei Argumente (triadisch) sollten, wenn möglich,
vermieden werden. Mehr als drei (polyadisch) erfordert eine sehr spezielle Begründung - und sollte dann trotzdem
nicht benutzt werden.
Gebräuchliche monadische Formen
Es gibt zwei sehr verbreitete Gründe, ein einziges Argument an eine Funktion zu übergeben. Man kann eine
Frage über das Argument stellen, wie etwa in boolean fileExists("MyFile"). Oder man möchte das
Argument manipulieren, etwa indem man es in etwas umwandelt und dann zurückgibt. Beispielsweise wandelt
InputStream fileOpen("MyFile") einen Dateinamens-String in einen InputStream-Rückgabewert um.
Eine etwas weniger gebräuchliche, aber immer noch sehr nützliche Form einer Funktion mit einem einzigen Argument ist
ein Event (Ereignis).
Flag-Argumente
Ein boolesches Argument an eine Funktion zu übergeben, ist eine wirklich schreckliche Technik.
Dyadische Funktionen
Manchmal sind natürlich zwei Argumente die passende Lösung. Beispielsweise ist an Point p = new
Point(0,0); überhaupt nichts auszusetzen.
Man sollte jedoch jede Möglichkeit nutzen, dyadische Funktionen in Monaden umzuwandeln.
Triaden
Funktionen, die drei Argumente übernehmen, sind erheblich schwerer zu verstehen als Dyaden. Die Probleme, zum Beispiel der
Reihenfolge, des mehrmaligen Lesens und des Übersehens, werden mehr als verdoppelt.
Argument-Objekte
Wenn eine Funktion anscheinend mehr als zwei oder drei Argumente benötigt, ist es wahrscheinlich, dass einige dieser
Argumente in eine separate Klasse eingehüllt werden sollten.
Argument-Listen
Funktionen, die eine variable Anzahl von Argumenten übernehmen, können Monaden, Dyaden oder sogar Triaden sein. Aber es
wäre ein Fehler, ihnen noch mehr Agumente zu übergeben.
Verben und Schlüsselwörter
Ein guter Name für eine Funktion kann sehr viel dazu beitragen, den Zweck der Funktion und die Reihenfolge und den Zweck der
Argumente zu erklären. Bei einer Monade sollten Funktion und Argument ein aussagestarkes Verb/Substantiv-Paar bilden.
Beispielsweise erklärt sich write(name) von selbst.
- Nebeneffekte vermeiden
Nebeneffekte sind Lügen. Eine Funktion verspricht, eine Aufgabe zu erfüllen, aber sie erledigt auch andere
verborgene Aufgaben.
Output-Argumente
Argumente werden auf natürlichste Weise als Inputs einer Funktion interpretiert.
Im Allgemeinen sollte man keine Output-Argumente verwenden. Wenn eine Funktion den Status einer Komponente ändern
muss, sollte sie den Status des Eigentümerobjekts ändern.
- Anweisungen und Abfrage trennen
Funktionen sollen entweder etwas tun oder etwas antworten, aber nicht beides.
public boolean set(String attribute, String value);
...
if (set("username", "johndoe"))...
Die richtige Lösung besteht darin, die Anweisung von der Abfrage zu trennen:
if (attributeExists("username")) {
setAttribute("username", "johndoe");
...
}
- Ausnahmen sind besser als Fehler-Codes
Fehler-Codes von Befehlen zurückgeben, ist eine subtile Verletzung der Anweisung-Abfrage-Trennung.
Wenn man einen Fehler-Code zurückgibt, schafft man das Problem, dass der Aufrufer den Fehler sofort behandeln muss.
Werden dagegen Ausnahmen anstelle von Fehler-Codes verwendet, dann kann man den Code für die Fehler-Verarbeitung von
dem Code des Normalverlaufs trennen und sehr vereinfachen.
Try/Catch-Blöcke extrahieren
Try/catch-Blöcke sind von Natur aus hässlich. Sie verdunkeln die Struktur des Codes und vermengen die
Fehler-Verarbeitung mit der normalen Verarbeitung. Deshalb ist es besser, die Körper der try- und catch-Blöcke
in separate Funktionen zu extrahieren.
Fehler-Verarbeitung ist eine Aufgabe
Funktionen sollten eine Aufgabe erfüllen. Fehler-Verarbeitung ist eine Aufgabe. Deshalb sollte eine Funktion, die
Fehler verarbeitet, nichts anderes tun.
Der Abhängigkeitsmagnet Error.java
Fehler-Codes zurückzugeben bedeutet normalerweise auch, dass es eine Klasse oder enum gibt, in der alle Fehler-Codes
definiert sind. Klassen wie diese sind ein Abhängigkeitsmagnet; viele andere Klassen müssen sie importieren
und verwenden.
Arbeitet man nicht mit Fehler-Codes sondern mit Ausnahmen, dann sind neue Ausnahmen abgeleitete Klassen der
Exception-Klasse. Man kann neue hinzufügen, ohne dass eine Neukompilierung und ein Redeployment erforderlich sind. Dies
ist ein Beispiel für das Open-Closed-Prinzip (OCP).
- Don't Repeat Yourself
Möglicherweise ist die Duplizierung die Wurzel allen Übels bei der Software-Entwicklung. Viele Prinzipien und
Techniken wurden zu dem Zweck entwickelt, sie zu kontrollieren oder zu eliminieren. Bei der objektorientierten
Programmierung bemüht man sich darum, andernfalls redundanten Code in Basisklassen zu konzentrieren.
- Strukturierte Programmierung
Jede Funktion und jeder Block innerhalb einer Funktion sollte genau einen Eingang und einen Ausgang haben.
Hält man Funktionen klein, dann richtet eine gelegentliche Anwendung von mehreren return-, break- oder
continue-Anweisungen keinen Schaden an und kann den Code manchmal ausdrucksstärker machen als die
Ein-Eingang-ein-Ausgang-Regel.
- Wie schreibt man solche Funktionen?
Man kann durchaus Funktionen schreiben, die zunächst lang und kompliziert sind. Dann sollte man den Code verfeinern,
Funktionen auslagern, Namen ändern und Duplizierungen eliminieren. Man sollte die Methoden verkleinern und umstellen.
Manchmal kann man ganze Klassen herausbrechen, während man gleichzeitig dafür sorgt, dass die Unit-Tests (weiterhin)
bestanden werden.