7 Tipps zum Schreiben von lesbarem Java-Code

11.10.2019

Webcast-Online CommunitiesBekanntlich wird Code viel öfter gelesen als geschrieben. Häufig wird er auch von anderen Personen gelesen als von derjenigen, die ihn verfasst hat. Und hast du nicht selbst schon erlebt, dass dein eigener Code nur wenige Monate später völlich unverständlich für dich war…? ;-) Umso wichtiger ist es, dass du dir darüber Gedanken machst, wie du deinen Code so lesbar und verständlich wie möglich gestalten kannst.

Doch was macht lesbaren, verständlichen Code aus? Er ist ausdrucksstark, das heißt er drückt aus was er tut. Und zwar genau das, und nur das.

Allerdings ist das Schreiben von lesbarem Code nicht wirklich leicht. Robert C. Martin („Uncle Bob“) zieht in seinem Buch über Clean Code1 den Vergleich zur Malerei: Es ist einfach zu erkennen ob ein Bild gut oder schlecht gemalt ist. Aber selbst ein gutes Bild zu malen, dafür braucht es viel Übung.

Daher möchte dir die folgenden 7 Tipps an die Hand geben, die dir dabei helfen können, lesbaren Code zu schreiben.

1. Fasse dich kurz!

Beherzige das Single Responsibility Principle. Und zwar auf Klassen- als auch auf Methodenebene. Klassen sollten nur eine Verantwortlichkeit haben. Genauso sollten Methoden nicht mehrere Aufgaben auf einmal erledigen. Erstelle für jede Aufgabe eine eigene Methode. Zum Beispiel sollte eine Methode entweder nur einen Wert ändern, oder einen Wert zurückliefern – nicht aber beides zugleich. Hat eine Methode Seiteneffekte, sollte dies über den Methodennamen kenntlich gemacht werden.

Vermeide Wiederholungen („don’t repeat yourself„). Wenn drei oder vier gleiche Codezeilen immer wieder auftauchen, ggf. auch leicht variiert, mach eine Methode daraus, und parametrisiere, um die Varianten abbilden zu können. Eine Zeile ist viel schneller zu erfassen und zu verstehen als mehrere – insbesondere wenn die Methode einen sprechenden Namen hat.

Benutze keine langen Lambdas mit Blöcken in Java 8 Streams. Lagere die Funktionalität stattdessen in eine Methode aus.

Vorher:

List<Person> adults = persons.stream()
    .filter(p -> {
        boolean result;
        if (p.age() >= 18) {
            result = false;
            LOG.info("Person is adult");
        } else {
            LOG.info("Person is not adult");
            result = true;
        }
        return result;
    })
    .collect(toList());

Nachher:

List<Person> adults = persons.stream()
    .filter(person -> person.isAdult())
    .collect(toList());

Schon besser. Nun ist schnell ersichtlich, welche Aktionen auf dem Personen-Stream ausgeführt werden. Die Einrückung hilft dabei, das Ganze noch überschaubarer zu machen. (Zugleich haben wir die „isAdult()„-Funktionalität in die Klasse Person geholt, wo sie eigentlich hingehört).

Noch kürzer wird der Code mit einer Methodenreferenz:

List<Person> adults = persons.stream()
    .filter(Person::isAdult)
    .collect(toList());

Um genau zu verstehen was in der filter(…)-Methode passiert – d.h. ob Erwachsene beibehalten oder ausgefilter werden – musst du jedoch bereits wissen wie filter(…) genau funktioniert, oder es im JavaDoc nachlesen. Eine noch bessere Variante lernst du daher noch in Tipp Nr. 5 kennen.

2. Wähle gute Namen

Sehr wichtig für die Lesbarkeit von Code ist dass die Namen für Klassen, Klassenvariablen und Methoden so gewählt sind, dass sie genau aussagen was das benannte Konstrukt macht. Namen sollten so lang wie nötig, aber so kurz wie möglich sein. Keinesfalls sollten sie „lügen“, die Leser also nicht dazu verleiten, Annahmen über den Code zu treffen die unwahr sind. Natürlich schreibt niemand absichtlich solchen Code, aber durch nachträgliche unachtsame Änderungen kann so etwas durchaus allmählich entstehen.

