Einleitung: Warum Legacy-Code kein Todesurteil ist
Jeder Entwickler stößt früher oder später auf Legacy-Code in JavaScript. Das können monolithische Skripte in Node.js sein, die vor 5 Jahren "auf die Schnelle" geschrieben wurden, oder verworrene Callback-Funktionen, die selbst Senior-Entwickler nicht anzufassen wagen. Refactoring von Legacy-Code ist jedoch nicht einfach "alles neu schreiben", sondern eine systematische Arbeit zur Verbesserung der Architektur, ohne das externe Verhalten zu ändern.
In diesem Artikel analysieren wir einen schrittweisen Plan für das Refactoring in JavaScript (Node.js): von der Analyse und dem Testen bis zur Implementierung moderner Patterns. Sie erfahren, wie Sie sicher einen "Sumpf" in wartbaren Code umwandeln, anhand realer Beispiele.
1. Analyse und Kartierung von Legacy-Code
Bevor Sie Code ändern, müssen Sie seinen aktuellen Zustand verstehen. Beginnen Sie mit der Erstellung einer Abhängigkeitskarte und der Identifizierung von "Hot Spots".
1.1. Werkzeuge für die Analyse
Für JavaScript/Node.js verwenden Sie:
- ESLint mit den Regeln
complexityundmax-lines— zur Identifizierung komplexer Funktionen. - Plato oder CodeClimate — zur Visualisierung der zyklomatischen Komplexität.
- Madge — zur Erstellung eines Abhängigkeitsgraphen von Modulen.
1.2. Identifizierung von "Code Smells"
Typische Probleme von Legacy-JS:
- Funktionen mit mehr als 100 Zeilen.
- Globale Variablen und Mutation des Zustands.
- Callback-Hell (verschachtelte asynchrone Aufrufe).
- Fehlende Modularität (eine Datei mit 2000+ Zeilen).
// Beispiel für einen "Code Smell": Funktion macht alles auf einmalfunction processUserData(userId) { // 100 Zeilen: Validierung, DB-Abfrage, Formatierung, E-Mail-Versand // ... schwer zu testen und zu warten}2. Vorbereitung eines "Sicherheitsnetzes": Testen
Ohne Tests ist Refactoring "Russisches Roulette". Erstellen Sie Characterization Tests (Tests, die das aktuelle Verhalten festhalten).
2.1. Hinzufügen von Unit-Tests
Verwenden Sie Jest oder Mocha. Beginnen Sie mit kritischen Pfaden:
// Testen der vorhandenen Funktion, ohne sie zu ändernconst { processUserData } = require('./legacy');
describe('processUserData (legacy)', () => { it('should process user with ID 1', () => { const result = processUserData(1); expect(result).toHaveProperty('email'); expect(result.status).toBe('processed'); });});2.2. Integrationstests für Node.js
Wenn der Code mit einer Datenbank oder API arbeitet, verwenden Sie supertest für HTTP-Anfragen:
const request = require('supertest');const app = require('../app');
describe('POST /api/users', () => { it('should create user (legacy behavior)', async () => { const res = await request(app) .post('/api/users') .send({ name: 'Test' }); expect(res.status).toBe(200); expect(res.body.id).toBeDefined(); });});3. Schrittweises Refactoring: Vom Monolithen zu Modulen
Wir unterteilen den Prozess in 3 Phasen: Isolierung, Dekomposition, Optimierung.
3.1. Isolierung und Wrapper
Fassen Sie das Innere nicht an — umhüllen Sie die Legacy-Funktion mit einer neuen Schnittstelle:
// Legacy-Funktionfunction oldFormatDate(date) { // ... 50 Zeilen mit Mutationen return formattedDate;}
// Neuer Wrapperfunction formatDateSafe(date) { if (!(date instanceof Date)) { throw new Error('Invalid date'); } return oldFormatDate(date);}3.2. Dekomposition mit Extract Function
Heben Sie logische Blöcke in separate Funktionen hervor:
// Vorher: eine Funktion mit 80 Zeilenfunction handleOrder(order) { // 1. Validierung if (!order.id || !order.total) { ... } // 2. Rabattberechnung