Każdy projekt do którego dołączyłem zawsze miał jakiś stan zastany. Oznacza to nic innego jak wgryzanie się w kod legacy od pierwszego dnia pracy. Kiedyś uznawałem, że to tylko stan przejściowy. Że kiedyś w końcu dołączę do projektu z “zieloną trawą”. Teraz… już nie chcę. Zacząłem zauważać w legacy coś niesamowitego. Że są to miesiące, jak nie lata, pracy moich poprzedników, którzy musieli borykać się z różnymi ówczesnymi problemami. Nie znam dokładnie ich sytuacji z przeszłości, więc trudno mi oceniać ich pracę. Jednak wierzę w głębi, że starali się zrobić to co mogli najlepiej jak potrafili.
Dzisiaj chciałbym przedstawić Ci czym jest refaktoryzacja. Według mnie jest to nieodłączna część pracy z kodem legacy. W każdym projekcie dowozimy nowe funkcjonalności. Doklejamy nowy kodzik do tego już istniejącego. Gdybyśmy funkcjonowali tylko w ten sposób, bez sprzątania, to po kilku iteracjach taki kod byłby ciężki w utrzymaniu. W danej chwili mogło nam się wydawać, że dodawany kod pasuje idealnie w wybrane przez nas miejsce. Jednak z biegiem czasu zaczyna on nam tam tylko przeszkadzać. Zaczynamy robić obejścia, które coraz bardziej zaciemniają obraz całości. Dlatego warto się raz na jakiś czas zatrzymać na chwilę i spojrzeć krytycznie na bieżącą sytuację. Zastanowić się co możemy zrobić, aby ten kod był czytelniejszy, bardziej utrzymywalny. “No i zacząć to robić”, cytując klasyka. I właśnie tutaj wkracza pojęcie refaktoryzacji.
Czym jest refaktoryzacja?
Wyobraźmy sobie sytuację, w której chcemy zamówić krzesło do naszego biura. Znaleźliśmy firmę, która wykonuje je na specjalne zamówienie. Wystarczy tylko wysłać specyfikację jak dane krzesło ma wyglądać (input), aby je otrzymać jako fizyczny obiekt (output). Właśnie w ten sposób opisaliśmy tzw. obserwowalne zachowanie dla użytkownika. Z jego perspektywy nie jest istotne JAK jego zamówienie zostanie wykonane. Dla niego najważniejsze jest to CO on dostanie. Oczywiście mówimy tutaj o “fizycznym” aspekcie. Patrząc na to z innej perspektywy - jasne jest, że wolałby on dostać krzesło jak najszybciej. Jednak szybkość wytworzenia krzesła nie powinna mieć wpływu na to, że nie będzie ono spełniało swoich funkcji, które zostały wcześniej zadeklarowane.
Odróżnienie obserwowalnego zachowania od implementacji
To właśnie jest ważne z punktu widzenia refaktoringu. Bo załóżmy, że chcielibyśmy przyspieszyć wytwarzenie krzeseł w naszej firmie np. wymieniając ludzi na roboty. Przy wymianie nie chcielibyśmy oczywiście dopuścić do tego, żeby ta zmiana była widoczna dla klientów pod kątem namacalności produktu. Czyli przekładając to na software. Chcemy wymienić “bebechy” tego w jaki sposób coś robimy nie martwiąc się o to, że coś popsujemy. Nie chcemy mieć na głowie tego, że nasza aplikacja nagle zacznie działać inaczej niż to miało miejsce przed wymianą, czyli refaktoryzacją.
Jak refaktoryzować?
Koncepcja refaktoryzacji sama w sobie nie jest trudna. Wymieniamy jedną rzecz na drugą. Jednak w praktyce niesie ona za sobą strach… Strach przed tym, że coś popsujemy. Boimy się poprawiać kod, bo być może zmienimy coś w taki sposób, że zacznie to działać kompletnie inaczej niż to miało miejsce przed zmianą. Jasne, jest to jak najbardziej uzasadnione. Jednak z jakiegoś powodu istnieją testy, które mają nas zabezpieczyć przed taką sytuacją. Ale co jeśli ich NIE MA?! Albo gorzej. Istnieją, ale dają mylne poczucie bezpieczeństwa, bo są w kiepskim stanie i nic nie wyłapują. Tutaj sprawa ma się o wiele gorzej. Nie chcę teraz wymieniać wszystkich znanych mi strategii dla takiej beznadziejnej sytuacji, skupię się na kilku z nich.
Refactoryzacja bez testów - Korzystanie z IDE
Najbardziej podstawową rzeczą, którą możemy robić, aby nie czuć strachu przed tym, że coś popsujemy, jest mechaniczna refaktoryzacja prowadzona przez nasze IDE takie jak IntelliJ. Jest cała masa możliwości, jakie dają nam tego typu narzędzia. Weźmy na tapet wcześniej wspomnianego IntelliJ. Ctrl + Alt + M
towarzyszy mi każdego dnia. Ten skrót pozwala nam na wyodrębnienie metody. Naszą odpowiedzialnością jest jedynie wybranie interesującego nas bloku kodu i dostarczenie nazwy dla nowopowstałej metody. Całą resztą zajmie się IDE. Jeśli coś nie będzie możliwe to zostaniemy o tym natychmiast poinformowani.
Kolejnym moim ulubionym skrótem jest Ctrl + Alt + P
. Jego odpowiedzialnością jest wyodrębnienie parametru aktualnej metody. Wybieramy zmienną, wciskamy skrót klawiszowy, nadajemy nazwę dla parametru i voila!. Ma to wiele ciekawych zastosowań, o których opowiem innym razem.
Ostatnim przypadkiem jest brak dopasowania metody do aktualnej klasy. W kodzie ewidentnie widać, że nie pasuje ona do miejsca, w którym jest. Możemy to łatwo zmienić. Wystarczy, że naprowadzimy kursor na sygnaturę metody i wciśniemy F6
. IntelliJ podpowie nam, gdzie możemy przenieść naszą metodę. Po wybraniu idealnego kandydata, IDE zrobi całą resztę. Czasami może to nie do końca zadziałać tak jak chcemy, ale łatwo się z tego wycofać.
Także widać, że nasze środowisko to nie tylko jakiś tam zwykły edytor tekstu. Ma on sporo ciekawych dodatków, z których warto korzystać w swojej codziennej pracy.
Refactoryzacja bez testów - Wprowadzenie dedykowanych klas
Kolejnym sposobem na mało inwazyjną refaktoryzację jest wprowadzenie klas, które niosą ze sobą jakiekolwiek znaczenie biznesowe (np. Value Objects). Co to oznacza w praktyce. Załóżmy, że mamy system, który działa na samych typach prymitywnych typu String
czy int
. Wszystko wygląda identycznie dla nas, jak i kompilatora. Dwa byty można odróżnić od siebie jedynie po nazwie - numer karty kredytowej, imię właściciela konta czy nazwa ulicy w adresie zamieszkania. Co możemy z tym zrobić? Jedną z możliwości jest stworzenie dedykowanych klas takich jak CardNumber
, FirstName
czy StreetName
. Wrapujemy typ prymitywny w taką klasę i przekazujemy dalej po aplikacji. Wtedy kompilator zabezpiecza nas przed złym przekazaniem np. parametrów, bo mamy dla nich dedykowany typ. Jednak wprowadzając taką klasę moglibyśmy przeorać całą aplikację. Jak się pewnie domyślasz, to nie tędy droga.
Zobaczmy na przykładzie co możemy zrobić. Załóżmy, że chcemy wprowadzić klasę CardNumber
. Znajdujemy zmienną String cardNumber
i wrzucamy ją do konstruktora nowopowstałej klasy. Teraz mamy silnie typowany obiekt dedykowany dla numeru karty. Ważne w tym wszystkim jest, gdzie postawimy tak zwane punkty odcięcia. Nie chcemy tej zmiany rozsiać od razu po całym systemie. Dlatego musimy stworzyć dedykowane metody w nowej klasie, które tłumaczą daną wartość na stary kontrakt. Mowa tutaj o metodzie takiej jak public String asString()
. Jeśli mamy dosyć dalszych zmian piszemy cardNumber.asString()
i zamykamy temat.
Wprowadzenie dedykowanej klasy to naprawdę mało inwazyjna zmiana. Jej wprowadzenie będzie ułatwione przez wsparcie kompilatora. Podpowie nam on, gdzie skorzystać z metody tłumaczącej nowe podejście na stare. Oczywiście system nie będzie wyglądał od razu ładnie, ale jest to mały krok w dobrym kierunku.
Podsumowanie i złota rada
To tyle na ten moment. Mam nadzieję, że te kilka sposobów pozwoli Ci się pozbyć strachu przed refaktoryzacją. Spróbuj, naprawdę warto. Weź swój aktualny projekt i nawet nie wystawiaj pull requsta. Po prostu pobaw się powyższymi sposobami lokalnie, aby zyskać wprawę i pewność siebie. Upewnisz się, że refaktoryzacja to nic strasznego nawet jak nie ma się testów (oczywiście inwazyjne metody wymagają odpowiednich zabezpieczeń). Zobaczysz, że nic nie popsujesz. Postaram się w przyszłych artykułach opisać inne sposoby refaktoryzacji. Zachęcam Cię na koniec do zapoznania się z jednym z moich wcześniejszych artykułów. Opisałem w nim sposób na pozycie się splatania dwóch encji JPA. Do następnego!