Der Lesbarkeit zuträglich ist es auch, wenn für das gleiche Konzept durchgängig derselbe Begriff verwendet wird. Hast du etwa eine Verwaltungsklasse „AbcHandler“ getauft, dann bleibe bei weiteren Klassen bei dem Begriff „Handler“. Führe keinen weiteren Begriff ein, indem du eine neue Verwaltungsklasse „XyzManager“ taufst. Bei Methodennamen kannst du dich z.B. zwischen store(…) und save(…) entscheiden, solltest dann aber konsistent nur den gewählten Begriff nutzen.

Um die Verständlichkeit zu erhöhen bietet es sich zudem an, bereits etablierte Begriffe zu verwenden. Zum Beispiel „save“ wenn es um das Speichern einer Datei geht, oder „insert„, wenn ein neuer Eintrag in eine Datenbanktabelle geschrieben werden soll.

Wichtig dabei ist sicherzustellen, dass alle im Team die gewählten Begrifflichkeiten kennen und einheitlich nutzen.

Namensgebung ist nicht einfach, und es gibt viel was dabei beachtet werden sollte. Viele hilfreiche Tipps dazu findest du im Buch „API-Design“ von Kai Spichale2.

3. Verzichte auf Kommentare

Kommentare bergen oftmals Redundanzen, die unnötiges „Rauschen“ im Code erzeugen und so die Verständlichkeit reduzieren.  Hier ein Beispiel, in dem die Felder einer Klasse kommentiert sind:

// Der Buchungs-Service
protected bookingService;

// Der Datenbankzugriff
protected dbAccess;

Solche Kommentare bergen keinerlei Mehrwert und können bedenkenlos weggelassen werden. Manchmal „wandern“ Kommentare von der ursprünglich kommentierten Stelle weg, oder der kommentierte Code hat seine Bedeutung geändert, ohne dass entsprechende Kommentare angepasst wurden. Somit können Kommentare durchaus auch anfangen zu „lügen“, weil sie etwas anderes aussagen, als was der Code tatsächlich tut. Weil Kommentare tendenziell eher stiefmütterlich behandelt werden und man oft vergisst, sie bei Codeänderungen mit anzupassen, sollte man sie wenn möglich lieber gleich ganz weglassen.

Auskommentierter Code stört nur und sollte nicht behalten, sondern gelöscht werden. Die Versionsverwaltung sorgt dafür, dass man ihn bei Bedarf wieder zurückholen kann.

Doch nicht immer sind Kommentare problematisch – manchmal sind sie durchaus sinnvoll und wichtig. Eine Faustregel lautet: „Kommentiere das Warum, nicht das Was„. Denn was der Code tut, kann in der Regel gut durch entsprechende Namensgebung für Methoden und Variablen ausgedrückt werden. Warum etwas im Code gemacht wird, lässt sich dagegen oft nur durch Kommentare ausdrücken.

Das Clean-Code-Buch1 widmet dem Thema Kommentare sogar ein ganzes Kapitel.

4. Bevorzuge Lesbarkeit vor Effizienz

Manche machen sich einen Sport daraus, all ihren Code auf Ressourcen- und Laufzeiteffizienz zu optimieren. Jedoch geht dies so gut wie immer zu Lasten der Lesbarkeit. Zum Beispiel sind Arrays effizienter als Listen. Müssen aber dynamisch Elemente entfernt oder hinzugefügt werden, ist der dafür nötige Code um Einiges komplexer, und damit schwerer verständlich, als bei Verwendung einer Liste. Einen merkbaren Performancegewinn bekommt man dadurch jedoch nicht.

In der Regel liegen Performanceprobleme ohnehin ganz woanders als dort wo wir Entwickler sie erwarten. Daher sollten wir stets nur an Stellen optimieren, an denen ein Performanceproblem durch Messungen nachweislich identifiziert wurde.

5. Drücke Fachlichkeit durch Code aus

Achte darauf, dass der Code wann immer möglich die Fachlichkeit ausdrückt. Folgender Code beispielsweise tut dies nicht, stattdessen ist er geprägt von programmiertechnischen Details:

private void grantBonusIfQualified(List<Employee> employees) {
    employees.stream()
        .filter(getBonusPredicate())
        .forEach(Employee::addBonus);
}

private static Predicate<Employee> getBonusPredicate() {
    return e -> e.getSalary() > MIN_SALARY_FOR_BONUS;
}

Predicate ist ein Java-Konstrukt, anhand dessen die filter(…)-Methode entscheidet, ob ein Element im Ergebnis verbleiben oder aussortiert werden soll. Der Name getBonusPredicate() trägt nicht gerade zur Lesbarkeit des Codes bei.

