Sieben Gewohnheiten, die es dem nächsten Entwickler erleichtern, Code zu ändern
Schutzklauseln, namen, die den Zweck offenbaren, API-Grenzen, präzise Typen, reine Entscheidungsfunktionen, strukturierte Fehler sowie fokussierte Pull-Requests – mit JavaScript-Beispielen.
Quellen: 7 Coding Habits That Separate Senior Developers From the Rest
Das Erlernen eines weiteren Frameworks bringt letztendlich nicht dazu, dass man zu einem deutlich besseren Entwickler wird. Was wirklich Früchte trägt, ist die Art und Weise, wie man Code schreibt und ändert: Erfahrene Entwickler erstellen oft weniger Code mit klarerer Absicht, der leichter zu lesen, zu testen und anzupassen ist, wenn sich die Anforderungen ändern. Dieser Artikel zeigt sieben konkrete Gewohnheiten, jeweils mit einem Beispiel in JavaScript oder TypeScript vor und nach der Anwendung, sowie die spezifischen Vorteile, die jede dieser Gewohnheiten bringt.
Keine dieser Gewohnheiten erfordert ein neues Tool. Es handelt sich dabei um Entscheidungen, die man trifft, während man eine Funktion schreibt, eine Variable benennt, Daten modelliert oder einen Pull Request öffnet.
1. Den „happy path“ mit Schutzklauseln sichtbar halten
Nehmen wir an, eine Funktion muss einen Benutzer suchen, prüfen, ob das Konto aktiv ist, ob der Benutzer Berechtigungen hat, und erst danach eine Aktion ausführen. Ein erster Entwurf würde normalerweise eine Bedingung in eine andere einbetten:
function performAction(userId) {
const user = findUser(userId);
if (user) {
if (user.isActive) {
if (user.hasPermission) {
return executeAction(user);
}
}
}
return null;
}
Es funktioniert, doch die eigentlich wichtige Operation befindet sich drei Ebenen tief, und der Fallback return null ganz unten ist visuell nicht mit den Bedingungen verbunden, die zu ihm führen.
Die Alternative besteht darin, ungültige Fälle sofort mit Schutzklauseln abzulehnen, sodass jede fehlgeschlagene Voraussetzung in ihrer eigenen Zeile endet. (Die Einrückung in diesem Auszug ist ungleichmäßig; alle vier Anweisungen nach der Abfrage gehören auf derselben Ebene.)
function performAction(userId) {
const user = findUser(userId);
if (!user) return null;
if (!user.isActive) return null;
if (!user.hasPermission) return null;
return executeAction(user);
}
Nun verläuft der Hauptweg in gerader Linie: Überprüfen, überprüfen, überprüfen, handeln. Das bedeutet nicht, dass verschachtelte Logik immer falsch ist. Wenn Zweige tatsächlich unterschiedliche Ergebnisse darstellen und nicht nur Voraussetzungen sind, kann if/else die klarste Form sein. Ziel ist es, zu verhindern, dass wesentliche Geschäftslogik unter Bedingungen begraben wird, die eigentlich nur als Zugangskontrolle dienen.
Die Gewohnheit: Zuerst mit Ausnahmefällen umgehen und den normalen Ablauf unverändert lassen. Jeder, der die Funktion aufruft, sollte erkennen können, was normalerweise passiert, ohne fünf Ebenen von Klammern verfolgen zu müssen. Ein zusätzlicher Vorteil ist, dass jede Schutzfunktion später ein präziseres Ergebnis oder einen spezifischeren Fehler zurückgeben kann, was in der verschachtelten Version nur schwer sauber umsetzbar ist.
2. Benennen Sie Dinge nach ihrem Zweck
Nur wenige Änderungen verbessern die Lesbarkeit so kostengünstig wie bessere Namen. Schauen Sie sich dieses Paar von Zeilen an:
const data = getOrder();
process(data);
Was ist data, und was macht process damit? Man muss dazu in anderen Code schauen, um es herauszufinden. Vergleichen Sie:
const pendingOrder = getPendingOrder();
processOrder(pendingOrder);
Die gleichen beiden Zeilen erklären nun, um welche Art von Reihenfolge es geht und was mit ihr geschieht. Gute Namen verringern die geistige Anstrengung, die zum Verstehen einer Codebasis erforderlich ist, denn der Leser muss den Sinn nicht mehr aus dem Kontext rekonstruieren.
Funktionsnamen verdienen dieselbe Sorgfalt. Ein generisches Verb wie dieses versteckt das Verhalten:
handle(order);
während diese Version es klar ausdrückt:
calculateOrderTotal(order);
Ebenso lässt ein einfaches send unklar, was gesendet wird und an wen:
send(order);
ein beschreibender Name beantwortet beide Fragen:
sendOrderConfirmation(order);
Lange Namen sind nicht das Ziel; wichtig sind Namen, die die Absicht vermitteln. Ein nützlicher Nebeneffekt ist, dass vage Namen oft zu vagen Funktionen führen. Wenn handle schwer umzubenennen ist, könnte es mehrere unverwandte Aufgaben erledigen und sollte daher aufgeteilt werden.
Die Gewohnheit: Bevor man sich auf einen Namen einigt, fragt man sich, ob jemand, der nur diese Zeile liest und sonst nirgendwo hinschaut, verstehen würde, was der Wert oder die Funktion darstellt. Wenn nicht, sollte man weiter nach einem besseren Namen suchen.
3. Externe Systeme hinter einer Grenze halten
Anwendungen kommunizieren mit vielen externen Systemen: Zahlungsdienstleistern, E-Mail-Diensten, Cloud-Speicherlösungen, Identitätsanbietern, APIs Dritter sowie internen Microservices. Ein häufiger Fehler besteht darin, die Antwortformate dieser Systeme direkt in den Geschäftskodex einzubinden:
const customer = await stripeCustomer.get();
console.log(customer.email_address);
console.log(customer.customer_name);
Jede Zeile, die email_address oder customer_name liest, hängt nun von den genauen Feldnamen des Anbieters ab. Wenn der Anbieter ein Feld umbenennt oder seine Antwortstruktur ändert, hat das Auswirkungen auf den gesamten Codebase.
Die Lösung besteht in einer schlichten Grenzfunktion, die den externen Dienst aufruft und die Antwort in ein vom eigenen Anwendungsmodell definiertes Format übersetzt:
async function getCustomer(customerId) {
const response = await externalService.getCustomer(customerId);
return {
email: response.email_address,
name: response.customer_name
};
}
Der Rest des Codes verwendet dieses interne Format und hat nie Kenntnis von den Namenskonventionen des Anbieters:
const customer = await getCustomer(customerId);
console.log(customer.email);
console.log(customer.name);
Wenn sich die externe API ändert, muss nur die Zuordnung innerhalb von getCustomer aktualisiert werden. Derselbe Bereich ist außerdem der geeignete Ort, um die Antwort zu validieren, fehlende Werte zu normalisieren und bei Tests künstliche Werte einzusetzen. Das ist die Idee hinter Adaptern und Schutzschichten in größeren Architekturen – angewandt im Maßstab einer einzelnen Funktion.
Die Gewohnheit: externe Komplexität an den Rändern halten. Die Geschäftslogik sollte nicht über die Besonderheiten jeder Drittanbieter-Dienstleistung Bescheid wissen, und diese Trennung wird umso wertvoller, je mehr Integrationen es gibt.
4. Ungültige Zustände schwer darstellbar machen
Eine verlockende Möglichkeit, ein Modell zu definieren, besteht darin, jedes Feld optional zu machen – schließlich ist das schnell und führt niemals zu Kompilierfehlern:
type Order = {
id?: string;
paymentId?: string;
status?: string;
};
Die Kosten treten überall dort auf, wo das Modell verwendet wird. Jeder Verbraucher muss vorsorglich prüfen, denn nichts garantiert, dass ein bestimmtes Feld vorhanden ist:
if (order.id) {
// ...
}
if (order.paymentId) {
// ...
}
if (order.status === "paid") {
// ...
}
Das tiefere Problem liegt nicht in der Umfangreiche Darstellung. Der Typ sagt nichts darüber aus, welche Kombinationen von Feldern gültig sind. Eine Bestellung mit einer Zahlungs-ID, aber ohne eigentliche ID, oder ein Status von "paid" ohne tatsächliche Zahlung, sind aus Sicht des Compilers alle zulässig.
Ein präziseres Modell beschreibt einen spezifischen Zustand:
type PaidOrder = {
id: string;
paymentId: string;
status: "paid";
};
Ein Wert vom Typ PaidOrder garantiert drei Dinge:
- er hat eine ID
- er hat eine Zahlungs-ID
- sein Status ist exakt
"paid"
Der Typ dient nun gleichzeitig als Dokumentation und als Sicherheitsnetz zur Laufzeit-Kompilierung. In der Praxis würde man für jeden Zustand einen eigenen Typ definieren, beispielsweise eine ausstehende Bestellung ohne Zahlungs-ID und eine bezahlte Bestellung mit einer, und diese in einer diskriminierten Union anhand von status zusammenfassen. TypeScript kann anschließend nach einer Überprüfung wie order.status === „paid“ den Typ automatisch einengen und Sie warnen, wenn ein neuer Zustand nicht behandelt wird. Ein funktionierendes Beispiel für dieses Muster finden Sie unter Ersatz von Ladeindikatoren durch eine diskriminierte Union.
Die Gewohnheit: Verwenden Sie keine optionalen Felder einfach deshalb, weil sie die Erstellung der ersten Version erleichtern. Klären Sie erst, welche Zustände tatsächlich möglich sein sollten, und gestalten Sie Ihre Typen entsprechend. Das beseitigt zwar nicht jeden Fehler, macht aber die Erstellung einer ganzen Klasse von fehlerhaften Daten erheblich schwieriger.
5. Entscheidungen von Aktionen trennen
Business-Regeln sind schwer zu testen, wenn sie mit Nebeneffekten verknüpft sind. Nehmen wir eine Funktion, die nur von verifizierten Erwachsenenbenutzern genutzt werden darf:
async function processUser(userId) {
const user = await database.getUser(userId);
if (user.isVerified && user.age >= 18) {
await database.enableFeature(userId);
await emailService.send(user.email);
}
}
Die Regel selbst – nämlich die Verifizierung des Alters von mindestens 18 Jahren – wird mit einer Datenbankabfrage, einer Datenbankschreiboperation und dem Versand einer E-Mail vermischt. Um die Regel zu testen, muss man alle drei Komponenten emulieren.
Nehmen Sie die Entscheidung in eine reine Funktion aus, die Daten entgegennimmt und ein Boolesches Wert zurückgibt:
function canUseFeature(user) {
return user.isVerified && user.age >= 18;
}
Die Orchestrierungsfunktion holt anschließend den Benutzer ab, stellt die Regel zur Überprüfung und führt nur dann Nebeneffekte aus, wenn diese bestanden wird:
async function processUser(userId) {
const user = await database.getUser(userId);
if (!canUseFeature(user)) {
return;
}
await database.enableFeature(userId);
await emailService.send(user.email);
}
Die Regel kann nun direkt mit einem einfachen Objekt getestet werden:
expect(
canUseFeature({
isVerified: true,
age: 25
})
).toBe(true);
Keine Datenbank, kein E-Mail-Service, keine Mocks – nur die Entscheidung und ihre Eingaben. Dadurch ist es einfach, Randfälle wie einen unverifizierten 30-Jährigen oder einen verifizierten Benutzer, der genau 18 Jahre alt ist, abzudecken.
Die Gewohnheit: Trenne „Sollten wir das tun?“ von „Wie tun wir es?“. Gute Kandidaten für diese Trennung sind:
- Zugriffs- und Berechtigungsprüfungen
- Preisberechnungen
- Eingabenauswertung
- Entscheidung, ob jemand benachrichtigt werden soll
- Wiederholungsstrategien
- Eignungskriterien und Feature-Flag-Regeln
Je wichtiger eine Regel für das Unternehmen ist, desto mehr profitiert es davon, in einer kleinen, reinen, gut getesteten Funktion untergebracht zu sein.
6. Machen Sie Fehler für Menschen und Software nützlich
Ein Fehler wie dieser liefert fast nichts zum Arbeiten:
throw new Error("Something went wrong");
Er gibt nicht an, was fehlgeschlagen ist, wo, warum oder ob der Aufrufer etwas dagegen unternehmen kann.
Ein besserer Fehler enthält einen stabilen Code zusammen mit einer für Menschen lesbaren Nachricht. Das Beispiel geht von einer AppError-Klasse aus, die Error erweitert und beide Werte speichert; es handelt sich dabei nicht um eine eingebaute Klasse.
throw new AppError(
"PAYMENT_FAILED",
"The payment could not be completed."
);
Wenn er für eine API-Antwort serialisiert wird, bietet er den Clients etwas Vorhersehbares:
{
"code": "PAYMENT_FAILED",
"message": "The payment could not be completed."
}
Die wichtige Unterscheidung, die man im Gedächtnis behalten sollte: Nachrichten sind für Menschen geschrieben, Code hingegen für Systeme. Nachrichten können umformuliert oder übersetzt werden, doch Codewerte sollten stabil bleiben, damit Clients darauf sicher aufbauen können. Ein Frontend kann beispielsweise auf einen bestimmten Fehler reagieren:
if (error.code === "PAYMENT_FAILED") {
showPaymentError();
}
Auch Logs verdienen dieselbe Behandlung. Eine einfache Nachricht ist um 3 Uhr morgens schwer zu handhaben:
console.log("Payment failed");
Ein strukturierter Kontext macht die Einträge suchbar und diagnostizierbar:
logger.error("Payment failed", {
orderId,
provider,
attempt
});
Siezen Sie sorgfältig aus, was in diesen Kontext aufgenommen wird. Protokollieren Sie niemals:
- Passwörter
- Zugriffstoken
- API-Schlüssel
- Weitere Geheimnisse
- Sensible personenbezogene Daten
Falls Sie eine Standardstruktur für HTTP-Fehlerkörper benötigen, lohnt es sich, einen Blick in die Problembeschreibungen von RFC 9457 zu werfen.
Die Gewohnheit: Jeder Fehler muss in eine Form gebracht werden, die ein Mensch lesen und ein Programm darauf reagieren kann. Das Werfen des Fehlers ist der einfache Teil; die eigentliche Arbeit besteht darin, Fehler vorhersehbar und schnell diagnostizierbar zu machen.
7. Halten Sie jede Änderung fokussiert
Die letzte Gewohnheit bezieht sich auf das Ändern von Code statt auf sein Schreiben. Stellen Sie sich einen einzigen Pull Request vor, der Folgendes enthält:
- eine neue Auscheck-Funktion
- eine Datenbank-Migration
- eine Refaktorierung des Zahlungsdienstes
- Updates am Frontend
- Änderungen an der Wiederholungslogik
- mehrere unverwandte Fehlerbehebungen
Auch wenn alles funktioniert, ist es sehr schwierig, diesen Pull Request gründlich zu überprüfen. Prüfer scannen nur oberflächlich, Risiken verstecken sich im Chaos, und wenn etwas in der Produktion ausfällt, bedeutet das Zurücksetzen eines Teils auch das Zurücksetzen von allem.
Wenn die Änderungen in fokussierte Teile aufgeteilt werden, sieht derselbe Arbeitsumfang so aus:
PR #1 → Add checkout validation
PR #2 → Refactor payment service
PR #3 → Update checkout UI
PR #4 → Improve payment retry logic
Jeder dieser Teile ist leichter zu:
- überprüfen
Fokussierte Änderungen unterstützen auch die Zusammenarbeit: Wenn es zu einer Regression kommt, ist der Bereich zur Untersuchung klein, und Tools wie git bisect weisen auf eine Änderung hin, die für sich genommen sinnvoll ist.
Die Gewohnheit: Bevor man eine weitere Änderung in die aktuelle Aufgabe einbindet, fragen, ob sie zum selben Problem gehört. Wenn nicht, sollte sie einen eigenen Commit oder Pull Request erhalten. Eine fokussierte Änderung ist vertrauenswürdiger als eine große, die zufällig die Tests besteht.
Der gemeinsame Nenner: Die nächste Änderung erleichtern
Zu Beginn scheinen diese Gewohnheiten unzusammenhängend zu sein, doch sie verfolgen ein gemeinsames Ziel. Gute Ingenieurarbeit bedeutet nicht nur, dass der Code heute funktioniert; sie zielt darauf ab, zukünftige Anpassungen einfacher zu gestalten. Jede Gewohnheit trägt dazu bei:
- eine sichtbare, erfolgreiche Ablaufroute macht die Logik leichter nachvollziehbar
- sinnvolle Namen machen die Absicht offensichtlich
- Grenzen um externe Systeme begrenzen die Auswirkungen ihrer Änderungen
- präzise Typen schließen ganze Kategorien von Fehlern aus
- die Trennung von Entscheidungen und Aktionen macht Geschäftsregeln testbar
- strukturierte Fehler ermöglichen eine Diagnose von Ausfällen
- fokussierte Änderungen halten die Codebasis einfach zu entwickeln
Frameworks und Bibliotheken kommen und gehen, aber diese Prinzipien gelten gleichermaßen für JavaScript, TypeScript, Python, Java, Go oder alles andere.
Kernpunkte
- Wenn Sie eine Funktion, ein Modell, eine Integration oder einen Pull Request abgeschlossen haben, fragen Sie nicht nur danach, ob es funktioniert, sondern auch, ob der nächste Entwickler es sicher ändern kann.
- Schutzklauseln, präzise Namen und präzise Typen verringern das, was ein Leser im Kopf behalten muss.