Code ohne Kommentare ist schlechter Code


Code ohne Kommentare ist schlechter Code

Ich möchte heute mit dir über Kommentare im Code sprechen. Dieses Thema ist ein heiß diskutiertes Thema, und ich selbst musste mich schon häufig für meine Verwendung von Kommentaren rechtfertigen. Ich habe es sogar schon erlebt, dass Pull Requests wegen meiner Art von Kommentaren abgelehnt worden sind.

Lass uns also die These “Code ohne Kommentare ist schlechter Code” betrachten.

Was wird an meinem Kommentarstil kritisiert? In der Regel die fast komplette Abwesenheit von Kommentaren.

Code muss sich selbst erklären

Ich bin klarer Verfechter der Theorie, dass Code sich selbst erklären soll. Wenn du deinen Code sauber strukturierst und vernünftige Bezeichner wählst, dann wirst du fast ohne Kommentare auskommen.

Lass uns dazu zunächst einmal ein paar offensichtlich überflüssige Kommentare betrachten:

function test() {
  let x = 1; // Lege Variable x auf 1 fest
}

Sowas habe ich auch bei erfahrenen Entwicklern schon gesehen. Was ist der Sinn davon?

/**
 * Ermittelt die Anzahl der Produkte in dem Array products
 */
function getProductCount(products: Array<Product>) {
  return products.length;
}

Wenn du davon überrascht bist, dass eine Funktion mit dem Namen getProductCount die Anzahl der Produkte zurückgibt, solltest du dir Fragen stellen, die sich nicht mit einem Kommentar beantworten lassen.

Komplexer Code muss erklärt werden

Schauen wir uns das folgende Beispiel an:

const names = Object.entries(products).filter(([_, p]) => p.status === 0).reduce((acc, [sku, p]) => acc.concat([sku, p.name].join('-')), []);

In dem Moment, als ich das geschrieben habe, war für mich klar, dass hier eine Liste von Produkten erzeugt wird, die den Status “0” haben. Die Liste beinhaltet eine Verkettung auf SKU und Name von Produkten. Ich muss aber zugeben, dass ich in ein paar Wochen vermutlich einen Moment brauchen würde, um das zu erkennen. Aber auch hier finde ich den Einsatz von Kommentaren nicht wirklich sinnvoll. Eher solltest du überdenken, wie du deinen Code lesbarer gestalten könntest. Schau dazu das folgende Beispiel an:

// Vorschlag 1:
const names = Object.entries(products)
  .filter(([_, p]) => p.status === 0)
  .reduce((acc, [sku, p]) => acc.concat([sku, p.name].join('-')), []);


// Vorschlag 2:

const productEntries = Object.entries(products);
const disabledProducts = productEntries.filter(([_, p]) => p.status === 0);
const names = []
for (const [sku, product] of disabledProducts) {
  names.push(`${sku}-${product.name}`);
}

Vorschlag 1 verschafft allein durch das Hinzufügen von Umbrüchen deutlich mehr Struktur. Mit Variante 2 teilen wir die verketteten Array-Funktionen und ihre Callbacks in „klassischeren“ Code auf. Durch die Verwendung geeigneter Variablen für Zwischenergebnisse machst du dir hier das Leben einfacher und machst den Kommentar überflüssig.

Wenn du auch bei Variante 2 Schwierigkeiten hast, bringst du vermutlich noch nicht genug Erfahrung mit und wirst von der Syntax herausgefordert. Befindest du dich in einem privaten Projekt, das nur für dich als Training gedacht ist, dann kannst du natürlich gerne Kommentare verwenden, um für dich schwierige Syntax zu erklären.

Lege aber deinen Fokus zusätzlich darauf, Code zu lesen und zu verstehen. Du wirst nur sinnvoll und langfristig an Projekten arbeiten können, wenn du Code lesen und verstehen kannst. Dabei sind die Syntax und die grundlegenden Funktionen der Sprache das A und O.

Warum Kommentare vermeiden?

