Jedna funkcja zaczyna się od walidacji formularza, wysyłki maila, zapisu do bazy i obsługi kilku wyjątków, a po miesiącu nikt nie chce jej dotknąć. Tak wygląda dirty code, czyli kod działający na pierwszy rzut oka, lecz trudny do zrozumienia, testowania i bezpiecznego rozwijania. Pokażę, jak rozpoznać takie problemy, jakie błędy architektoniczne najczęściej do nich prowadzą oraz jak refaktoryzować aplikację bez niepotrzebnego ryzyka.
Brudny kod szybko zamienia prostą zmianę w kosztowny problem
- Dirty code może działać poprawnie, a mimo to być trudny w utrzymaniu.
- Duplikacja, długie funkcje i zależności są sygnałami problemów projektowych.
- Refaktoryzacja poprawia strukturę kodu bez zmiany jego zachowania.
- Testy i małe kroki ograniczają ryzyko podczas porządkowania starej aplikacji.
- Architektura warstwowa i zasady SOLID pomagają zapobiegać powrotowi problemu.

Po czym poznać, że kod wymaga porządkowania
Brudny kod nie zawsze powoduje natychmiastowy błąd. Częściej sprawia, że każda kolejna zmiana wymaga coraz większej ostrożności. Programista nie boi się samej funkcji, lecz tego, że poprawka w jednym miejscu zepsuje zachowanie w trzech innych.
Najbardziej charakterystyczny sygnał to rosnący koszt małych zmian. Jeżeli dodanie jednego pola do formularza wymaga modyfikacji kontrolera, zapytania SQL, kilku warunków w widoku i ręcznego sprawdzania wielu ekranów, problem leży zwykle głębiej niż w pojedynczej linijce.
Typowe oznaki złej jakości
- Długie funkcje, które jednocześnie pobierają dane, sprawdzają uprawnienia i budują odpowiedź.
- Duplikacja logiki, na przykład ta sama walidacja skopiowana do kilku kontrolerów.
-
Nazwy typu
data,tmplubflag, które nie mówią, co naprawdę przechowuje zmienna. - Magiczne liczby i teksty rozsiane po kodzie bez wyjaśnienia ich znaczenia.
- Zagnieżdżone warunki, w których trudno ustalić, jaki przypadek prowadzi do konkretnego wyniku.
- Klasy odpowiedzialne za zbyt wiele rzeczy, na przykład za płatność, e-mail i zapis użytkownika.
Nie każdy zapach kodu oznacza awarię. Traktuję go raczej jako sygnał ostrzegawczy, że przyszłe zmiany mogą być droższe i bardziej ryzykowne. Prosty skrypt używany raz w migracji nie musi spełniać tych samych standardów co kod obsługujący codziennie tysiące użytkowników.
Dlaczego architektura zamienia prosty kod w trudny system
Problemy często zaczynają się od niewinnego skrótu. Ktoś dopisuje warunek w kontrolerze, potem kolejny wyjątek, a po kilku tygodniach jedna warstwa zna szczegóły bazy danych, interfejsu użytkownika i zewnętrznego API. Powstaje silne sprzężenie, czyli sytuacja, w której zmiana jednego modułu wymusza poprawki w wielu innych.
Najczęstsze błędy projektowe
God object to jedna duża klasa, która wie i robi prawie wszystko. W aplikacji webowej może obsługiwać żądanie HTTP, sprawdzać sesję, liczyć cenę koszyka, zapisywać zamówienie i wysyłać wiadomość. Taki kod trudno testować, bo do sprawdzenia jednej reguły biznesowej trzeba uruchomić pół aplikacji.
Drugim problemem jest mieszanie poziomów abstrakcji. W tej samej metodzie pojawia się kod SQL, reguła biznesowa i formatowanie HTML. Gdy zmieni się sposób przechowywania danych albo wygląd odpowiedzi, trzeba ingerować w logikę, która powinna pozostać niezależna.
Trzeci błąd to kopiowanie zamiast wydzielenia wspólnej reguły. Duplikacja bywa wygodna przez kilka godzin, ale później każdą poprawkę trzeba pamiętać w dwóch, pięciu albo dziesięciu miejscach. W praktyce właśnie wtedy najłatwiej o niespójne zachowanie aplikacji.
Przeczytaj również: Wzorzec Memento - Cofanie zmian w aplikacji webowej bez chaosu!
Wzorce pomagają, ale nie zastąpią myślenia
Wzorzec projektowy ma sens wtedy, gdy rozwiązuje konkretny problem. Strategy dobrze pasuje do sytuacji, w której cena lub sposób dostawy zależy od wybranej metody. Factory może uporządkować tworzenie różnych implementacji usługi. Adapter odgradza resztę aplikacji od niewygodnego API zewnętrznego.
Nie próbuję jednak dodawać wzorca do każdej klasy. Nadmiar abstrakcji także tworzy bałagan. Jeżeli prostą funkcję można czytelnie zapisać w kilkunastu liniach, budowanie dla niej pięciu interfejsów i trzech fabryk najpewniej pogorszy sytuację.
Jak wygląda brudny kod w aplikacji webowej
Załóżmy, że kontroler ma utworzyć zamówienie. W złej wersji może wyglądać tak:
function createOrder(request) {
if (!request.user || request.user.role !== 'customer') {
return redirect('/login');
}
if (!request.body.email || !request.body.address) {
return render('error', { message: 'Brak danych' });
}
const total = calculateCart(request.session.cart);
if (total > 500) {
sendEmail(request.body.email, 'Darmowa dostawa');
}
database.query(
'INSERT INTO orders (...) VALUES (...)',
[request.body.email, request.body.address, total]
);
return render('success', { total });
}
Ten przykład jest krótki, ale łączy autoryzację, walidację, regułę cenową, komunikację i persystencję. Każdy fragment może być poprawny, jednak razem tworzą funkcję, której nie da się łatwo wykorzystać poza jednym kontrolerem.
Lepszy podział może wyglądać następująco:
function createOrder(request) {
authorizeCustomer(request.user);
const input = validateOrderInput(request.body);
const order = orderService.create(input, request.session.cart);
return render('success', { total: order.total });
}
Nie chodzi o samo skrócenie funkcji. Zysk polega na tym, że każda odpowiedzialność ma własne miejsce. Serwis zamówień może zostać przetestowany bez uruchamiania widoku, walidację można wykorzystać w innym wejściu, a sposób zapisu do bazy da się wymienić bez przebudowy kontrolera.
Jak bezpiecznie refaktoryzować stary kod
Najgorszy pomysł to przepisać całą aplikację od zera bez zabezpieczenia obecnego zachowania. Nowa wersja może wyglądać ładniej, ale przy okazji łatwo zgubić reguły, których nie opisano w dokumentacji. Ja zaczynam od małego, mierzalnego fragmentu, który ma wyraźny problem i realnie przeszkadza w pracy.
- Opisz obecne zachowanie. Zapisz, jakie dane wchodzą do funkcji, jaki wynik powinna zwrócić i jakie błędy są obsługiwane.
- Dodaj test ochronny. Nawet prosty test integracyjny lepiej chroni przed regresją niż pamięć autora.
- Wydziel jedną odpowiedzialność. Przenieś walidację, obliczenia albo komunikację do osobnej funkcji.
- Uruchom testy po małej zmianie. Duże refaktoryzacje trudniej przejrzeć i trudniej cofnąć.
- Usuń duplikację dopiero po zrozumieniu reguły. Dwa podobne fragmenty nie zawsze oznaczają tę samą logikę biznesową.
Refaktoryzacja powinna zachować zachowanie programu. Jeżeli przy okazji zmieniam regułę biznesową, traktuję to jako osobne zadanie. Takie rozdzielenie jest ważne, bo porządkowanie kodu i naprawianie błędu wymagają innych testów oraz innego sposobu oceny ryzyka.
| Problem | Bezpieczny pierwszy krok | Czego unikać |
|---|---|---|
| Długa funkcja | Wydzielenie nazwanych funkcji pomocniczych | Przenoszenia całej logiki do nowej, równie dużej klasy |
| Duplikacja | Porównanie, czy fragmenty mają tę samą regułę | Łączenia kodu tylko dlatego, że wygląda podobnie |
| Trudne testowanie | Oddzielenie zależności przez interfejs lub parametr | Dodawania globalnych zmiennych i ukrytych obejść |
| Niejasne dane | Zmiana nazw i wprowadzenie małych obiektów wartości | Dodawania komentarzy wyjaśniających niezrozumiałą logikę |
Komentarz nie powinien być plastrem na źle nazwany kod. Jeśli muszę długo tłumaczyć, co robi pięć zagnieżdżonych warunków, najpierw próbuję zmienić strukturę i nazwy. Dobry kod opowiada swoją historię, a komentarz dopowiada tylko to, czego nie da się jasno wyrazić implementacją.
Jak nie dopuścić do powrotu problemu
Jednorazowe sprzątanie nie wystarczy, jeśli zespół nadal dostarcza zmiany bez testów i przeglądu. Potrzebny jest prosty proces, który utrzymuje jakość bez zamieniania programowania w kontrolę każdego przecinka.
- Code review powinien sprawdzać odpowiedzialności, zależności i obsługę błędów, nie tylko styl formatowania.
- Formatter i linter automatycznie usuwają część dyskusji o wyglądzie kodu.
- Testy jednostkowe chronią reguły biznesowe, a testy integracyjne sprawdzają współpracę modułów.
- Małe pull requesty są łatwiejsze do oceny niż jedna ogromna zmiana raz na kwartał.
- Rejestr długu technicznego pomaga ustalić, które problemy naprawdę blokują rozwój.
Nie każdy fragment trzeba od razu idealizować. Jeśli moduł jest stabilny, rzadko zmieniany i nie generuje błędów, pełna przebudowa może nie mieć sensu. Najwięcej uwagi poświęcam miejscom, które są często modyfikowane, krytyczne biznesowo albo podatne na awarie.
Dobrą zasadą jest refaktoryzowanie kodu przy okazji zmiany, ale w kontrolowanym zakresie. Gdy dotykam starej funkcji, poprawiam jeden widoczny problem, dodaję test i zostawiam moduł odrobinę lepszym. Po kilku miesiącach taka regularność daje więcej niż heroiczna akcja porządkowa raz w roku.
Najlepszy moment na zmianę to chwila przed kolejną funkcją
Brudny kod rzadko pojawia się w jednym dniu. Zwykle narasta przez małe decyzje podejmowane pod presją czasu, brak testów i dokładanie kolejnych wyjątków do istniejącej struktury. Nie oznacza to, że każdą starszą aplikację trzeba przepisać od początku.
Najrozsądniejsza droga prowadzi przez rozpoznanie największego ryzyka, małą refaktoryzację i test zabezpieczający. Architektura, wzorce i narzędzia są pomocne, ale najważniejsza pozostaje czytelna odpowiedzialność modułów. Jeśli kolejny programista może zrozumieć kod bez rozmowy z jego autorem, wykonano już dużą część pracy.