Besser wäre:

private void grantBonusIfQualified(List<Employee> employees) {
    employees.stream()
        .filter(employeeIsQualifiedForBonus())
        .forEach(Employee::addBonus);
}

private static Predicate<Employee> employeeIsQualifiedForBonus() {
    return e -> e.getSalary() > MIN_SALARY_FOR_BONUS;
}

Hier könnte man sich jedoch wie oben wieder fragen, ob die filter(…)-Methode bonusberechtigte Mitarbeiter beibehält oder aussortiert. Am ausdrucksstärksten ist daher die folgende Version:

private void grantBonusIfQualified(List<Employee> employees) {
    employees.stream()
        .filter(keepingEmployeesQualifiedForBonus())
        .forEach(Employee::addBonus);
}

private static Predicate<Employee> keepingEmployeesQualifiedForBonus() {
    return e -> e.getSalary() > MIN_SALARY_FOR_BONUS;
}

Hier wird deutlich, dass die Filter-Funktion alle Mitarbeiter beibehält, die für einen Bonus berechtigt sind. (Der Begriff „keeping“ ist übrigens absichtlich so gewählt, aus Gründen die im nächsten Tipp erläutert werden.)

Auch komplexere, verschachtelte if-Statements werden lesbarer, wenn man sie in Methoden packt, deren Namen die Fachlichkeit widerspiegeln:

Vorher:

private void grantBonusIfQualified(Employee employee) {
    if (employee.isSenior() && employee.getSalary() > MIN_SALARY_FOR_BONUS)) {
        employee.addBonus(); 
    }
}

Nachher:

private void grantBonusIfQualified(Employee employee) {
    if (isQualifiedForBonus(employee)) {
        employee.addBonus();
    }
}

private static boolean isQualifiedForBonus(Employee employee) {
    return employee.isSenior() && isSalaryQualifiedForBonus(employee);
}

private static boolean isSalaryQualifiedForBonus(Employee employee) {
    return employee.getSalary() > MIN_SALARY_FOR_BONUS;
}

Nun muss sich der Leser eigentlich nur noch die Methode grantBonusIfQualified(…) anschauen und begreift sofort, was darin passiert. Wenn ihn nicht interessiert wie die Kriterien für den Bonus genau aussehen, war’s das schon (möglicherweise ist es dann sogar ausreichend, nur den Methodennamen an der aufrufenden Stelle zu lesen!). Andernfalls kann er sich die beiden Untermethoden im Detail anschauen.

6. Fördere den natürlichen Lesefluss

Besonders lesbar ist Code, wenn er sich wie gesprochene (bzw. geschriebene) Sprache anfühlt. Die APIs moderner Java-Bibliotheken sind oft in dieser Hinsicht optimiert. Das wird deutlich, wenn wir die herkömmlichen JUnit-Asserts mit denen aus AssertJ oder Hamcrest vergleichen:

// JUnit:
assertEqual("Miller", employee.getLastName());

// AssertJ:
assertThat(employee.getLastName()).isEqualTo("Miller");

