Lesbarer Code: Konventionen, sprechende Namen, Kommentare und das Prinzip der geringsten Überraschung

https://bildung.social/@oerinformatik/

https://oer-informatik.de/lesbarer-code

Clean Code, Lesbarkeit, Namenskonvention, PEP 8, Magic Number, Kommentar, Prinzip der geringsten Überraschung, Command-Query-Separation, Linter, Checkstyle, Ruff

tl/dr; (ca. 34 min Lesezeit): Code wird viel öfter gelesen als geschrieben. Dieser Artikel zeigt an der Kata der CSB-Rad GmbH, wie Du Code mit wenigen Handgriffen lesbar machst: einheitliche Schreibweisen nach den Konventionen für Java und Python (PEP 8), sprechende Namen statt Abkürzungen, benannte Konstanten und Aufzählungen statt magischer Zahlen, Kommentare, die das Warum erklären, und Methoden, die tun, was ihr Name verspricht (Prinzip der geringsten Überraschung, Command-Query-Separation). Zum Schluss geht es um Linter und Formatierer, die einen Teil dieser Regeln automatisch prüfen [AI-Assisted]. (Zuletzt geändert am 10.11.2026)

Dieser Artikel ist Teil einer Artikelreihe über Clean Code: Überblick / Lesbarer Code / Einfach bleiben / Refactoring / Methoden gliedern / Klassen schneiden / Vererbung richtig einsetzen / Erweitern statt ändern / SOLID / Vom Entwurf zum Code

Dieser Artikel baut auf der Kann-Liste des Artikels Clean Code: Werte, Kopplung und Kohäsion, Code Smells, Prinzipien und Praktiken im Überblick auf.

Schon fit in diesem Thema? Nutze die Kann-Liste und die Leitfragen unter dem Artikel als Checkliste, ob Du die relevanten Kompetenzen bereits hast.

Als Grundlagen lesbaren Codes kann ich nach Durchlesen des Artikels…

  • … die Source Code Conventions für Java und Python (PEP 8) nennen und anwenden: Schreibweisen von Klassen, Methoden, Variablen und Konstanten, Einrückung und Zeilenlänge.

  • … sprechende Namen für Variablen, Methoden und Klassen wählen.

  • … schlechte Namen (Abkürzungen, Typ im Namen, irreführende Namen) erkennen und verbessern.

  • … magische Zahlen und Zeichenketten durch benannte Konstanten oder Aufzählungen ersetzen.

  • … beurteilen, welche Kommentare sinnvoll sind (Warum, verworfene Alternativen, Dokumentationskommentare) und welche durch besseren Code ersetzt werden sollten.

  • … Linter und Formatierer (Checkstyle, Ruff, Flake8, Black) als Werkzeuge einordnen und sagen, was sie prüfen können und was nicht.

Darüber hinaus kann ich als Vertiefung…

  • … das Prinzip der geringsten Überraschung erklären und Verstöße erkennen (Getter mit Seiteneffekt, Methodenname widerspricht Verhalten, unerwartete Rückgabewerte).

  • … Command-Query-Separation erklären und eine Methode, die abfragt und zugleich verändert, aufteilen.

  • … begründen, wann eine Ausnahme von Command-Query-Separation vertretbar ist.

Stell Dir vor, Du sollst in einem fremden Programm einen Fehler finden. Der Code ist korrekt eingerückt, die Tests sind grün, aber die Variablen heißen s, r und vk, eine Methode getNr() zählt heimlich einen Zähler hoch, und ein Kommentar behauptet, die Preise seien brutto, obwohl sie netto sind. Bevor Du den Fehler suchen kannst, musst Du den Code entschlüsseln. Code wird viel öfter gelesen als geschrieben: beim Fehlersuchen, beim Erweitern, im Review, beim Einarbeiten neuer Teammitglieder. Jede Minute, die Du beim Schreiben in Lesbarkeit investierst, sparen alle Lesenden später mehrfach.

Dieser Artikel nimmt sich die unterste Ebene der Landkarte der Prinzipien vor: den einzelnen Ausdruck mit seinen Namen, Zahlen und Kommentaren. Am Beispiel der Code-Kata der CSB-Rad GmbH räumst Du Stand 01 zu Stand 02 auf, ohne am Verhalten etwas zu ändern. Zum Üben gibt es Übungsaufgaben zu lesbarem Code.

Die Gegenüberstellung zeigt den Kern des Umbaus: Aus der Bestellposition mit Ein-Buchstaben-Namen wird ein Datentyp, der sich selbst erklärt.

Vorher-Nachher-Gegenüberstellung der Bestellposition der Kata: links record Pos mit den Feldern nr, b, p, m, g, r und einem nichtssagenden Kommentar, rechts record Position mit artikelnr, bezeichnung, nettopreisCent, menge, gewichtGramm, istRad und einem Dokumentationskommentar (mit KI erstellt)

Warum Lesbarkeit zählt

„Programme müssen so geschrieben werden, dass Menschen sie lesen können, und nur nebenbei so, dass Maschinen sie ausführen.“ Diesen Satz schreiben Harold Abelson und Gerald Jay Sussman 1985 im Vorwort ihres Lehrbuchs „Structure and Interpretation of Computer Programs“ (Programs must be written for people to read, and only incidentally for machines to execute). Dem Compiler ist es gleich, ob eine Variable s oder warenwert heißt. Für Menschen ist es der Unterschied zwischen Raten und Verstehen.

Lesbarkeit ist kein Selbstzweck. Sie zahlt auf den Wert Wandelbarkeit ein, den Du im Überblicksartikel kennengelernt hast, und auf das Teilmerkmal Analysierbarkeit der Wartbarkeit nach ISO/IEC 25010. Ein Code, den niemand versteht, lässt sich nicht sicher ändern.

Ausgangspunkt ist der erste Stand der Kata. Die Methode abschliessen berechnet Warenwert, Rabatt, Versandkosten und Mehrwertsteuer, vergibt eine Bestellnummer, erzeugt eine Bestätigungsmail und speichert die Bestellung. Sie liefert die richtigen Ergebnisse (alle Charakterisierungstests sind grün), ist aber schwer zu lesen:

// Auszug aus src/kata/java/stand-01: Pos.java und Shop.java

// Position einer Bestellung
public record Pos(String nr, String b, int p, int m, int g, boolean r) {
}