Du könntest dich fragen, warum wir Kommentare vermeiden sollen, wenn sie gerade Einsteigern helfen könnten. Nun, das ist einfach: Überflüssige Kommentare hindern erfahrene Entwickler bei der Arbeit und machen meiner Meinung nach Code schwerer lesbar.

Es ist eine entscheidende Fähigkeit, Code nur durch kurzes Betrachten verstehen zu können. Wenn du eine Datei öffnest, zum Beispiel für den Review, musst du dir zunächst einen Überblick verschaffen. Dazu wirst du mit etwas Erfahrung nicht jede Zeile und jede Anweisung einzeln lesen. Gängige Konstrukte haben durch die starre Syntax von Programmiersprachen sehr häufig eine ähnliche Struktur, womit es einfach ist, diese wiederzuerkennen.

Die deutsche oder generell natürliche Sprache hat eine sehr viel weniger strenge Syntax, was die Erkennung von Inhalten auf Basis der Struktur deutlich erschwert.

Ich habe mal zwei Bilder für dich erstellt. Beide zeigen denselben Inhalt. Es kommt zunächst etwas Code und dann ein längerer Kommentar, der den Code in natürlicher Sprache beschreibt.

Für die erste Variante habe ich etwas Blur über das Bild gelegt:

Blur

Auch wenn du vermutlich keinen der Variablennamen lesen kannst, wirst du bestimmt einige Konstrukte im Code wiedererkennen, oder? Aber könntest du den Inhalt des Kommentars lesen?

Zur Auflösung nochmal das Bild ohne Blur:

Blur

Querlesen ist einfach unglaublich wichtig, aber bei Kommentaren nicht möglich. Und je mehr Kommentare im Code enthalten sind, desto mehr können diese auch stören.

Keine Kommentare sind auch keine Lösung

Wenn du jetzt denkst, dass du einfach alle Kommentare weglassen könntest, kennst du mich vermutlich noch nicht genug. Es gibt Bereiche, in denen Kommentare sinnvoll sind und in denen wir sie auf jeden Fall einsetzen sollten.

Lass uns jetzt genauer betrachten, wann Kommentare in deinem Code einen Mehrwert bieten.

Dafür stellen wir uns das folgende Beispiel vor: Du willst eine Umkreissuche für Geocaching implementieren. Dazu sollen alle Geocaches in einem Kreis rund um deinen Standort ermittelt werden. Da die Erde bekanntermaßen keine Scheibe ist, können wir die Haversine-Formel zur ungefähren Berechnung der Distanz zwischen zwei Koordinaten verwenden. Wir betrachten die folgende Funktion und ihre Kommentare:

/**
 * Berechnet die Entfernung zwischen zwei Punkten auf der Erde anhand der Haversine-Formel.
 * 
 * Formel:
 * a = sin²(Δφ/2) + cos(φ1) * cos(φ2) * sin²(Δλ/2)
 * c = 2 * atan2(√a, √(1−a))
 * distance = R * c
 * 
 * @returns Entfernung in Kilometern
 */
getHaversineDistance(p1: Coordinate, p2: Coordinate): number {
  // Radius der Erde in Kilometern
  const EARTH_RADIUS_KM = 6371;

  // Koordinaten von Grad in Bogenmaß umwandeln
  const start = { lat: this.toRad(p1.lat), lng: this.toRad(p1.long) };
  const end = { lat: this.toRad(p2.lat), lng: this.toRad(p2.long) };

  const deltaLat = end.lat - start.lat;   // Δφ
  const deltaLng = end.lng - start.lng;   // Δλ

  // Haversine-Formel: a = sin²(Δφ/2) + cos(φ1) * cos(φ2) * sin²(Δλ/2)
  const haversine = Math.sin(deltaLat / 2) ** 2
      + Math.cos(start.lat) * Math.cos(end.lat) * Math.sin(deltaLng / 2) ** 2;

  // Zentralwinkel c = 2 * atan2(√a, √(1−a))
  const centralAngle = 2 * Math.atan2(Math.sqrt(haversine), Math.sqrt(1 - haversine));

  // Entfernung entlang der Erdoberfläche: distance = R * c
  return EARTH_RADIUS_KM * centralAngle;
}

