Czym jest „stary kod” w Javie i skąd się bierze problem
Różne oblicza legacy code w Javie
Określenie legacy code w Javie rzadko oznacza tylko „stary wiek” kodu. Najczęściej chodzi o jedną z kilku sytuacji, które często występują razem:
- Kod bez testów – nie wiadomo, czy zmiana niczego nie popsuje. Każde kliknięcie „Run” to mała ruletka.
- Stary stos technologiczny – Java 6/7, stare Springi, własne frameworki webowe, biblioteki bez wsparcia.
- Brak maintainerów – zespół, który to pisał, już nie pracuje, albo pamięta tylko „że to działa, nie ruszać”.
- Kod „kogoś innego” – nawet w świeżym projekcie kod pisany przez inny zespół bywa traktowany jako legacy.
W praktyce legacy code to najczęściej po prostu kod trudny do bezpiecznej zmiany. Może być napisany wczoraj, jeśli powstał „na szybko”, bez testów i bez myślenia o strukturze. To ważne mentalnie: nie chodzi o wiek, ale o koszt wprowadzania zmian.
Jak rośnie dług technologiczny w projektach javowych
Dług technologiczny rzadko pojawia się z dnia na dzień. Najczęściej to długa seria małych kompromisów:
- Ciśnienie na deadline’y – „Zróbmy, żeby działało, potem się posprząta”. „Potem” zwykle nie nadchodzi.
- Brak code review – każdy programuje po swojemu, standardy istnieją tylko w Confluence.
- Rotacja ludzi – nowi programiści nie znają kontekstu, kopiują istniejące rozwiązania, bo „tak już jest”.
- Brak właściciela architektury – decyzje są ad hoc, rośnie liczba wyjątków od zasad.
W projektach javowych dochodzą do tego zmiany w ekosystemie: przejścia z Java EE na Spring Boot, migracje z XML na konfigurację Java, zmiany ORM, serwerów aplikacyjnych. Często stare moduły zostają „jak są”, bo nikt nie ma czasu ani odwagi ich ruszyć.
Charakterystyczne cechy starej bazy kodu
W praktyce stary kod w Javie da się rozpoznać po kilku wzorcach:
- Długie klasy i metody – klasa serwisu na kilka tysięcy linii, metoda kontrolera na kilkaset.
- Brak jasnych warstw – logika biznesowa, dostępy do bazy, walidacja i mapping JSON w jednej klasie.
- Mieszanie technologii i epok – Spring + EJB, Hibernate + bezpośrednie JDBC, nowe adnotacje obok starych XML-i.
- Zakomentowany kod – „historyczne” fragmenty, których nikt się nie odważy usunąć, bo „może jeszcze się przyda”.
- Statyczne helpery i singletons – globalny stan, trudne do testowania zależności, niekontrolowane side-effecty.
W takich systemach każda zmiana wiąże się z obawą, że coś wybuchnie w zupełnie innym miejscu. To główny powód, dla którego refaktoryzacja legacy code w Javie bywa odkładana tak długo, aż problem staje się krytyczny.
Emocje przy pracy ze starym kodem
Przy czytaniu zastanego kodu dochodzi jeszcze warstwa emocjonalna. Pojawia się frustracja („kto to napisał?”), obawa przed popsuciem („jak to zmienię, spadną raporty dla zarządu”) i efekt „świętej krowy” – są moduły, których „nie można dotykać”, bo były kiedyś ratowane na szybko i działają na szczęściu.
Zamiast wchodzić w ton moralizatorski, lepiej przyjąć prostą zasadę: twoim zadaniem nie jest ocena przeszłości, ale poprawa przyszłości. Kod taki jest, bo takie były warunki. Twoim celem jest go zrozumieć, ogrodzić testami i refaktoryzować w małych, bezpiecznych krokach.
Przygotowanie do pracy z legacy Java: kontekst, narzędzia, nastawienie
Zebranie kontekstu biznesowego przed pierwszą zmianą
Bez zrozumienia biznesu nawet najlepsze techniki refaktoryzacji Java są jak strzelanie z zamkniętymi oczami. Najpierw trzeba wiedzieć:
- Kto używa systemu – dział sprzedaży, klienci zewnętrzni, integracje B2B.
- Jakie są krytyczne ścieżki – logowanie, składanie zamówienia, księgowanie płatności.
- Gdzie są pieniądze i ryzyko – błędne naliczenia faktur, podwójne pobrania z karty, utrata danych.
Praktyczny krok: narysuj na kartce prosty przepływ najważniejszego procesu (np. zakup online) i zaznacz, które moduły Java są w niego zaangażowane. Refaktoryzację zacznij od miejsc kluczowych dla biznesu – i tam najpierw dodaj testy charakterystyki.
Szybkie rozpoznanie architektury i stacku
Kolejny krok to zrozumienie struktury technicznej. Krótka checklista:
- Monolit czy mikrousługi? A może modularny monolit?
- Jaka wersja Javy (8, 11, 17…)? Czy są plany aktualizacji?
- Jaki framework webowy: Spring MVC, Spring Boot, JSF, Struts, coś własnego?
- Jak wygląda warstwa persystencji: Hibernate/JPA, MyBatis, czyste JDBC, mieszanka?
- Gdzie są konfiguracje: pliki XML, adnotacje, pliki properties/yaml?
Dla wielu decyzji refaktoryzacyjnych (np. wprowadzenie nowych wzorców projektowych Java, rozbijanie warstw) kluczowe jest, czy system jest blisko „standardowego” ekosystemu Spring Boot, czy raczej to customowy twór z wieloma niestandardowymi rozwiązaniami.
Podstawowy zestaw narzędzi do pracy ze starym kodem
Do pracy z legacy code w Javie przydaje się rozsądny zestaw narzędzi:
- IDE: IntelliJ IDEA (najczęściej), Eclipse lub VS Code z dobrym wsparciem dla Javy.
- Git – zrozumienie historii plików, sensowne branche, możliwość bezpiecznej pracy eksperymentalnej.
- Narzędzia analizy statycznej – SonarLint, SonarQube, SpotBugs, PMD, Checkstyle.
- Profiler – VisualVM, YourKit, Flight Recorder – przydaje się szczególnie przy refaktoryzacji pod wydajność.
Narzędzia do analizy statycznej potraktuj jako „drugi zestaw oczu”: wychwycą typowe antywzorce w starym kodzie, ale ich wyniki trzeba filtrować. Nie każde ostrzeżenie warto naprawiać od razu.
Jasno określony cel refaktoryzacji
Zanim ruszysz z refaktoryzacją, odpowiedz sobie wprost:
- Po co to robisz? (utrzymywalność, wydajność, przygotowanie do migracji technologicznej, redukcja długu technologicznego)
- Co jest sukcesem? (np. pokrycie testami krytycznych ścieżek, rozbicie God Class na 3 klasy, eliminacja statycznych zależności w module X)
Spisz ten cel choćby w Jirze czy README modułu. Bez tego łatwo wpaść w pułapkę „refaktoryzujemy wszystko”, co kończy się przerwanym na pół roku projektem i brakiem namacalnych efektów.
Nastawienie: najpierw zrozum, potem zmieniaj
Przy starym kodzie szczególnie kuszące jest „napiszę to od zera, będzie lepiej”. W 99% przypadków to zła decyzja. Bez pełnych testów i wiedzy domenowej nowa implementacja powtórzy stare błędy i doda własne.
Lepsze podejście:
- Najpierw czytanie zastanego kodu i tworzenie hipotez, jak działa.
- Potem testy charakterystyki, które tę wiedzę „zamrażają” w testach.
- Dopiero potem stopniowa refaktoryzacja modułów – mikro krok po mikro kroku.
Zasada robocza: jeśli nie potrafisz napisać prostego testu, który odtwarza aktualne zachowanie fragmentu kodu, prawdopodobnie jeszcze go nie rozumiesz na tyle, by bezpiecznie go mocniej przebudowywać.
Jak skutecznie czytać stary kod w Javie – podejście krok po kroku
Zaczynanie od punktów wejścia do systemu
Najtrudniej czyta się pojedyncze klasy wyrwane z kontekstu. Lepiej zacząć od punktów wejścia:
- Kontrolery REST / MVC – klasy z adnotacjami
@RestController,@Controller. - Joby batchowe – klasy z
main,@Scheduled, konfiguracje Spring Batch. - Listener’y – kolejki JMS, Kafka, cron.
Dzięki temu od razu widzisz, jak użytkownik lub inny system uruchamia logikę. Łatwiej wtedy zrozumieć, które ścieżki są ważne, a które to rzadko używane boczne funkcje.
Na co patrzeć przy pierwszym czytaniu klasy
Szybka analiza klasy to dobre wejście do dalszej refaktoryzacji. Sprawdź:
- Nazwę i komentarz klasowy – czy odzwierciedlają faktyczną odpowiedzialność?
- Pola – szczególnie statyczne, współdzielone kolekcje, cache’e, singletons.
- Ilość publicznych metod – im więcej, tym większa powierzchnia zmian.
- Wyjątki – co jest łapane, co propagowane, gdzie ukrywa się logika w
catch.
Przykład: klasa OrderService ma 40 metod publicznych i kilka pól statycznych typu Map. To sygnał, że masz do czynienia z God Class i potencjalnie niebezpiecznym stanem współdzielonym. Zanim cokolwiek tu refaktoryzujesz, dobrze jest napisać testy wokół najważniejszych metod.
Strategia „od zewnątrz do środka”
Dobra praktyka czytania starego kodu to przechodzenie przez warstwy w kierunku od interfejsu do domeny:
- Kontroler (np.
OrderController) – jakie endpointy obsługuje? - Serwis (np.
OrderService) – jakie metody biznesowe są wywoływane? - Warstwa domenowa (np.
Order,OrderItem,PricingPolicy) – gdzie jest logika biznesowa? - Warstwa persystencji (repozytoria, DAO) – jakie dane są ładowane/zapisywane?
Podczas przechodzenia zapisuj sobie krótkie notatki: „POST /orders → OrderService.createOrder → PricingService.calculate → baza”. Nawet prosty tekstowy diagram sekwencji pomaga, gdy po kilku dniach wracasz do tego samego fragmentu.
Notowanie hipotez o działaniu i drobne diagramy
Podczas pierwszego czytania starego kodu w Javie trudno od razu zrozumieć wszystkie szczegóły. W praktyce lepiej:
- Spisywać hipotezy („ta flaga decyduje, czy naliczyć rabat dla stałego klienta”).
- Przy każdej hipotezie zaznaczyć niepewność (np. znak zapytania, jeśli nie jesteś pewien).
- Potem potwierdzać/obalać hipotezy przez krótkie testy lub debugowanie.
Prosty diagram przepływu (np. w notatniku, na kartce lub w narzędziu typu draw.io) potrafi zaoszczędzić godziny klikania „Go to definition” po klasach. Nie musi być ładny – ważne, żeby był zrozumiały dla ciebie i kilku osób z zespołu.
Krótkie sesje eksploracji i zmiana perspektywy
Łatwo ugrzęznąć w jednej klasie na kilka godzin, analizując każdy warunek if. Lepsza strategia:
- Ustaw sobie limit: np. 25–30 minut na jedną ścieżkę analizy.
- Po tym czasie zrób krótką przerwę lub zmień poziom – wróć do kontrolera albo spójrz na testy (jeśli istnieją).
- Jeśli dalej nic nie rozumiesz – zmień narzędzie: włącz debugger, użyj call hierarchy, spójrz na logi produkcyjne.
Czytanie kodu to wysiłek poznawczy. W krótszych, intensywnych sesjach będziesz podejmować rozsądniejsze decyzje refaktoryzacyjne niż po trzech godzinach gapienia się w ten sam fragment if-else.

