Refaktoryzacja legacy code to kontrolowana poprawa struktury starego kodu bez zmiany jego zachowania — bez przepisywania systemu od zera i bez zatrzymywania produkcji. Dobrze przeprowadzona refaktoryzacja obniża koszt każdej kolejnej zmiany, skraca czas wdrożeń i zmniejsza liczbę błędów. Ten przewodnik pokazuje, jak ją zaplanować, jakie techniki działają w starym kodzie PHP i kiedy refaktoryzacja przestaje się opłacać.
Najważniejsze: Refaktoryzacja nie zmienia tego, co system robi — zmienia tylko to, jak jest zbudowany. Zasada bezpieczeństwa jest jedna: najpierw testy zabezpieczające (charakterystyczne), potem refaktoryzacja, na końcu wdrożenie modułami. Bez testów refaktoryzacja to nie modernizacja, a hazard.
Czym jest refaktoryzacja i czym nie jest
Refaktoryzacja to zmiana wewnętrznej struktury kodu przy zachowaniu identycznego zachowania zewnętrznego. Martin Fowler definiuje ją jako „proces poprawy projektu istniejącego kodu bez zmiany jego zachowania" — i ten warunek jest istotą, nie ozdobnikiem. Jeśli po zmianie system zachowuje się inaczej (nawet „lepiej"), to nie jest refaktoryzacja, lecz zmiana funkcjonalna, która wymaga osobnego procesu i osobnych testów.
Refaktoryzacja to nie jest
- przepisanie modułu od zera — to wymiana implementacji, opisana szerzej w artykule refaktoryzacja czy przepisanie od zera,
- optymalizacja wydajności — refaktoryzacja może ją ułatwić, ale sama nie goni za milisekundami,
- kosmetyka formatowania — porządki w stylu kodu robi się automatem, nie projektem modernizacji,
- dodawanie funkcji — nowe zachowanie wymaga nowych testów i zwykle innego cyklu wdrożenia.
Refaktoryzacja to inwestycja w koszt przyszłych zmian. Każdy moduł zrefaktoryzowany do czytelnej struktury sprawia, że kolejne dwadzieścia zmian w tym module będzie tańszych, szybszych i bezpieczniejszych.
Kiedy refaktoryzować, a kiedy tylko łatać
Nie każdy fragment starego kodu zasługuje na refaktoryzację — i to jest pierwsza decyzja, którą warto podjąć świadomie. W projektach, które przejmuję, dzielę kod na cztery kategorie:
- kod krytyczny i często zmieniany — refaktoryzuj pierwszy, bo tu każda zmiana boli najbardziej,
- kod krytyczny, ale stabilny — zabezpiecz testami i zostaw; dotykaj tylko przy okazji realnej potrzeby,
- kod niekrytyczny i często zmieniany — refaktoryzuj przy okazji zmian funkcjonalnych,
- kod martwy — usuń, nie refaktoryzuj. Porządkowanie nieużywanego kodu to najczęstsza pułapka modernizacji.
Punkt wyjścia to zawsze rozpoznanie systemu — mapa zależności, lista modułów krytycznych i ich historii zmian. Bez tego refaktoryzacja zamienia się w porządkowanie losowych plików. Jak wygląda taki przegląd od strony praktycznej, opisuję w przewodniku o audycie kodu legacy, a cały plan modernizacji — od audytu po CI/CD — w artykule o modernizacji legacy code krok po kroku.
Testy zabezpieczające — warunek konieczny
Refaktoryzacja bez testów jest niebezpieczna z prostego powodu: jeśli nie wiesz, co system robi teraz, nie możesz stwierdzić, czy po zmianie robi to samo. W starym kodzie rzadko są testy jednostkowe, ale to nie znaczy, że jesteś bez narzędzi.
Sprawdzone podejście to testy charakterystyczne — opisujesz testami bieżące zachowanie systemu, także to nieidealne, i traktujesz je jako kontrakt. Technikę i konkretne przykłady w PHPUnit opisuję w artykule o testach charakterystycznych. Uzupełnieniem są testy end-to-end na ścieżkach krytycznych: logowanie, zamówienie, płatność, faktura. Te kilka testów daje więcej bezpieczeństwa niż tydzień analizy.
Dopiero gdy moduł ma siatkę bezpieczeństwa, można go tkwić. Kolejność jest nienegocjowalna — i to jest różnica między refaktoryzacją a improwizacją.
Techniki refaktoryzacji, które działają w starym kodzie
W legacy kodzie sprawdzają się techniki o małym zasięgu — każda z nich jest odwracalna i weryfikowalna przez testy
- wydzielanie funkcji i metod (extract method) — najprostsza i najskuteczniejsza technika; rozbija długie procedury na kroki o nazwach opisujących intencję,
- wprowadzenie warstwy pośredniej (branch by abstraction) — między starą i nową implementację wstawiasz interfejs, co pozwala podmieniać implementacje bez zmian w miejscach wywołań,
- odwrócenie zależności — zamiast wywołań globalnych funkcji i statycznych klas wprowadzasz wstrzykiwanie zależności, co czyni kod testowalnym,
- usuwanie duplikacji — powtarzalne bloki zamieniasz na wspólne funkcje; w starym kodzie duplikacja jest głównym źródłem „naprawiłem w jednym miejscu, zepsuło się w trzech",
- porządki w nazewnictwie — zmienne
$data2,$tmpi funkcjeprocessStuff()kosztują więcej czasu niż jakakolwiek architektura, - izolacja efektów ubocznych — wydzielenie kodu odpowiedzialnego za zapisy do bazy, wysyłkę maili i wywołania API od logiki biznesowej.
Dla dużych monolitów techniką nadrzędną jest wzorzec Strangler Fig — nowy kod wyrasta wokół starego i przejmuje jego funkcje moduł po module. Opisuję go w artykule o wzorcu Strangler Fig.
Refaktoryzacja wspierana narzędziami
Ręczna refaktoryzacja dużego systemu to miesiące pracy. Automatyzacja zmienia ekonomię całego procesu
- Rector wykonuje masowe refaktoryzacje i aktualizacje składni PHP — zmiany, które ręcznie zajęłyby kwartał, potrafi przygotować jako pull request w godzinę,
- PHPStan i Psalm wychwytują błędy typów i martwy kod przed uruchomieniem; w legacy zaczynasz od najłagodniejszego poziomu i podnosisz poprzeczkę moduł po module,
- PHPUnit i Pest utrwalają zachowanie modułu przed zmianami,
- IDE (PhpStorm, VS Code) z analizą statyczną bezpiecznie wykonuje zmiany nazw, sygnatur i przenoszenie kodu,
- Laravel Shift automatyzuje aktualizacje frameworka między wersjami, jeśli system korzysta z Laravel.
Zestaw startowy jest darmowy lub tani: PHPStan, Rector i PHPUnit to open source. Kolejność wdrożenia opisuję w pillarze o modernizacji legacy code.
Proces refaktoryzacji krok po kroku
Sprawdzony cykl dla jednego modułu wygląda tak
- Wybierz moduł — krytyczny dla biznesu i często zmieniany, ale nie najtrudniejszy w systemie. Pierwszy moduł ma dać zespół i proces, nie medal.
- Zabezpiecz testami — charakterystyczne na bieżące zachowanie, end-to-end na ścieżkę użytkownika, jeśli moduł jej dotyczy.
- Zrefaktoryzuj w małych krokach — każdy krok kończy się uruchomieniem testów. Commit po każdym zielonym kroku.
- Wdróż na staging — niech nowa wersja „poleży" pod ruchem testowym.
- Wdróż na produkcję i monitoruj — błędy, wydajność, logi. Kilka dni obserwacji przed uznaniem modułu za zamknięty.
- Dopisz testy jednostkowe nowych, wyodrębnionych fragmentów — dług techniczny spłacasz przy okazji, nie „kiedyś".
Tempo ma znaczenie mniejsze niż rytm. Jeden moduł na sprint, wdrożony i zweryfikowany, bije trzy moduły zrefaktoryzowane „prawie na gotowo" i wdrożone razem.
Kiedy refaktoryzacja się nie opłaca
-
Uczciwa odpowiedź: czasem refaktoryzacja nie jest właściwym narzędziem.
-
Moduł jest nie do uratowania, a jego logika prosta — taniej przepisać go od zera z testami niż refaktoryzować warstwa po warstwie.
-
Kod działa i nikt go nie zmienia — każda godzina refaktoryzacji takiego modułu to koszt bez zwrotu; zostaw go z testami bezpieczeństwa i monitoringiem.
-
Zbliża się wymiana całego systemu — jeśli firma zdecydowała o migracji do nowej platformy w horyzoncie kwartału, refaktoryzacja starego kodu to praca wrzucona do kosza.
-
Brak testów i brak czasu na nie — refaktoryzacja bez zabezpieczenia to nie modernizacja; w takiej sytuacji zacznij od testów, nie od zmian struktury.
Refaktoryzacja jest narzędziem, nie celem. Celem jest system, który znów daje się rozwijać w przewidywalnym czasie i budżecie.
Najczęściej zadawane pytania
Ile trwa refaktoryzacja legacy code?
To zależy od skali i jakości testów. Refaktoryzacja pojedynczego modułu to zwykle od kilku dni do dwóch tygodni — z testami charakterystycznymi i wdrożeniem. Cały system dzieli się na moduły i refaktoryzuje etapami przez miesiące, bez zatrzymywania produkcji.
Czy refaktoryzacja wymaga zatrzymania systemu?
Nie. Refaktoryzacja modułowa polega na tym, że system działa cały czas — zmiany przechodzą przez staging i są wdrażane pojedynczo, z monitoringiem po każdym wdrożeniu. Przestój nie jest częścią procesu.
Czym różni się refaktoryzacja od przepisania od zera?
Refaktoryzacja poprawia strukturę istniejącego kodu, zachowując jego zachowanie i technologie. Przepisanie od zera buduje nowy system obok starego, z nowymi decyzjami technologicznymi — jest droższe, dłuższe i obarczone większym ryzykiem. Większość systemów potrzebuje refaktoryzacji, a nie przepisania.
Czy refaktoryzacja może zepsuć działający system?
Może, jeśli prowadzisz ją bez testów i wdrożeń modułami. Z testami charakterystycznymi, wdrożeniem na staging i monitoringiem po wdrożeniu ryzyko jest ograniczone do jednego modułu — a nie całego systemu. To dokładnie ten sam standard bezpieczeństwa, który stosuje się przy każdej innej zmianie w kodzie produkcyjnym.