Refaktoring kodu JavaScript – od spaghetti do czystego kodu
Każdy programista zna ten moment. Otwierasz plik, który pisałeś sześć miesięcy temu – albo, co gorsza, przejąłeś po kimś innym – i stajesz przed ścianą niezrozumiałego kodu. Funkcje liczące 200 linii, zmienne o nazwach x, temp2 czy data_final, zagnieżdżone callbacki sięgające dziesiątego poziomu. To właśnie spaghetti code – i masz z nim do czynienia znacznie częściej, niż byś chciał.
Dobra wiadomość jest taka, że refaktoring to umiejętność, której można się nauczyć. Zła – że wymaga dyscypliny, cierpliwości i dobrego zestawu narzędzi. Ten artykuł pokaże Ci, od czego zacząć i jak systematycznie przekształcać chaos w czysty, czytelny JavaScript.
Czym jest refaktoring i czym NIE jest
Refaktoring to proces poprawy wewnętrznej struktury kodu bez zmiany jego zewnętrznego zachowania. Kluczowe słowo: bez zmiany zachowania. Nie dodajesz nowych funkcji, nie naprawiasz błędów (chyba że przypadkowo). Po refaktoringu program robi dokładnie to samo, co przed nim – tyle że kod jest łatwiejszy do czytania, testowania i rozbudowy.
Refaktoring nie jest:
- przepisywaniem całej aplikacji od zera,
- dodawaniem nowych funkcjonalności,
- optymalizacją wydajności (to osobny temat),
- naprawianiem błędów logicznych.
Martin Fowler, autor kultowej książki Refactoring: Improving the Design of Existing Code, opisuje refaktoring jako serię małych, bezpiecznych kroków. Każdy krok jest wystarczająco mały, by nie zepsuć niczego – i wystarczająco znaczący, by poprawić jakość kodu.
Kiedy wiesz, że czas na refaktoring?
Fowler opisał pojęcie „code smells" – zapachów kodu sygnalizujących problemy. W JavaScript najczęściej spotykamy:
- Długie funkcje – funkcja powinna robić jedną rzecz. Jeśli jej opis wymaga spójnika „i", to jest za długa.
- Duplikacja kodu – ten sam blok logiki pojawia się w kilku miejscach.
- Głębokie zagnieżdżenia – tzw. „pyramid of doom" z callbackami lub warunkami if/else.
- Magiczne liczby i stringi –
if (status === 3)zamiastif (status === ORDER_COMPLETED). - Złe nazwy – zmienne i funkcje o nazwach, które nic nie mówią o ich przeznaczeniu.
- Zbyt duże klasy/moduły – God Object, który wie i robi za dużo.
Krok 1: Zanim cokolwiek zmienisz – napisz testy
To zasada numer jeden refaktoringu, którą zbyt często się ignoruje. Bez testów nie masz pewności, że nic nie zepsułeś. Zanim zaczniesz reorganizować kod, napisz testy jednostkowe lub integracyjne, które pokrywają istniejące zachowanie.
Przykład prostego testu w Jest:
// Przed refaktoringiem – testujemy istniejące zachowanie
describe('calculateDiscount', () => {
test('powinien zwrócić 10% zniżki dla zamówień powyżej 100zł', () => {
expect(calculateDiscount(150)).toBe(15);
});
test('powinien zwrócić 0 dla zamówień poniżej 100zł', () => {
expect(calculateDiscount(80)).toBe(0);
});
});
Testy działają jak sieć bezpieczeństwa. Każda zmiana, która zepsuje funkcjonalność, natychmiast to pokaże.
Krok 2: Wyodrębnianie funkcji (Extract Function)
To najczęściej stosowana technika refaktoringu. Jeśli widzisz blok kodu, który robi jedną konkretną rzecz – wyodrębnij go do osobnej funkcji z opisową nazwą.
Przed refaktoringiem:
function processOrder(order) {
// Walidacja
if (!order.userId || !order.items || order.items.length === 0) {
throw new Error('Nieprawidłowe zamówienie');
}
// Obliczenie wartości
let total = 0;
for (let item of order.items) {
total += item.price * item.quantity;
}
// Zniżka
let discount = 0;
if (total > 100) discount = total * 0.1;
if (total > 500) discount = total * 0.2;
const finalTotal = total - discount;
// Zapis do bazy
db.save({ ...order, total: finalTotal, processedAt: new Date() });
console.log(`Zamówienie ${order.id} przetworzone. Wartość: ${finalTotal}`);
}
Po refaktoringu:
function validateOrder(order) {
if (!order.userId || !order.items || order.items.length === 0) {
throw new Error('Nieprawidłowe zamówienie');
}
}
function calculateTotal(items) {
return items.reduce((sum, item) => sum + item.price * item.quantity, 0);
}
function applyDiscount(total) {
if (total > 500) return total * 0.8;
if (total > 100) return total * 0.9;
return total;
}
function saveOrder(order, finalTotal) {
db.save({ ...order, total: finalTotal, processedAt: new Date() });
}
function processOrder(order) {
validateOrder(order);
const total = calculateTotal(order.items);
const finalTotal = applyDiscount(total);
saveOrder(order, finalTotal);
console.log(`Zamówienie ${order.id} przetworzone. Wartość: ${finalTotal}`);
}
Każda funkcja teraz robi dokładnie jedną rzecz. Kod jest samodokumentujący się – nazwy funkcji zastępują komentarze.
Krok 3: Walka z Callback Hell – async/await
JavaScript ma swoją szczególną wersję spaghetti code: zagnieżdżone callbacki, zwane „piramidą zagłady". Nowoczesne podejście z async/await radykalnie poprawia czytelność.
Przed – Callback Hell:
getUserData(userId, function(err, user) {
if (err) {
handleError(err);
} else {
getOrders(user.id, function(err, orders) {
if (err) {
handleError(err);
} else {
getProductDetails(orders[0].productId, function(err, product) {
if (err) {
handleError(err);
} else {
renderPage(user, orders, product);
}
});
}
});
}
});
Po – async/await:
async function loadUserPage(userId) {
try {
const user = await getUserData(userId);
const orders = await getOrders(user.id);
const product = await getProductDetails(orders[0].productId);
renderPage(user, orders, product);
} catch (err) {
handleError(err);
}
}
Różnica jest kolosalna. Kod czyta się jak zwykłe, synchroniczne instrukcje.
Krok 4: Eliminacja magicznych wartości
Magiczne liczby i stringi to pułapki. Nikt nie wie, co oznacza status === 4 po miesiącu od napisania kodu. Rozwiązaniem są stałe z opisowymi nazwami lub obiekty konfiguracyjne.
// Źle
if (user.role === 2 && order.status === 4) {
applySpecialDiscount(order, 0.15);
}
// Dobrze
const UserRole = {
ADMIN: 1,
PREMIUM: 2,
STANDARD: 3,
};
const OrderStatus = {
PENDING: 1,
PROCESSING: 2,
SHIPPED: 3,
COMPLETED: 4,
CANCELLED: 5,
};
const PREMIUM_DISCOUNT_RATE = 0.15;
if (user.role === UserRole.PREMIUM && order.status === OrderStatus.COMPLETED) {
applySpecialDiscount(order, PREMIUM_DISCOUNT_RATE);
}
Krok 5: Upraszczanie warunków – Early Return
Technika Early Return (wcześniejszy powrót) eliminuje głębokie zagnieżdżenia poprzez wychodzenie z funkcji jak najszybciej, gdy warunek nie jest spełniony.
// Źle – zagnieżdżone ify
function processPayment(payment) {
if (payment) {
if (payment.amount > 0) {
if (payment.currency === 'PLN') {
if (isCardValid(payment.card)) {
return executeTransaction(payment);
}
}
}
}
return null;
}
// Dobrze – Early Return
function processPayment(payment) {
if (!payment) return null;
if (payment.amount <= 0) return null;
if (payment.currency !== 'PLN') return null;
if (!isCardValid(payment.card)) return null;
return executeTransaction(payment);
}
Krok 6: Wykorzystanie nowoczesnego JavaScript
ES6+ wprowadził wiele konstrukcji, które naturalnie prowadzą do czystszego kodu. Używaj ich świadomie:
Destrukturyzacja zamiast wielokrotnego dostępu:
// Źle
function greetUser(user) {
return `Cześć, ${user.firstName} ${user.lastName}! Twój email: ${user.email}`;
}
// Dobrze
function greetUser({ firstName, lastName, email }) {
return `Cześć, ${firstName} ${lastName}! Twój email: ${email}`;
}
Spread operator zamiast Object.assign:
// Bardziej czytelne aktualizacje obiektów
const updatedUser = { ...existingUser, email: newEmail, updatedAt: new Date() };
Optional chaining – koniec z „Cannot read property of undefined":
// Źle
const city = user && user.address && user.address.city;
// Dobrze
const city = user?.address?.city;
Narzędzia wspierające refaktoring
Dobry programista to taki, który korzysta z odpowiednich narzędzi:
- ESLint – automatycznie wykrywa problemy z jakością kodu i wymusza spójny styl.
- Prettier – formatuje kod, eliminując dyskusje o stylu.
- Jest / Vitest – frameworki do testów jednostkowych, niezbędne przy refaktoringu.
- SonarQube / SonarCloud – zaawansowana analiza statyczna, wykrywa „code smells" i długi techniczny.
- VS Code z rozszerzeniami – np. „JavaScript Refactoring Assistant" czy „Code Spell Checker".
Zasada Skauta: zostaw obóz czystszy niż go znalazłeś
Refaktoring nie musi być wielkim projektem zaplanowanym na tygodnie. Stosuj zasadę skauta: za każdym razem, gdy dotykasz pliku, zostaw go odrobinę lepszym niż go zastałeś. Zmień jedną mylącą nazwę zmiennej. Wyodrębnij jedną funkcję. Usuń jeden zbędny komentarz.
Małe zmiany, konsekwentnie stosowane przez cały zespół, przez miesiące kumulują się w dramatyczną poprawę jakości całego projektu.
Podsumowanie
Refaktoring JavaScript to nie jednorazowa akcja – to stały nawyk profesjonalnego programisty. Zacznij od testów, rób małe kroki, używaj opisowych nazw i nowoczesnych konstrukcji języka. Twój przyszły „ja" – oraz cały zespół – podziękuje Ci za każdą godzinę spędzoną na porządkowaniu kodu.
Pamiętaj: kod jest czytany znacznie częściej, niż jest pisany. Inwestycja w jego czytelność zawsze się zwraca.