Użycie IDE i narzędzi do zrozumienia i analizy starego kodu
Nawigacja w IntelliJ, Eclipse i VS Code
Dobre opanowanie nawigacji w IDE to często największy przyspieszacz przy pracy z legacy code w Javie. Kluczowe funkcje:
- Go to declaration / implementation – szybki skok do definicji klasy/metody.
Korzystanie z hierarchii wywołań i odniesień
Przy dużej bazie kodu sam Go to declaration nie wystarcza. Potrzebujesz widoku, który pokaże, kto woła dany fragment i co on sam wywołuje. Tu wchodzą:
- Call Hierarchy – drzewo wywołań metody/klasy.
- Find Usages / References – lista miejsc użycia klasy, metody, pola.
Prosty schemat pracy:
- Stoisz na metodzie podejrzanej o bycie „centralnym punktem” (np.
OrderService.process()). - Uruchamiasz Call Hierarchy i oglądasz, z ilu miejsc jest wołana.
- Jeśli lista jest długa – oznacza to silne sprzężenie. Przed refaktoryzacją trzeba ogarnąć, które wywołania są krytyczne.
W IntelliJ zwróć uwagę na różnicę między „Find Usages” a „Highlight usages in file”. Pierwsza opcja pomaga w skali projektu, druga – w obrębie jednej klasy. Przy dużych God Classach ta druga jest wygodna, bo od razu widzisz, jak wewnątrz klasy używane są konkretne pola i metody.
Debugowanie starego kodu bez strachu
Debugger to najkrótsza droga, żeby zweryfikować hipotezy o działaniu systemu. Kilka praktyk, które oszczędzają czas:
- Ustawiaj breakpointy warunkowe (np. zatrzymaj się tylko, gdy
orderId == 123L). - Korzystaj z log breakpoints (w IntelliJ) – zamiast zatrzymywać program, wypisz stan do loga i pozwól mu biec dalej.
- Debuguj poziom wyżej niż chcesz refaktoryzować (np. kontroler zamiast prywatnej metody), żeby nie utknąć w detalach.
Przykład: masz skomplikowany PricingService i trzy różne typy rabatów. Ustaw breakpoint w metodzie publicznej, przepuść dwa–trzy różne scenariusze zamówień i poobserwuj, które warunki faktycznie się wykonują. Wiele martwych gałęzi logiki samo wyjdzie na wierzch.
Przegląd historii w Gicie jako narzędzie analizy
Historia zmian często wyjaśnia „dlaczego jest tak brzydko”. Kilka rzeczy, które można szybko sprawdzić:
- git blame na problematycznym fragmencie – kto i kiedy go wprowadził, z jakim komentarzem.
- Historia pliku – czy był wielokrotnie ruszany w ostatnim czasie, czy to „skamielina”.
- Połączenie z Jirą – jeśli w commit message są numery ticketów, odszukaj zgłoszenie i zobacz kontekst biznesowy.
Jeżeli widzisz, że klasa była dotykana co sprint, a każda zmiana dorzuca kolejne if (specialCase), masz kandydata na wydzielenie nowego modułu lub refaktoryzację warunków do strategii.
Analiza statyczna jako filtr problemów, nie lista TODO
Narzędzia typu SonarQube potrafią wygenerować setki ostrzeżeń. Żeby nie utonąć:
- Na początek filtruj po module, nad którym pracujesz, a nie po całej bazie.
- Skup się na Bug i Vulnerability, a nie na kosmetyce typu konwencje nazewnicze.
- Twórz krótkie sesje „cleanup” – np. 30 minut tygodniowo na wybrane, poważniejsze ostrzeżenia.
Jeśli narzędzie wskazuje na potencjalne NullPointerException w starym kodzie, to dobry pretekst, żeby dodać test, który reprodukuje sytuację i dopiero potem ją naprawić.
Metryki złożoności i rozmiaru klas
Większe projekty dobrze przeskanować metrykami (np. pluginy w IntelliJ, SonarQube). Przydatne wskaźniki:
- Cyclomatic complexity – ile gałęzi warunków ma metoda.
- Lines of code (LOC) na klasę/metodę – ile dokładnie jest kodu.
- Depth of inheritance – jak głęboka jest hierarchia dziedziczenia.
Nie ma sensu ślepo gonić za „idealnymi” wartościami, ale lista metod o najwyższej złożoności to często dobry backlog refaktoryzacyjny. To tam zacznij od testów charakterystyki.
Bezpieczne wprowadzanie testów do nieprzetestowanego kodu (testy charakterystyki)
Na czym polegają testy charakterystyki
Test charakterystyki nie sprawdza „czy system działa poprawnie”. Sprawdza „czy system dalej zachowuje się tak samo jak przed zmianą”. Nawet jeśli to zachowanie zawiera bugi – na początku chodzi o stabilizację.
Tego typu testy:
- Odwzorowują obecne zachowanie, często w formie „snapshotu”.
- Pozwalają bezpiecznie refaktoryzować, bo wykryją niezamierzone zmiany.
- Stają się bazą do późniejszej korekty biznesowej (najpierw refaktoring, potem zmiana zachowania).
Wybór miejsca na pierwsze testy
Zamiast mierzyć w „idealne pokrycie testami”, skup się na:
- Najczęściej używanych endpointach REST / akcjach w UI.
- Procesach batchowych, które robią dużo w tle (faktury, importy).
- Miejscach, gdzie awaria najmocniej uderza w biznes (płatności, generowanie raportów).
W praktyce często zaczyna się od jednego kontrolera lub jednego serwisu. Dopiero gdy ich zachowanie jest „zabetonowane” testami, można bez stresu poprawiać środek.
Jak pisać testy charakterystyki w istniejącej architekturze
Najczęstszy błąd: próba natychmiastowego pisania czystych testów jednostkowych w mocno splecionym kodzie. Znacznie łatwiej zacząć od:
- Testów integracyjnych wokół Spring Boot / Spring MVC.
- Testów na poziomie serwisu z wstrzykiwanymi atrapami (Mockito).
Przykładowy szkielet testu charakterystyki dla kontrolera Spring:
@SpringBootTest
@AutoConfigureMockMvc
class OrderControllerCharacteristicsTest {
@Autowired
private MockMvc mockMvc;
@Test
void shouldCreateOrderWithDefaultDiscountForNewCustomer() throws Exception {
mockMvc.perform(post("/orders")
.contentType(MediaType.APPLICATION_JSON)
.content("{"customerId": 1, "items": [...]}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.discount").value(0.0))
.andExpect(jsonPath("$.status").value("CREATED"));
}
}
Taki test nie rozumie jeszcze „dlaczego jest 0.0”, ale dokumentuje, że tak jest. Po refaktoryzacji natychmiast zobaczysz, jeśli coś ruszyłeś poza planem.
Testy charakterystyki dla legacy bez Springa
Stare aplikacje bez Springa często opierają się na new rozrzuconym po kodzie i pojedynczych klasach‑serwisach. Tam można:
- Pisać testy na poziomie „public API” klas (np.
OrderProcessor.process(order)). - Dla trudnych zależności stosować wzorzec „Sprout Method” – nową logikę wydzielać do nowej metody, która jest już testowalna.
- W ostateczności użyć bibliotek pozwalających na mockowanie statyków (np. Mockito +
mockStatic), ale traktować to jako tymczasowy most.
Zabezpieczanie zachowania przez testy danych
W wielu systemach duża część logiki jest zakodowana w danych (np. konfiguracje rabatów w bazie). Dla takich przypadków przydatne są:
- Fixture’y testowe – małe zestawy danych SQL/JSON odtwarzające rzeczywiste scenariusze.
- Testy „input → output” z konkretnymi, realnymi danymi.
Podejście: weź kilka reprezentatywnych rekordów z produkcji (po anonimizacji), odpal proces na nich lokalnie i zapisz wynik jako oczekiwany. Potem kod refaktoryzujesz, a dane we/wy w testach muszą się zgadzać.
Stopniowe zmniejszanie zasięgu testów integracyjnych
Na starcie dominują testy integracyjne, bo inaczej nie da się złapać systemu za „uchwyt”. Warto jednak stopniowo:
- Identyfikować fragmenty logiki, które można odseparować (np. kalkulacja rabatu).
- Wyciągać je do małych klas/serwisów z czystymi zależnościami.
- Dopisując do nich testy jednostkowe, jednocześnie redukując ilość asercji na wyższym poziomie.
W efekcie po jakimś czasie testy integracyjne pilnują głównych ścieżek, a cięższa logika ma swoje, szybsze i bardziej precyzyjne testy jednostkowe.
Podstawowe techniki refaktoryzacji w Javie stosowane na starym kodzie
Mikro‑refaktoryzacje wspierane przez IDE
Przy legacy łatwo zrobić za duży krok. Bezpieczniej jest korzystać z małych ruchów:
- Rename (klasa, metoda, pole) – z IDE, tak żeby zmiana objęła cały projekt.
- Extract Method – wydzielanie fragmentu do osobnej metody.
- Inline Variable/Method – uproszczenie przepływu, gdy coś stało się zbędne.
- Introduce Parameter Object – gdy metoda ma zbyt wiele parametrów.
Zasada: jedna zmiana refaktoryzacyjna = jeden commit. Dzięki temu łatwo znaleźć, co poszło nie tak, jeśli testy zaczną padać.
Rozbijanie długich metod
Metody po kilkaset linii są standardem w starym kodzie. Prosty schemat ich rozbijania:
- Zidentyfikuj logiczne kroki (np. „walidacja → kalkulacja → zapis → notyfikacja”).
- Wyciągnij każdy krok do osobnej, prywatnej metody z sensowną nazwą.
- Na początek nie zmieniaj sygnatur ani nazw parametrów, jedynie wydzielaj kod.
Po takim ruchu metoda nadrzędna staje się czymś w rodzaju scenariusza biznesowego, który łatwo przeczytać, a poszczególne kroki można później przenosić do osobnych klas.
Eliminacja duplikacji przez wydzielanie wspólnych komponentów
Duplikacja logiki w legacy to klasyk. Zamiast od razu polować na wszystkie powtórzenia:
- Wybierz jedno, krytyczne miejsce (np. logika naliczania opłaty).
- Znajdź 2–3 oczywiste duplikaty.
- Wydziel wspólną część do nowej metody/serwisu, przepnij te 2–3 miejsca.
Dopiero gdy nowy komponent okazuje się stabilny, warto systematycznie przepinać kolejne miejsca. W przeciwnym razie drobna pomyłka przy