public class Shop {

    private int nr = 1001;

    // liefert die Bestellnummer
    public int getNr() {
        return nr++;
    }

    // schliesst die Bestellung ab
    public int[] abschliessen(List<Pos> l, String v, String c, String m, boolean x) {
        int s = 0;
        for (Pos p : l) {
            if (p.m() <= 0) {
                throw new IllegalArgumentException("Fehler");
            }
            s = s + p.p() * p.m();
        }
        // ...
    }
}

In den folgenden Abschnitten gehst Du die Probleme einzeln durch: Schreibweisen, Namen, Zahlen, Kommentare und Methoden, die mehr tun, als ihr Name sagt. Jeder Abschnitt folgt demselben Muster: Problem, Prinzip, Umbau, Grenzen. Alle Umbauten ändern nur die Form des Codes, nicht sein Verhalten; die Charakterisierungstests bleiben grün. Den vollständigen Code beider Stände findest Du im Ordner src/kata des Repositorys zu dieser Reihe (stand-01 und stand-02).

Konventionen: einheitlich schreiben

Das Problem: Wenn jede Person im Team anders schreibt, die eine berechneRabatt, der andere Berechne_rabatt, die dritte BERECHNERABATT, muss jede Lesende bei jedem Namen neu überlegen, ob sie eine Klasse, eine Methode oder eine Konstante vor sich hat. Unterschiedliche Einrückungen und überlange Zeilen erschweren zusätzlich das Erfassen der Struktur.

Das Prinzip: Source Code Conventions (Programmier- oder Codierrichtlinien) legen fest, wie Code geschrieben wird: Schreibweise der Namen, Einrückung, Zeilenlänge, Leerzeichen, Anordnung von Klassenbestandteilen. Für Java hat Sun Microsystems 1997 die „Code Conventions for the Java Programming Language“ veröffentlicht; heute orientieren sich viele Teams am Google Java Style Guide. Für Python gibt es seit 2001 den Python Enhancement Proposal PEP 8 („Style Guide for Python Code“) von Guido van Rossum, Barry Warsaw und Alyssa Coghlan. Konventionen sind konstruktive Qualitätssicherung: Sie vermeiden Fehler, bevor sie entstehen (siehe Teststrategie und Qualitätssicherung).

Die wichtigsten Regeln im Vergleich:

Schreibweisen nach den Konventionen für Java und Python
Element Java Python (PEP 8) Beispiel aus der Kata
Klasse, Record, Interface, Enum UpperCamelCase (Substantiv) CapWords (wie UpperCamelCase) Shop, Position
Methode, Funktion lowerCamelCase (Verb) snake_case (Verb) Java vergibBestellnummer, Python vergib_bestellnummer
Variable, Parameter, Attribut lowerCamelCase snake_case Java kundenEmail, Python kunden_email
Konstante UPPER_SNAKE_CASE, static final UPPER_SNAKE_CASE auf Modulebene PAKETPAUSCHALE_CENT
Paket bzw. Modul Kleinbuchstaben, umgekehrte Domain kurze Kleinbuchstaben, ggf. snake_case de.csbrad.shop, csbrad.position
„privat“ Schlüsselwort private führender Unterstrich _ (Konvention) _postausgang
Einrückung 4 Leerzeichen (Sun), 2 Leerzeichen (Google) 4 Leerzeichen, keine Tabs
Zeilenlänge 80 (Sun) bzw. 100 Zeichen (Google) 79 Zeichen (PEP 8), 88 beim Formatierer Black

Die Schreibweisen findest Du ausführlicher im Artikel Datentypen, Variablen und Arrays. Für Getter und Setter in Java gilt zusätzlich die JavaBeans-Konvention getName(), setName(…) und isAktiv() für Wahrheitswerte (siehe Klassen implementieren).

Der Umbau: In der Kata waren die Schreibweisen schon in Stand 01 konventionsgemäß; das Problem lag bei den Namen selbst (nächster Abschnitt). Nach dem Umbau hält Stand 02 die Konventionen weiter ein, hat aber längere Namen und damit längere Zeilen. Wie Du das automatisch prüfst, zeigt der Abschnitt Werkzeuge: Linter und Formatierer.

Grenzen und Zielkonflikte: Konventionen sind Vereinbarungen, keine Naturgesetze. PEP 8 sagt selbst, Einheitlichkeit innerhalb eines Projekts sei wichtiger als die Einhaltung des Style Guides, und Einheitlichkeit innerhalb eines Moduls noch wichtiger (Abschnitt „A Foolish Consistency is the Hobgoblin of Little Minds“). Arbeitest Du in einem Projekt mit eigener Richtlinie, gilt diese. Wichtiger als die Frage, ob 100 oder 120 Zeichen, ist, dass das Team eine Regel festlegt und ein Werkzeug sie prüft, damit niemand im Review über Leerzeichen diskutieren muss.

In Java und Python

Einige Regeln sehen in beiden Sprachen ähnlich aus, beruhen aber auf unterschiedlichen Mechanismen:

  • Sichtbarkeit: Java erzwingt private durch den Compiler. Python kennt keine echte Sichtbarkeit; ein führender Unterstrich (_postausgang) sagt nur „intern, bitte nicht von außen benutzen“. Die Konvention ist also das einzige Schutzschild, umso wichtiger ist es, sie einzuhalten.
  • Konstanten: Java macht eine Konstante mit static final unveränderlich. In Python signalisieren Großbuchstaben nur die Absicht; mit der Typannotation Final aus dem Modul typing meldet ein Typprüfer wie mypy eine spätere Zuweisung als Fehler.
  • Getter: In Java sind get…-Methoden üblich. In Python greift man idiomatisch direkt auf ein Attribut zu oder nutzt eine @property, die wie ein Attribut gelesen wird. Die Kata verwendet in Python trotzdem get_postausgang(), damit beide Fassungen parallel lesbar bleiben.
  • Dokumentationskommentare: Java nutzt Javadoc-Kommentare /** … */ vor der Methode, Python Docstrings """…""" als erste Anweisung im Methodenrumpf (PEP 257).

// Konstante und Getter in Java
public class Kundenkonto {

    public static final int MINDESTALTER_JAHRE = 16;

    private final String email;

    public Kundenkonto(String email) {
        this.email = email;
    }

    public String getEmail() {
        return email;
    }
}

Sprechende Namen statt Rätsel

