Startseite / Artikel

Lesbares JavaScript am Beispiel: Zehn Refaktorisierungen vor und nach der Umsetzung

Zehn kleine Refaktorisierungen in JavaScript und React – von Namensgebung und Parameterobjekten über Fehlerbehandlung bis hin zur Formatierung –, die es dem nächsten Entwickler erleichtern, den Code zu lesen und zu ändern.

Verlag: REACTAPP.TOP7 Min. Lesezeit1583 Wörter

Quellen: 10 Clean Code Principles I Wish I Knew When I Started Coding

Dass eine Funktion funktioniert, ist die erste Hälfte der Arbeit. Die zweite Hälfte besteht darin sicherzustellen, dass ein Teamkollege – oder man selbst in sechs Monaten – diesen Code lesen, ihn sicher ändern und nicht falsch interpretieren kann. Dieser Artikel zeigt zehn konkrete Beispiele für Refaktorisierungen vor und nach in JavaScript und React, jeweils mit der Begründung dahinter sowie dem Punkt, ab dem die Ratschläge nicht mehr hilfreich sind.

1. Benennen Sie Werte nach dem, was sie darstellen

Kurze, rätselhafte Namen zwingen den Leser dazu, anderswo nachzuschauen. In diesem Auszug verrät nichts, was d ist oder welche Eigenschaft a enthält.

const d = 30;
const x = users.filter(u => u.a);

Durch Umbenennung beider wird die Absicht an dem Ort, an dem sie verwendet werden, sichtbar. Die Zahl wird zu einer Testlänge:

const trialPeriodDays = 30;

Und der Filter liest sich nun wie ein Satz über aktive Benutzer:

const activeUsers = users.filter(user => user.isActive);

Ein guter Name macht klar, was der Wert darstellt; allgemeine Namen wie data sagen fast nichts aus.

const data = getData();

Ein Name, der das jeweilige Konzept beschreibt, ist weitaus informativer:

const customerOrders = getCustomerOrders();

Falls Sie das Bedürfnis verspüren, einen Kommentar hinzuzufügen, um eine Variable zu erklären, ist das in der Regel ein Zeichen dafür, dass der Name diese Information bereits enthalten sollte.

2. Geben Sie jeder Funktion eine einzige Aufgabe

Eine Funktion, die Eingaben validiert, Daten umformt, in eine Datenbank schreibt, E-Mails sendet und Aktivitäten protokolliert, ist schwer nachvollziehbar und noch schwieriger zu testen, da jeder Test alle diese Aufgaben gleichzeitig umsetzen muss. Der untenstehende Überblick zeigt eine solche Funktion mit jeder Aufgabe direkt integriert.

function createUser(user) {
  // validate user
  // format data
  // save to database
  // send email
  // log activity
}

Durch Aufteilung in benannte Schritte wird die Funktion auf höchster Ebene zu einem verständlichen Überblick. Sie beginnt mit der Validierung:

function createUser(user) {
  validateUser(user);

Und es geht weiter mit der Formatierung, dem Speichern sowie der Begrüßungse-Mail – alles wird einer eigenen Funktion zugeordnet:

  const formattedUser = formatUser(user);  saveUser(formattedUser);  sendWelcomeEmail(formattedUser);
}

Nun liest sich createUser() wie eine Liste von Schritten, und jede Hilfsfunktion kann getrennt voneinander getestet werden. Der Nachteil ist die Indirektheit: Ein Leser, der die Details benötigt, muss in jede Hilfsfunktion schauen; daher müssen die Namen ausreichend präzise sein, damit dies nur selten nötig ist.

3. Doppelungen beseitigen, die denselben Änderungsgrund haben

Wiederholte Logik eignet sich zur Extraktion. Hier kommt dieselbe Steuerformel dreimal vor.

const total1 = price1 + price1 * 0.15;
const total2 = price2 + price2 * 0.15;
const total3 = price3 + price3 * 0.15;

Durch die Verlegung in eine Funktion erhält die Regel einen Namen sowie einen eindeutigen Ort:

function calculatePriceWithTax(price) {
  return price + price * 0.15;
}

Jeder Aufrufortpunkt wendet anschließend einfach diese Regel an:

const total1 = calculatePriceWithTax(price1);
const total2 = calculatePriceWithTax(price2);
const total3 = calculatePriceWithTax(price3);