// Hamcrest:
assertThat(employee.getLastName(), is(equalTo(("Miller")));

Die letzten beiden Varianten lesen sich flüssiger, da sie mehr Ähnlichkeit mit der geschriebenen Sprache aufweisen als die JUnit-Version. Das wird durch unterschiedliche Mittel erreicht: AssertJ setzt auf eine Fluent API, während Hamcrest mit der Verschachtelung von Methoden arbeitet. Gemein ist solchen APIs, dass sie eine „interne DSL3“ bilden, mit deren Hilfe der jeweilige Verwendungszweck in flüssig lesbaren Code gegossen werden kann. Ähnliche DSL-APIs haben beispielsweise REST-assured oder jOOQ.

Auch neuere APIs der Java-Standardbibliothek sind (teilweise) auf bessere Lesbarkeit getrimmt – z.B. die Methoden der Collectors-Klasse für Java 8 Streams:

List<Person> personsFiltered = persons.stream()
   .filter(/* ... */)
   .collect(Collectors.toList());

Die Klassenangabe „Collectors.“ erzeugt hier noch ein Rauschen, das den Lesefluss behindert. Für optimale Lesbarkeit bietet sich daher ein statischer Import für die Methode toList() an:

import static java.util.stream.Collectors.toList;
// ...
List<Person> personsFiltered = persons.stream()
    .filter(/* ... */)
    .collect(toList());

Weitere Collectors-Methoden wie partitioningBy(…), groupingBy(…), joiningBy(…) u.a. sind ebenfalls so benannt, dass sie sich zusammen mit der collect(…)-Methode flüssig und natürlich lesen, wie z.B.:

personsPartitioned = persons.stream()
    .collect(partitioningBy(Person::isAdult));

Auch Negierungen lassen sich durch Methodennamen besser ausdrücken als mit dem Java-Standardoperator ! – dieser kann nur zu leicht übersehen werden, wie du am folgenden Codebeispiel vielleicht erkennst:

if (!StringUtils.isEmpty(employeeName)) {
    // ...
}

Die Klasse StringUtils aus der Bibliothek Apache commons-lang3 bietet für solche Fälle extra negierte Methoden an:

if (StringUtils.isNotEmpty(employeeName)) {
    // ...
}

Die Lesbarkeit wird auch hier durch den statischen Import weiter verbessert:

import static org.apache.commons.lang3.StringUtils.isNotEmpty;
// ...
if (isNotEmpty(employeeName)) {
   // ... 
}

Von den genannten und ähnlichen APIs können wir uns durchaus ein paar Tricks abschauen. Zum Beispiel die Art und Weise wie Methoden benannt sind, das Anbieten von „not„-Methoden, oder die Einführung von Fluent APIs im eigenen Code.

Übrigens ist testgetriebene Entwicklung ein gutes Hilfsmittel beim Schreiben von lesbarem Code. Schreibt man zuerst einen Test, überlegt man sich eher, wie der neu zu schreibende Code aus der Perspektive des Aufrufers aussehen soll. Auf diese Art und Weise ist es einfacher, lesbaren Code zu produzieren, als wenn man den Produktivcode zuerst, oder ganz ohne Test schreibt.

7. Verbessere stetig die Lesbarkeit

Scheue dich nicht, Bestandscode auf bessere Lesbarkeit zu trimmen. Der erste Wurf ist in der Regel nicht der Beste. Selbst der Meister des „Clean Code“, Robert „Uncle Bob“ Martin gibt das zu. Hinterlasse die Stellen die du antriffst gemäß dem „Pfadfinder-Prinzip“ stets besser als du sie angetroffen hast. Code Reviews sind ein hilfreiches Mittel, schließlich hat der Reviewer einen Blick von außen auf den Code, da er die gedanklichen Hintergründe des Verfassers nicht kennt, die dieser bei der Programmierung im Kopf hatte. Hier ist es wichtig, dass Verbesserungsvorschläge sachlich und konstruktiv vorgebracht werden. Zudem müssen alle verstanden haben, dass der Code dem Team gehört, und nicht das „Baby“ von demjenigen ist der ihn geschrieben hat. Jedes Teammitglied sollte offen für konstruktive Kritik sein.

Weitere Hilfestellung kannst du dir von Tools holen. Zum Beispiel von SonarQube, das problematische Stellen aufzeigt, oder eine IDE wie IntelliJ IDEA, die mit ihren Inspections auf mögliche Verbesserungen hinweist. Eine umfangreiche und gut gepflegt Suite von Unit- und Integrationstests gibt dir die nötige Sicherheit, Änderungen durchzuführen ohne Angst haben zu müssen, dass du etwas kaputt machst.

Fazit

Diese 7 Tipps können dir beim Schreiben von gut lesbarem Code helfen:

  1. Fasse dich kurz!
  2. Wähle gute Namen
  3. Verzichte auf Kommentare
  4. Bevorzuge Lesbarkeit vor Effizienz
  5. Drücke Fachlichkeit durch Code aus
  6. Fördere den natürlichen Lesefluss
  7. Verbessere stetig die Lesbarkeit

 

Weil Code viel öfter gelesen als geschrieben wird, spart lesbarer und verständlicher Code viel Zeit. Er „lügt“ nicht, verleitet nicht zu falschen Annahmen und birgt keine unangenehmen Überraschungen. In ausdrucksstarkem Code findet man sich schnell zurecht und muss nicht lange suchen um die Stelle zu finden, an der man bei einer Änderung ansetzen muss.

Anhang

 

1 Clean Code – Refactoring, Patterns, Testen und Techniken für sauberen Code, von Robert C. Martin ^
2 API-Design – Praxishandbuch für Java- und Webservice-Entwickler, Kai Spichale ^
3 Domain Specific Language ^

Zurück zur Übersicht

Kommentar verfassen

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

*Pflichtfelder

*