Das Problem: In Stand 01 heißen die Parameter von abschliessen l, v, c, m und x, die Felder der Position nr, b, p, m, g und r. Was p.p() * p.m() berechnet, erschließt sich erst, wenn Du die Testdaten danebenlegst. m bedeutet einmal die Menge einer Position und einmal die E-Mail-Adresse der Kundin. Das Attribut liste sagt nur, welcher Datentyp darin steckt, nicht, was: Bestellungen? Positionen? Mails? Martin Fowler führt diesen Code Smell als rätselhafter Name (Mysterious Name).

Das Prinzip: Sprechende Namen (Meaningful Names, auch intention-revealing names) verraten, wofür etwas steht, warum es existiert und wie es benutzt wird. Das Kapitel „Meaningful Names“ in Robert C. Martins „Clean Code“ (2008) stammt von Tim Ottinger, der seine Regeln schon in den 1990er-Jahren als „Ottinger’s Rules for Variable and Class Naming“ veröffentlicht hatte. Die wichtigsten Regeln:

Regeln für sprechende Namen mit Beispielen aus der Kata
Regel schlecht besser
Absicht offenlegen s, g, a warenwert, gesamtgewicht, anzahlRaeder
keine Abkürzungen, die man erst entschlüsseln muss vk, mw, txt versandkosten, mehrwertsteuer, mailtext
kein Datentyp im Namen liste, strEmail, posArray bestellungen, kundenEmail, positionen
nicht irreführen getListe() liefert Object[]-Einträge, m mal Menge, mal Mail getBestellungen(), menge, kundenEmail
Einheit nennen, wenn sie nicht offensichtlich ist p, g nettopreisCent, gewichtGramm
Klassen sind Substantive, Methoden Verben Pos, getNr() (zählt hoch) Position, vergibBestellnummer()
Wahrheitswerte als Frage r istRad
aussprechbar und suchbar s2, x warenwertFuerRabatt, express
ein Begriff je Konzept, Fachsprache der Anwender mal Auftrag, mal Order, mal Bestellung überall Bestellung wie im Shop

Die letzte Regel hat einen eigenen Namen: Eric Evans nennt die gemeinsame Fachsprache von Fachabteilung und Entwicklung 2003 in „Domain-Driven Design“ Ubiquitous Language. Wenn die Mitarbeitenden der CSB-Rad GmbH von „Abholung in der Werkstatt“ sprechen, sollte auch der Code ABHOLUNG heißen und nicht SELF_PICKUP.

Der Umbau: In Stand 02 heißen Record und Parameter so, wie die Fachleute im Shop sprechen. Die Einheit steckt im Namen, weil Beträge in Cent und Gewichte in Gramm gespeichert werden. Bei einem Feld preis würde jede Lesende raten, ob Euro oder Cent gemeint sind:

// Auszug aus src/kata/java/stand-02: Position.java und Shop.java

/** Eine Bestellposition; Preise sind Nettopreise in Cent, Gewichte in Gramm. */
public record Position(String artikelnr, String bezeichnung, int nettopreisCent, int menge, int gewichtGramm, boolean istRad) {
}

public int[] abschliessen(List<Position> positionen, String versandart, String gutscheincode, String kundenEmail, boolean express) {
    int warenwert = 0;
    for (Position position : positionen) {
        if (position.menge() <= 0) {
            throw new IllegalArgumentException("Menge muss positiv sein: " + position.artikelnr());
        }
        warenwert = warenwert + position.nettopreisCent() * position.menge();
    }
    // ...
}

Nebenbei sind auch die Fehlermeldungen sprechend geworden: Aus "Fehler" wird "Menge muss positiv sein: SCH-BUE-01". Wer die Meldung im Log liest, weiß sofort, wo er suchen muss. In Python heißt die Datei jetzt position.py statt pos.py, denn auch Modulnamen sind Namen.

Ein Name ist jetzt sprechend, aber trotzdem verdächtig: express. Der Parameter wird nirgends benutzt. Ein guter Name macht so etwas sichtbar, beseitigt es aber nicht. Um überflüssigen Code geht es im nächsten Artikel der Reihe, Einfach bleiben.

Grenzen und Zielkonflikte: Länger ist nicht automatisch besser. Die Länge eines Namens darf zur Größe seines Gültigkeitsbereichs passen: Eine Laufvariable i in einer dreizeiligen Schleife ist für alle verständlich, eine Klassenvariable i nicht. Namen wie warenwertNachRabattOhneVersandInCent sind ein Hinweis, dass ein Datentyp fehlt (etwa eine Klasse Geldbetrag), nicht dass der Name noch länger werden muss. Ein zweiter Zielkonflikt ist die Sprache: Die Kata benennt fachliche Begriffe auf Deutsch, weil die Fachabteilung so spricht, nutzt aber die englischen Konventionsteile get und is. Viele Teams schreiben Code durchgängig englisch, auch damit internationale Kolleg*innen mitlesen können. Beides ist vertretbar, solange es im Projekt einheitlich ist.

Magische Zahlen und Zeichenketten benennen

Das Problem: In Stand 01 stehen Zahlen wie 690, 31500, 10000, 4900 und 19 mitten im Code, dazu Zeichenketten wie "KURIER10". Was bedeutet 31500? Erst wer weiß, dass ein Paket höchstens 31,5 kg wiegen darf und Gewichte in Gramm gespeichert sind, versteht die Zeile. Und wenn der Versanddienstleister die Pauschale erhöht, musst Du alle Stellen mit 690 finden und darfst keine übersehen.

// Auszug aus src/kata/java/stand-01, Klasse Shop (Paketversand)
int n = g / 31500;
if (g % 31500 > 0 || n == 0) {
    n = n + 1;
}
if (s - r >= 10000) {
    vk = 0;
} else {
    vk = n * 690;
}

Das Prinzip: Eine magische Zahl (Magic Number) ist ein Zahlen- oder Textwert ohne Namen, dessen Bedeutung sich nicht aus dem Code ergibt. Die Regel, solche Werte durch benannte Konstanten zu ersetzen, ist eine der ältesten Programmierregeln; Martin Fowler hat sie 1999 in „Refactoring“ als Umbauschritt Replace Magic Number with Symbolic Constant beschrieben, in der zweiten Auflage 2018 heißt er Replace Magic Literal (magisches Literal ersetzen). Der Name einer Konstanten erklärt die Bedeutung, und der Wert steht nur an einer Stelle.