Falls sich der Prozentsatz von 15 % auf 18 % ändert, ändern Sie nur eine Zeile anstelle dessen, dass Sie das gesamte Projekt durchsuchen müssen. Die wichtige Vorsichtsmaßnahme besteht darin, sinnvolle Duplikate zu beseitigen – nicht jedes Paar von Zeilen, das zufällig ähnlich aussieht. Zwei Codeabschnitte, die heute identisch sind, sich aber aus unterschiedlichen Gründen ändern, sollten besser getrennt bleiben; ihr Zusammenführen verknüpft Dinge, die unabhängig voneinander entwickelt werden sollten. Der Artikel zu wann gemeinsamer Code vorhanden sein sollte untersucht diesen Kompromiss ausführlich.

4. Ersetzen Sie lange Positionalparameterlisten durch ein Objekt

Positionale Argumente verbergen ihre Bedeutung am Aufrufort. Wenn man sich diesen Aufruf ansieht, kann man ohne Öffnen der Funktionseinheit nicht erkennen, was 25 oder true bedeuten.

createUser(
  "Shayan",
  "shayan@example.com",
  25,
  "developer",
  true,
  "Pakistan"
);

Durch die Übermittlung eines einzigen Objekts mit benannten Eigenschaften wird jeder Wert selbstbeschreibend:

createUser({
  name: "Shayan",
  email: "shayan@example.com",
  age: 25,
  role: "developer",
  isActive: true,
  country: "Pakistan"
});

Dieser Stil ist bereits in API-Payloads, Konfigurationsobjekten und React-Props üblich, wodurch er in größeren Anwendungen oft als natürlich empfunden wird.

5. Bevorzugen Sie offensichtlichen Code vor cleverem Code

Die Komprimierung von Logik in eine Zeile kann die Lesbarkeit erschweren. Die folgende Zeile verknüpft optionale Aufrufe über abgekürzte Eigenschaftsnamen.

const result = users?.filter(u => u.a)?.map(u => u.n);

Durch Aufteilung in zwei Schritte mit beschreibenden Namen wird jede Transformation klar dargestellt. Zuerst werden die aktiven Benutzer ausgewählt:

const activeUsers = users.filter(user => user.isActive);

Dann werden ihre Namen extrahiert:

const activeUserNames = activeUsers.map(user => user.name);

Es kostet eine zusätzliche Zeile, aber ein Leser versteht es auf den ersten Blick. Beachten Sie einen Verhaltensunterschied: Die ursprüngliche Version nutzte optional chaining, wodurch sie es zuließ, dass users den Wert null oder undefined hatte, während die überarbeitete Version in diesem Fall einen Fehler auslöst. Wenn users tatsächlich fehlen kann, sollte dies explizit behandelt werden (zum Beispiel mit einem Standardwert von []) anstatt es in einer Kette von ?.-Operatoren zu verbergen. Das Leitprinzip ist es, den Code verständlich zu machen – nicht beeindruckend.

6. Planen Sie für Fehler, nicht nur für den erfolgreichen Ablauf

Echte Systeme versagen: APIs liefern Fehlermeldungen, Benutzer geben ungültige Eingaben ein, Netzwerke brechen zusammen, Datenbanken gehen offline. Der folgende Code geht davon aus, dass alles erfolgreich verläuft.

const response = await fetch("/api/users");
const users = await response.json();

Ein wichtiger Punkt ist, dass fetch nur bei Netzwerkfehlern abgelehnt wird; ein HTTP 404 oder 500 wird dennoch erfolgreich verarbeitet. Die verbesserte Version umschließt die Anfrage in einen try-Block:

try {
  const response = await fetch("/api/users");

und prüft anschließend response.ok, wirft bei einem fehlerhaften Status einen Fehler aus, parsiert den Inhalt und fängt alle Fehler auf:

  if (!response.ok) {
    throw new Error("Failed to fetch users");
  }  const users = await response.json();  return users;
} catch (error) {
  console.error("Unable to load users:", error);
}

Dadurch werden Fehler sichtbar und es fällt viel leichter, sie zu debuggen. Beachten Sie, dass der catch-Block hier protokolliert und anschließend implizit undefined zurückgibt, sodass Aufrufer keinen Unterschied zwischen einem Fehler und einem leeren Ergebnis erkennen können. In echtem Code sollte entweder nach dem Protokollieren erneut ein Fehler geworfen oder ein expliziter Wert zurückgegeben werden, den der Aufrufer überprüfen kann, damit der Fehler dort behandelt wird, wo er tatsächlich bearbeitet werden kann.

7. Kommentieren Sie das Warum, nicht das Was

Kommentare sind nützlich; überflüssige Kommentare sind nur Störgeräusche. Dieser Kommentar wiederholt genau das, was die Zeile tut.