Dafür, dass ich keine Kommentare verwenden möchte, ist es hier reichlich voll geworden. Findest du das in diesem Beispiel sinnvoll?

Der Doc Block

Zunächst der Doc Block vor der Funktion. Dieser enthält zu Beginn eine Beschreibung, was die Funktion tut. Die Beschreibung finde ich an dieser Stelle hilfreich, weil wir die Haversine-Formel nicht als Allgemeinwissen unterstellen können. Somit könnte dieser Kommentar als Erklärung für den Verwender in diesem konkreten Beispiel hilfreich sein. Wichtig ist hier die Information, dass wir Punkte auf der Erde miteinander vergleichen.

Im Doc Block ist zusätzlich die Formel angegeben, die hilft, den danach folgenden Code zu verstehen. Den Rückgabewert definiere ich, weil wir hier angeben können, um welche Einheit es sich handelt. Das ist für den Verwender eine wichtige Information und könnte auch aus dem Code nicht unmittelbar abgeleitet werden.

Generell sind Doc-Blöcke immer dann sinnvoll, wenn du APIs oder Libraries bereitstellst. Diese Blöcke können in der Regel von der IDE gelesen werden und helfen den Benutzer zu verstehen, was die Funktion tut. Wenn allerdings offensichtlich ist, was eine Funktion tut, solltest du auf den Block verzichten.

Konstanten

// Radius der Erde in Kilometern

Wäre hier nicht zwingend notwendig, ich habe ihn für dieses Beispiel aber trotzdem hinzugefügt. Wenn du Konstanten definierst, sollte der Name so spezifisch sein, dass der Kommentar wie in diesem Fall überflüssig ist. Es wird aber immer wieder Situationen geben, in denen das nicht ohne Weiteres möglich ist. Hier hilft der Kommentar, den festgelegten Wert zu verstehen.

Komplexität

  // Haversine-Formel: a = sin²(Δφ/2) + cos(φ1) * cos(φ2) * sin²(Δλ/2)
  const haversine = Math.sin(deltaLat / 2) ** 2
      + Math.cos(start.lat) * Math.cos(end.lat) * Math.sin(deltaLng / 2) ** 2;

In diesem konkreten Fall ist die Syntax von TypeScript für den Ausdruck einer Berechnung dieser Art nicht sinnvoll, da sie die Komplexität erhöht. Mathematische Funktionen sind hier einfach einfacher zu lesen.

Wann solltest du kommentieren?

Zu dem oben genannten Beispiel würde ich noch Workarounds bzw. bewusste Fehler hinzufügen wollen. Wenn du also aufgrund von anderem Code oder aufgrund der Umgebung einen bewussten Fehler in deinen Code einbaust, solltest du das mit einem Kommentar kennzeichnen. So bekommt dein Nachfolger keine Probleme, wenn er deinen vermeintlichen Fehler korrigiert.

Immer erlaubt sind natürlich Kommentare, die unfertigen Code kennzeichnen. Ein @TODO oder ähnliches kannst du verwenden, um Stellen zu kennzeichnen, mit denen du noch nicht fertig bist.

Fazit

Ich würde, wie so oft, sagen, die Welt ist nicht Schwarz/Weiß, sondern hat viel mehr Schattierungen dazwischen. Verwendest du keine Kommentare, erschwerst du in manchen Bereichen das Lesen, verwendest du jedoch zu viele Kommentare, hat es denselben Effekt.

Ich würde in der Regel bevorzugen, den Code so zu gestalten, dass der Kommentar überflüssig wird, und die Kommentare dort zu platzieren, wo sie einen echten Mehrwert liefern.

Also zum Beispiel für:

  • APIs / Dokumentation
  • Komplexer Code
  • Konstantenwerte
  • Workarounds / bewusste Fehler

Aber wie so oft in der Entwicklung ist der passende Kommentarstil etwas, das sich entwickeln muss. Meine klare Devise: Weniger ist mehr!