Der Umbau: Stand 02 sammelt die Werte als Konstanten am Anfang der Klasse bzw. des Moduls. Jede Konstante trägt die Einheit im Namen:

// Auszug aus src/kata/java/stand-02, Klasse Shop
private static final int ERSTE_BESTELLNUMMER = 1001;
private static final String GUTSCHEINCODE_KURIER = "KURIER10";
private static final int GUTSCHEIN_RABATT_PROZENT = 10;
private static final int PAKETPAUSCHALE_CENT = 690;
private static final int HOECHSTGEWICHT_PAKET_GRAMM = 31500;
private static final int VERSANDKOSTENFREI_AB_CENT = 10000;
private static final int SPEDITION_JE_RAD_CENT = 4900;
private static final int MEHRWERTSTEUER_PROZENT = 19;

// ... im Paketversand:
int anzahlPakete = gesamtgewicht / HOECHSTGEWICHT_PAKET_GRAMM;
if (gesamtgewicht % HOECHSTGEWICHT_PAKET_GRAMM > 0 || anzahlPakete == 0) {
    anzahlPakete = anzahlPakete + 1;
}
if (warenwert - rabatt >= VERSANDKOSTENFREI_AB_CENT) {
    versandkosten = 0;
} else {
    versandkosten = anzahlPakete * PAKETPAUSCHALE_CENT;
}

Jetzt liest sich die Bedingung fast wie die Regel im Shop: „Wenn der Warenwert nach Rabatt die Versandkostenfreigrenze erreicht, kostet der Versand nichts.“

Aufzählungen für feste Wertemengen

Für eine feste Menge zusammengehöriger Werte, etwa die Versandarten "PAKET", "SPEDITION" und "ABHOLUNG", ist eine Konstante je Wert nur der halbe Weg. Eine Aufzählung (Enumeration, enum) macht daraus einen eigenen Datentyp. Dann kann ein Tippfehler wie "PAKTE" gar nicht erst an die Methode übergeben werden, und die Entwicklungsumgebung schlägt die erlaubten Werte vor. Ein kleines eigenes Beispiel:

// Eigenes Beispiel: Versandart als Aufzählung
enum Versandart {
    PAKET, SPEDITION, ABHOLUNG
}

class VersandartBeispiel {

    static String bezeichnung(Versandart versandart) {
        return switch (versandart) {
            case PAKET -> "Paket";
            case SPEDITION -> "Spedition";
            case ABHOLUNG -> "Abholung";
        };
    }

    public static void main(String[] args) {
        Versandart gewaehlt = Versandart.valueOf("PAKET");  // Text aus dem Bestellformular
        System.out.println(bezeichnung(gewaehlt));          // Paket
    }
}

Ein unbekannter Text wie "BRIEF" scheitert schon beim Umwandeln (Versandart.valueOf wirft eine IllegalArgumentException, Versandart("BRIEF") einen ValueError) und nicht erst tief in der Berechnung. In Java prüft der Compiler bei einem switch-Ausdruck über ein Enum zusätzlich, dass jeder Wert behandelt wird. Die Kata behält die Zeichenketten in Stand 02 noch bei, weil sich sonst die Schnittstelle von abschliessen ändern würde; wie die Versandarten später eigene Klassen werden, zeigt der Artikel Erweitern statt ändern.

Grenzen und Zielkonflikte: Nicht jede Zahl ist magisch. 0 als Startwert einer Summe, + 1 beim Hochzählen oder die 100 beim Umrechnen von Prozent sind selbsterklärend; PROZENT_NENNER = 100 macht den Code nicht klarer. Stand 02 enthält noch die Rechnung (betrag * prozent + 50) / 100: Die 50 sorgt für kaufmännisches Runden, und das sagt jetzt ein kurzer Kommentar. Besser als eine Konstante RUNDUNGSZUSCHLAG = 50 ist hier eine eigene Methode mit dem Namen prozentKaufmaennischGerundet; dieser Schritt folgt im Artikel Refactoring. Und eine Konstante ist nur dann ein Gewinn, wenn ihr Name mehr sagt als ihr Wert: NEUNZEHN = 19 ist genauso magisch wie 19.

Kommentare: das Warum erklären

Das Problem: Die Klasse Shop in Stand 01 enthält fünf Kommentare. Keiner davon hilft:

Die Kommentare in Stand 01 der Kata
Kommentar in Stand 01 Was ist das Problem?
// liefert die Bestellnummer über getNr() wiederholt den Methodennamen und verschweigt das Wichtigste: dass die Methode den Zähler erhöht
// schliesst die Bestellung ab wiederholt den Methodennamen, sagt nichts über Rückgabe und Nebenwirkungen
// Rabatt ersetzt einen fehlenden Namen; mit int rabatt ist er überflüssig
// Versand ist ab 100 Euro immer kostenlos falsch: gilt nur für den Paketversand und für den Warenwert nach Rabatt
// Preise sind brutto falsch: Die Preise sind netto, die Mehrwertsteuer wird ja gerade aufgeschlagen

Dazu kommt ein sechster Kommentar über dem Mehrwertsteuer-Cache („damit die MwSt nicht jedes Mal neu berechnet wird (schneller!)“). Er rechtfertigt Code, der gar nicht gebraucht wird; um ihn kümmert sich der Artikel Einfach bleiben. Ein falscher Kommentar ist schlimmer als keiner: Wer ihm glaubt, baut den nächsten Fehler ein. Und Kommentare veralten leicht, denn der Compiler prüft sie nicht und Tests auch nicht.

Das Prinzip: Schon Brian W. Kernighan und P. J. Plauger rieten 1974 in „The Elements of Programming Style“: Don’t comment bad code – rewrite it (kommentiere schlechten Code nicht, schreib ihn neu). Robert C. Martin widmet den Kommentaren in „Clean Code“ (2008) ein eigenes Kapitel. Die Faustregel daraus: Der Code sagt, was und wie; der Kommentar sagt, warum. Bevor Du einen Kommentar schreibst, prüfe, ob ein besserer Name, eine Konstante oder eine eigene Methode ihn überflüssig macht.