// Increment counter by 1
counter++;

Ein guter Kommentar erklärt eine Entscheidung, die der Code allein nicht ausdrücken kann – beispielsweise warum eine Anfrage absichtlich verzögert wird:

// Delay the request to avoid triggering the API rate limit.
setTimeout(fetchUsers, 1000);

Ein praktisches Prinzip: Lassen Sie den Code ausdrücken, was er tut, und verwenden Sie Kommentare, um zu erklären, warum er es tut.

8. Halten Sie Geschäftsregeln aus JSX raus

In React-Komponenten ist es einfach, Filterung und Berechnungen direkt in die Markup-Struktur einzubauen. Diese Render-Ausgabe wendet zwei Filter inline an, bevor über die Produkte abgelaufen wird.

<div>
  {products
    .filter(product => product.stock > 0)
    .filter(product => product.price < 100)
    .map(product => (
      <ProductCard product={product} />
    ))}
</div>

Durch das Verschieben der Regel in eine benannte Variable wird getrennt, was als verfügbares Produkt gilt, von der Art und Weise, wie die Produkte angezeigt werden:

const availableProducts = products.filter(
  product => product.stock > 0 && product.price < 100
);

Das JSX beschreibt dann nur die Benutzeroberfläche:

<div>
  {availableProducts.map(product => (
    <ProductCard key={product.id} product={product} />
  ))}
</div>

Beachten Sie, dass die überarbeitete Version auch eine key-Eigenschaft hinzufügt, die im Original fehlte und die React benötigt, um die Listeneinträge korrekt zu verfolgen.

9. Lassen Sie Tools eine einheitliche Formatierung durchsetzen

Die Formatierung erscheint unbedeutend, solange eine Codebasis verschiedene Stile mischt. Hier wird dieselbe Variable von drei Personen auf drei verschiedene Arten deklariert: camelCase,

const userName = "Shayan";

snake_case,

const user_name = "Shayan";

sowie PascalCase ohne Leerzeichen oder Semikolon:

const UserName="Shayan"

Alle drei Varianten funktionieren, doch die Inkonsistenz verursacht jedes Mal Probleme, wenn jemand den Code liest oder darin sucht. Tools wie Prettier und ESLintbeseitigen dieses Problem: Prettier formatiert automatisch, während ESLint Namenskonventionen durchsetzen und häufige Fehler aufzeigen kann.

10. Schreiben Sie so, dass es auch der Nächste verstehen kann

Dieses Prinzip verbindet alle anderen miteinander. Die nächste Person, die Ihre Datei öffnet, könnte ein neuer Kollege, ein Teamkollege oder Ihr zukünftiges Ich sein – und Ihr zukünftiges Ich hat bereits alles vergessen, was Sie heute gedacht haben.

Vor dem Commit fragen Sie sich, ob Sie die Datei auch nach sechs Monaten ohne Vorkenntnisse verstehen würden. Falls nicht, vereinfachen Sie sie: geben Sie verwirrenden Variablen neue Namen, teilen Sie große Funktionen auf, entfernen Sie Duplikate, bei denen ein gemeinsamer Grund für Änderungen besteht, und straffen Sie die Struktur. Das grundlegende Ziel ist es, die Anzahl der Gedanken zu verringern, die nötig sind, um den Code zu verstehen. Weitere Gewohnheiten im selben Geiste finden Sie unter Gewohnheiten, die es dem nächsten Entwickler erleichtern, Code zu ändern.

Zusammenfassung

Sauberer Code bedeutet nicht perfekten Code. Fristen, sich ändernde Anforderungen und entwickelnde Projekte bedeuten, dass jedes Team etwas unübersichtlichen Code schreibt. Entscheidend ist die Richtung: Jede Änderung sollte den Code etwas klarer, einfacher und wartbarer machen.

  • Die Namen sollten angeben, was ein Wert darstellt.
  • Funktionen sollten eine einzige Aufgabe erfüllen, und Duplikationen sollten nur dann beseitigt werden, wenn sie denselben Anlass für eine Änderung haben.
  • Objekte sind besser als lange Listen mit Positionsparametern.
  • Lesbarkeit ist wichtiger als Kürze, halten Sie das Verhalten jedoch beim Umschreiben unverändert.
  • Behandeln Sie Fehler explizit und kommentieren Sie Entscheidungen statt die Mechanismen.
  • Lassen Sie Geschäftsregeln außerhalb der Markup-Struktur – überlassen Sie die Formatierung den Tools.