Sinnvoll sind vor allem diese Kommentare:

  • Warum-Kommentare: fachliche Gründe, die nicht im Code stehen können, z. B. „Räder passen in kein Paket“.
  • Verworfene Alternativen: warum der naheliegende Weg nicht gewählt wurde, z. B. „Kein double für Beträge, weil Rundungsfehler bei 0,1 + 0,2 auftreten; Beträge in Cent als int“. So probiert die nächste Person nicht dasselbe noch einmal.
  • Warnungen und Hinweise auf Folgen: „Reihenfolge nicht ändern: Die Nummer darf erst nach allen Prüfungen vergeben werden.“
  • Dokumentationskommentare für öffentliche Klassen und Methoden: Javadoc bzw. Docstrings beschreiben, was die Methode verspricht, welche Parameter und Rückgaben es gibt und welche Ausnahmen sie wirft. Aus ihnen erzeugen Werkzeuge eine Dokumentation (siehe Entwicklungswerkzeuge).
  • Rechtliche Hinweise wie Lizenzkopf und TODO-Kommentare mit Verweis auf ein Ticket, solange sie abgearbeitet werden.

Überflüssig oder schädlich sind dagegen Kommentare, die den Code nacherzählen (// i erhöhen), Namen ersetzen (// Rabatt), falsch oder veraltet sind, auskommentierter Code („vielleicht brauchen wir das noch“, dafür gibt es die Versionsverwaltung) und Änderungsprotokolle im Code („geändert am … von …“, auch das weiß Git besser).

Der Umbau: In Stand 02 sind die nichtssagenden Kommentare entfernt, die falschen korrigiert und um das Warum ergänzt, und abschliessen hat einen Dokumentationskommentar:

// Auszug aus src/kata/java/stand-02, Klasse Shop
/**
 * Berechnet die Beträge einer Bestellung, verschickt die Bestätigungsmail und speichert die Bestellung.
 *
 * @return {Warenwert, Rabatt, Versandkosten, Mehrwertsteuer, Gesamtbetrag} in Cent
 */
public int[] abschliessen(List<Position> positionen, String versandart, String gutscheincode, String kundenEmail, boolean express) {
    // ...
    // kaufmännisch gerundet
    rabatt = (warenwertFuerRabatt * GUTSCHEIN_RABATT_PROZENT + 50) / 100;
    // ...
    if (versandart.equals("PAKET")) {
        // Räder passen in kein Paket; die Freigrenze gilt für den Warenwert nach Rabatt
        // ...
    }
    // ...
    // Preise sind Nettopreise; die Mehrwertsteuer kommt auf Waren und Versand
    int nettobetrag = warenwert - rabatt + versandkosten;
    // ...
}

Der Dokumentationskommentar nennt auch die Reihenfolge der Werte im Rückgabe-Array. Dass eine Methode diesen Hinweis überhaupt braucht, ist ein Warnsignal: Ein Rückgabewert, dessen Bedeutung von der Position abhängt, ist selbst ein kleines Rätsel. Dafür bekommt die Kata später einen eigenen Datentyp (Klassen schneiden).

Grenzen und Zielkonflikte: „Guter Code braucht keine Kommentare“ ist eine Übertreibung. Kein Name kann erklären, warum die Freigrenze nach dem Rabatt gilt; das ist eine Entscheidung der Geschäftsleitung, und genau dafür ist ein Kommentar da. Öffentliche Schnittstellen einer Bibliothek brauchen Dokumentationskommentare auch dann, wenn die Namen gut sind, weil Nutzende den Quelltext oft gar nicht sehen. Umgekehrt gilt: Je mehr Kommentare, desto mehr muss beim Ändern mitgepflegt werden. Schreib wenige, aber richtige.

Das Prinzip der geringsten Überraschung

Das Problem: Die Methode getNr() aus Stand 01 sieht aus wie ein harmloser Getter. Tatsächlich zählt sie bei jedem Aufruf die Bestellnummer hoch:

// Auszug aus src/kata/java/stand-01, Klasse Shop
private int nr = 1001;

// liefert die Bestellnummer
public int getNr() {
    return nr++;
}

Wer nur nachsehen möchte, welche Nummer die nächste Bestellung bekommt, und dazu getNr() aufruft, verbraucht dabei eine Nummer. Die nächste Kundin bekommt dann CSB-1002 statt CSB-1001, und in der Buchhaltung fehlt eine Nummer. Noch tückischer: Ein Debugger, der den Getter beim Anzeigen der Objektwerte aufruft, oder eine Log-Ausgabe zum Testen verändert das Programmverhalten. Der Fehler verschwindet oder taucht auf, je nachdem, ob Du hinsiehst.

Das Prinzip: Das Prinzip der geringsten Überraschung (Principle of Least Astonishment, POLA, auch Principle of Least Surprise) verlangt, dass sich ein Bestandteil so verhält, wie die meisten Nutzenden es aufgrund seines Namens und seines Umfelds erwarten. Der Grundsatz stammt aus der Gestaltung von Programmiersprachen und Benutzungsoberflächen; wer ihn zuerst so genannt hat, ist nicht eindeutig belegt. Bekannt gemacht hat ihn unter anderem Eric S. Raymond, der ihn 2003 in „The Art of Unix Programming“ als Rule of Least Surprise aufführt. Auch die Initiative Clean Code Developer führt ihn als eigenes Prinzip; in der Landkarte dieser Reihe gehört er zur Ebene Ausdruck. Typische Verstöße:

  • Getter mit Seiteneffekt: Ein get… liest, er ändert nichts. getNr() verstößt dagegen.
  • Name widerspricht Verhalten: pruefeEmail() ändert die E-Mail-Adresse in Kleinbuchstaben, berechneSumme() speichert zusätzlich in die Datenbank, isLeer() löscht den Inhalt.
  • Unerwartete Rückgabewerte: eine Methode, die bei „nichts gefunden“ mal null, mal eine leere Liste und mal -1 liefert; ein Array, dessen Werte nur über ihre Position verständlich sind; ein getBestellungen(), das die interne, veränderbare Liste herausgibt, sodass Aufrufende den Zustand der Klasse unbemerkt ändern können.
  • Unerwartete Nebenwirkungen auf Parameter: eine Methode, die die übergebene Liste sortiert oder leert, obwohl ihr Name das nicht ankündigt.

Der Umbau: Stand 02 entfernt den öffentlichen Getter. Die Nummernvergabe wird eine private Methode, deren Name ankündigt, dass etwas passiert:

// Auszug aus src/kata/java/stand-02, Klasse Shop
private int naechsteBestellnummer = ERSTE_BESTELLNUMMER;

private int vergibBestellnummer() {
    int bestellnummer = naechsteBestellnummer;
    naechsteBestellnummer = naechsteBestellnummer + 1;
    return bestellnummer;
}

Das Verb „vergeben“ macht klar, dass danach eine Nummer weg ist. Weil die Methode privat ist, kann sie niemand von außen versehentlich aufrufen. Auch der kurze Ausdruck nr++, der zugleich liefert und erhöht, ist zwei verständlichen Zeilen gewichen. Die übrigen Getter heißen jetzt nach ihrem Inhalt: getListe() wird getBestellungen(), getMails() wird getPostausgang().

Die beiden Klassendiagramme fassen den Umbau zusammen. Vorher steht der überraschende Getter öffentlich in der Schnittstelle, die Position hat Ein-Buchstaben-Felder:

UML-Klassendiagramm von Stand 01: Klasse Shop mit Attributen nr, liste, mails, cache und den öffentlichen Operationen getNr, getListe, getMails, abschliessen(l, v, c, m, x); Record Pos mit den Attributen nr, b, p, m, g, r (mit KI erstellt)

Nachher sind die Konstanten benannt, die Nummernvergabe ist privat, und alle Namen erklären sich selbst:

UML-Klassendiagramm von Stand 02: Klasse Shop mit acht statischen Konstanten, den Attributen naechsteBestellnummer, bestellungen, postausgang, mehrwertsteuerCache, der privaten Operation vergibBestellnummer und den öffentlichen Operationen getBestellungen, getPostausgang, abschliessen(positionen, versandart, gutscheincode, kundenEmail, express); Record Position mit artikelnr, bezeichnung, nettopreisCent, menge, gewichtGramm, istRad (mit KI erstellt)

Grenzen und Zielkonflikte: Was überrascht, hängt davon ab, wen Du fragst. Eine erfahrene Python-Entwicklerin erwartet, dass liste.sort() die Liste selbst verändert und None liefert, jemand mit Java-Hintergrund vielleicht nicht. Maßstab sind deshalb die Konventionen der Sprache und des Projekts, nicht das eigene Bauchgefühl. Und getBestellungen() gibt in Stand 02 noch immer die interne Liste heraus; die Kata nimmt diesen Rest in Kauf, weil die Tests darüber prüfen. Wer das ändern will, liefert eine unveränderliche Kopie (List.copyOf(bestellungen) bzw. tuple(self._bestellungen)).

Command-Query-Separation: fragen oder ändern

Das Problem: Der eigentliche Fehler von getNr() ist, dass die Methode zwei Dinge auf einmal tut: Sie beantwortet eine Frage („Welche Nummer ist dran?“) und ändert den Zustand (der Zähler steigt). Wer nur die Antwort braucht, bekommt die Änderung gratis dazu.

Das Prinzip: Bertrand Meyer hat 1988 in „Object-Oriented Software Construction“ die Command-Query-Separation (CQS, Trennung von Kommando und Abfrage) formuliert: Jede Methode ist entweder eine Abfrage (Query), die ein Ergebnis liefert und den beobachtbaren Zustand nicht ändert, oder ein Kommando (Command), das den Zustand ändert und nichts zurückgibt. Meyer fasst das so zusammen: Eine Frage zu stellen, darf die Antwort nicht verändern (Asking a question should not change the answer). Abfragen lassen sich dann beliebig oft, in beliebiger Reihenfolge und auch im Debugger aufrufen, ohne dass etwas passiert.

Der Umbau: Streng nach CQS wird aus getNr() ein Paar aus einer Abfrage und einem Kommando. Das kleine eigene Beispiel zeigt einen Nummernkreis, wie ihn eine Buchhaltung kennt:

// Eigenes Beispiel: Nummernkreis nach Command-Query-Separation
class Bestellnummernkreis {

    private static final int ERSTE_BESTELLNUMMER = 1001;

    private int naechsteNummer = ERSTE_BESTELLNUMMER;

    /** Abfrage: liefert die nächste freie Nummer und ändert nichts. */
    int naechsteNummer() {
        return naechsteNummer;
    }

    /** Kommando: markiert die nächste freie Nummer als vergeben. */
    void nummerVerbrauchen() {
        naechsteNummer = naechsteNummer + 1;
    }

    public static void main(String[] args) {
        Bestellnummernkreis kreis = new Bestellnummernkreis();
        System.out.println(kreis.naechsteNummer());  // 1001
        System.out.println(kreis.naechsteNummer());  // 1001, die Abfrage ändert nichts
        int bestellnummer = kreis.naechsteNummer();
        kreis.nummerVerbrauchen();
        System.out.println(bestellnummer + " vergeben, als Nächstes " + kreis.naechsteNummer());  // 1001 vergeben, als Nächstes 1002
    }
}

Jetzt kann eine Anzeige „nächste Bestellnummer“ die Abfrage so oft aufrufen, wie sie will, ohne dass eine Nummer verloren geht.

Grenzen und Zielkonflikte: Die Methode vergibBestellnummer() in Stand 02 der Kata hält CQS genau genommen nicht ein: Sie ändert den Zähler und liefert die Nummer. Das ist eine bewusste Entscheidung. Die strenge Trennung hat einen Preis: Wer naechsteNummer() aufruft und nummerVerbrauchen() vergisst, vergibt dieselbe Nummer zweimal. Laufen zwei Bestellungen gleichzeitig (in zwei Threads), können beide zwischen Abfrage und Kommando dieselbe Nummer lesen. Für solche Fälle sind kombinierte Methoden üblich und akzeptiert, wenn ihr Name die Änderung ankündigt: Iterator.next() in Java, next() in Python, pop() bei Stapeln und Listen oder getAndIncrement() bei AtomicInteger. Martin Fowler nennt das Entfernen vom Stapel (pop) als Beispiel, bei dem er die Regel bewusst bricht; CQS ist eine starke Regel, aber kein Gesetz. Entscheidend ist, dass niemand überrascht wird: Ein Getter ändert nie etwas, und eine Methode, die etwas ändert, sagt es im Namen. Das ist der Unterschied zwischen getNr() und vergibBestellnummer().

CQS wirkt auch bei Gettern und Settern: Ein Setter ist ein Kommando (void), ein Getter eine Abfrage. Ein Setter, der den alten Wert zurückgibt, oder ein Getter, der „nebenbei“ ein Attribut initialisiert, ist eine Überraschung. Eine Erweiterung des Prinzips auf ganze Architekturen ist Command Query Responsibility Segregation (CQRS), bei der Lese- und Schreibzugriffe über getrennte Modelle laufen; das geht über diesen Artikel hinaus.

Werkzeuge: Linter und Formatierer

Das Problem: Konventionen von Hand zu prüfen, ist mühsam und fehleranfällig. Im Review kostet jede Anmerkung zu Leerzeichen oder Groß- und Kleinschreibung Zeit, die für die eigentlichen Fragen fehlt: Stimmt die Logik? Sind die Namen fachlich richtig?

Das Prinzip: Zwei Arten von Werkzeugen übernehmen das Mechanische:

  • Ein Linter untersucht den Quelltext ohne ihn auszuführen (statische Codeanalyse) und meldet Verstöße gegen Regeln: Schreibweisen, Zeilenlänge, ungenutzte Variablen, magische Zahlen, typische Fehlermuster. Der Name stammt vom Unix-Werkzeug lint, das Stephen C. Johnson 1978 für C-Programme schrieb; es sollte wie ein Flusensieb (lint = Fussel) die kleinen Unsauberkeiten herausholen. Für Java ist Checkstyle (seit 2001) verbreitet, ergänzt durch PMD und SpotBugs; für Python Flake8 (fasst pycodestyle für PEP 8 und Pyflakes für Fehlermuster zusammen) und Ruff (seit 2022, in Rust geschrieben, bündelt die Regeln von Flake8, pep8-naming, Pylint und weiteren und ist sehr schnell).
  • Ein Formatierer prüft nicht nur, sondern schreibt den Code selbst um: Einrückung, Zeilenumbrüche, Leerzeichen. Für Python ist Black (seit 2018, „The uncompromising code formatter“) verbreitet, Ruff bringt mit ruff format einen kompatiblen Formatierer mit; für Java gibt es google-java-format und die Formatierer der Entwicklungsumgebungen.

Der Umbau: Für die Kata reicht eine kleine Konfiguration. Bei Checkstyle legst Du die Regeln in einer XML-Datei fest, bei Ruff in der pyproject.toml des Projekts:

<?xml version="1.0"?>
<!DOCTYPE module PUBLIC "-//Checkstyle//DTD Checkstyle Configuration 1.3//EN"
  "https://checkstyle.org/dtds/configuration_1_3.dtd">
<!-- checkstyle-lesbar.xml: einige Regeln dieses Artikels -->
<module name="Checker">
  <module name="LineLength"><property name="max" value="120"/></module>
  <module name="TreeWalker">
    <module name="TypeName"/>
    <module name="MethodName"/>
    <module name="ConstantName"/>
    <module name="ParameterName"><property name="format" value="^[a-z][a-zA-Z0-9]{2,}$"/></module>
    <module name="LocalVariableName">
      <property name="format" value="^[a-z][a-zA-Z0-9]{2,}$"/>
      <!-- kurze Laufvariablen wie i oder p in Schleifen bleiben erlaubt -->
      <property name="allowOneCharVarInForLoop" value="true"/>
    </module>
    <module name="MagicNumber"/>
  </module>
</module>

Aufgerufen auf Stand 01 meldet Checkstyle 35 Verstöße, darunter (gekürzt):

$ java -jar checkstyle-10.20.1-all.jar -c checkstyle-lesbar.xml src/main/java
Shop.java:10:22: '1001' is a magic number. [MagicNumber]
Shop.java:29:41: Name 'l' must match pattern '^[a-z][a-zA-Z0-9]{2,}$'. [ParameterName]
Shop.java:30:13: Name 's' must match pattern '^[a-z][a-zA-Z0-9]{2,}$'. [LocalVariableName]
Shop.java:57:25: '31500' is a magic number. [MagicNumber]
Shop.java:64:26: '690' is a magic number. [MagicNumber]
Shop.java:102: Line is longer than 120 characters (found 136). [LineLength]
Checkstyle ends with 35 errors.

Für Stand 02 bleiben bei Checkstyle zwölf Meldungen: die Zahlen 50 und 100 der Rundungsformel und der Euro-Formatierung sowie vier Zeilen über 120 Zeichen. Ruff meldet für Stand 02 nur noch zu lange Zeilen, weil die sprechenden Namen länger sind als s und vk. Das ist kein Grund, zu den kurzen Namen zurückzukehren: ruff format bzw. Black brechen die Zeilen automatisch um, oder das Team erlaubt in der Konfiguration längere Zeilen. Die Rundungsformel bekommt im Artikel Refactoring eine eigene Methode.

Am meisten bringen die Werkzeuge, wenn sie automatisch laufen: in der Entwicklungsumgebung beim Tippen, vor jedem Commit (etwa über einen pre-commit-Hook) und in der CI-Pipeline, die eine Änderung mit Verstößen gar nicht erst annimmt.

Grenzen und Zielkonflikte: Die Ausgaben zeigen auch, was die Werkzeuge nicht können. Ruff meldet in dieser Konfiguration weder s noch vk (nur l, weil man es mit 1 und I verwechseln kann), und als magischen Wert nur die 10000 im Vergleich, nicht die 690 in der Rechnung. Checkstyle findet kurze Namen nur, weil die Konfiguration oben mindestens drei Zeichen verlangt; abc würde trotzdem durchgehen. Ob warenwert fachlich der richtige Name ist, ob ein Kommentar stimmt oder ob getNr() heimlich hochzählt, kann kein Linter beurteilen. Zu viele Regeln erzeugen außerdem so viele Meldungen, dass niemand sie mehr liest. Starte mit wenigen Regeln, behebe die Meldungen und verschärfe dann. Linter ersetzen das Review durch Menschen nicht, sie entlasten es (siehe Statische Codeanalyse).

Fazit

Lesbarer Code entsteht nicht durch Genie, sondern durch viele kleine Entscheidungen: einheitliche Schreibweisen nach den Konventionen, Namen, die Absicht und Einheit verraten, Konstanten und Aufzählungen statt magischer Werte, wenige Kommentare, die das Warum erklären, und Methoden, die nicht überraschen. Getter fragen nur ab, Kommandos ändern und sagen es im Namen. Werkzeuge prüfen das Mechanische, über die Bedeutung entscheidest Du.

In der Kata ist Stand 02 jetzt lesbar, aber noch nicht einfach: Der Warenwert wird zweimal berechnet, der Mailtext ist doppelt aufgebaut, ein Cache rechnet um, was gar nicht teuer ist, und der Parameter express wird nie benutzt. Darum geht es im nächsten Artikel, Einfach bleiben. Zum Üben gibt es die Übungsaufgaben zu lesbarem Code.

Leitfragen

  1. Was regeln Source Code Conventions, und wie schreibst Du in Java und in Python Klassen, Methoden, Variablen und Konstanten?

Konventionen regeln Schreibweise der Namen, Einrückung, Zeilenlänge und Anordnung. Klassen in beiden Sprachen UpperCamelCase; Methoden und Variablen in Java lowerCamelCase, in Python snake_case; Konstanten in beiden UPPER_SNAKE_CASE. Einrückung mit 4 Leerzeichen (PEP 8, Sun), Zeilenlänge 79 Zeichen nach PEP 8, 80 bzw. 100 in Java je nach Richtlinie. siehe Abschnitt „Konventionen: einheitlich schreiben“

  1. Woran erkennst Du einen schlechten Namen, und wie verbesserst Du ihn?

Schlechte Namen sind rätselhafte Abkürzungen (vk, mw), Namen mit Datentyp statt Inhalt (liste, strEmail), irreführende Namen (ein Getter, der hochzählt; m für zwei verschiedene Dinge) und Namen ohne Einheit bei Beträgen oder Gewichten. Besser sind Namen aus der Fachsprache, die Absicht und Einheit verraten (versandkosten, nettopreisCent), Substantive für Klassen, Verben für Methoden und Fragen für Wahrheitswerte (istRad). siehe Abschnitt „Sprechende Namen statt Rätsel“

  1. Was ist eine magische Zahl, und wann lohnt sich eine Konstante oder eine Aufzählung?

Ein Zahlen- oder Textwert ohne Namen, dessen Bedeutung sich nicht aus dem Code ergibt (690, 31500, "KURIER10"). Eine Konstante gibt ihm einen Namen und legt ihn an einer Stelle fest; eine Aufzählung (Enum) fasst eine feste Wertemenge wie die Versandarten zu einem eigenen Datentyp zusammen, sodass ungültige Werte früh auffallen. Selbsterklärende Werte wie 0, 1 oder die 100 der Prozentrechnung brauchen keinen Namen. siehe Abschnitt „Magische Zahlen und Zeichenketten benennen“

  1. Welche Kommentare sind sinnvoll, welche solltest Du durch besseren Code ersetzen?

Sinnvoll sind Kommentare, die das Warum erklären, verworfene Alternativen und Warnungen festhalten, Dokumentationskommentare (Javadoc, Docstring) für öffentliche Schnittstellen sowie rechtliche Hinweise. Ersetzt werden Kommentare, die den Code nacherzählen oder einen fehlenden Namen ausgleichen; falsche und veraltete Kommentare, auskommentierter Code und Änderungsprotokolle werden gelöscht. siehe Abschnitt „Kommentare: das Warum erklären“

  1. Was besagt das Prinzip der geringsten Überraschung? Nenne drei typische Verstöße.

Ein Bestandteil soll sich so verhalten, wie Nutzende es nach Name und Umfeld erwarten. Verstöße: Getter mit Seiteneffekt (getNr() zählt hoch), Methodenname widerspricht dem Verhalten (pruefeEmail() verändert die Adresse), unerwartete Rückgabewerte (mal null, mal -1, Arrays mit Positionsbedeutung, interne veränderbare Listen). siehe Abschnitt „Das Prinzip der geringsten Überraschung“

  1. Was bedeutet Command-Query-Separation, und wie teilst Du eine Methode auf, die abfragt und ändert?

Nach Bertrand Meyer (1988) ist eine Methode entweder eine Abfrage, die ein Ergebnis liefert und nichts ändert, oder ein Kommando, das den Zustand ändert und nichts zurückgibt. Eine Methode wie getNr() wird in eine Abfrage naechsteNummer() und ein Kommando nummerVerbrauchen() geteilt. Kombinierte Methoden wie next() oder pop() sind vertretbar, wenn der Name die Änderung ankündigt und die Trennung Fehler (vergessener Aufruf, gleichzeitige Zugriffe) begünstigen würde. siehe Abschnitt „Command-Query-Separation: fragen oder ändern“

  1. Was leisten Linter und Formatierer, und wo liegen ihre Grenzen?

Linter (Checkstyle, Flake8, Ruff) prüfen den Quelltext statisch auf Regelverstöße wie Schreibweisen, Zeilenlänge, ungenutzte Variablen oder magische Werte; Formatierer (Black, ruff format, google-java-format) schreiben Einrückung und Umbrüche selbst um. Sie laufen am besten automatisch in IDE, Commit-Hook und CI. Ob Namen fachlich passen, Kommentare stimmen oder eine Methode überrascht, beurteilen sie nicht; sie entlasten das Review, ersetzen es aber nicht. siehe Abschnitt „Werkzeuge: Linter und Formatierer“


Hinweis zur Nachnutzung als Open Educational Resource (OER)

Dieser Artikel und seine Texte, Bilder, Grafiken, Code und sonstiger Inhalt sind - sofern nicht anders angegeben - lizenziert unter CC BY 4.0. Nennung gemäß TULLU-Regel bitte wie folgt: “Lesbarer Code: Konventionen, sprechende Namen, Kommentare und das Prinzip der geringsten Überraschung” von oer-informatik.de (H. Stein), Lizenz: CC BY 4.0. Der Artikel wurde unter https://oer-informatik.de/lesbarer-code veröffentlicht, die Quelltexte sind in weiterverarbeitbarer Form verfügbar im Repository unter https://gitlab.com/oer-informatik/design-prinzipien/clean-code. Erstellt mit KI-Assistenz; von mir umfassend redaktionell überarbeitet, geprüft und freigegeben. Stand: 10.11.2026.

[Kommentare zum Artikel lesen, schreiben] / [Artikel teilen] / [gitlab-Issue zum Artikel schreiben]

Kommentare gerne per Mastodon, Verbesserungsvorschläge per gitlab issue (siehe oben). Beitrag teilen: