Jak nie zwariować przy pracy z LEGACY CODE?

Większość programistów prędzej czy później trafi na projekt, który będzie zawierać legacy code. Warto być odpowiednio przygotowanym na takie spotkanie, gdyż każde z nich jest unikalnym przeżyciem, które gwarantuje sporą dawkę stresu, poczucie bezsilności, a czasem wzbudza irytację. Jak zatem podjeść do tematu, żeby nie zwariować i osiągnąć upragniony cel? O tym już za chwilę.

Legacy code – z czym to się je?

Nasze rozważania należałoby rozpocząć od szczegółowej definicji legacy code. Będzie to bowiem kluczowe dla zrozumienia opisywanej problematyki. Polskie tłumaczenie podaje, iż jest to „kod odziedziczony”, nazwa ta nie wydaje się być jednak do końca odpowiednia. Pierwotnie oznaczał on kod, który jest zależny od niewspieranej wersji systemu, języka, frameworka bądź innego oprogramowania. Utrzymuje się, ponieważ nadal działa i przynosi rezultaty. Pojęcie legacy code z czasem zaczęło ewoluować w społeczności programistycznej, dlatego ta definicja, mimo że jest prawidłowa, może nie być wystarczająca. Temat ten podejmuje Michael Feathers w książce zatytułowanej „Working Effectively with Legacy Code”. Posługuje się w niej legacy codem rozumianym jako „kod bez testów”. Co to oznacza? Otóż można stworzyć aplikację od zera, która już w dniu wypuszczenia będzie zabytkiem z punktu widzenia programisty. To jednak nie jest w tym wypadku najważniejsze. Powodem, dla którego testy według Feathersa są tak istotne, jest możliwość sprawdzenia, czy po zmianie system nadal będzie działał w oczekiwany sposób. Moim zdaniem źle napisane testy są równie, a niekiedy nawet bardziej, niebezpieczne. Jakie mogą być powody tego, że programista nie napisał testów lub zrobił to po prostu źle? Najpewniej nie zdawał sobie sprawy z istnienia testów automatycznych bądź nie potrafił tego zrobić. Innym czynnikiem mógł być na przykład brak czasu. W pierwszym wypadku już samo to gwarantuje bardzo niską jakość kodu, zaś w drugim może wskazywać na problemy z architekturą aplikacji oraz tym, jak jest napisana. 

Kod o słabej jakości bardzo ciężko się testuje. Zazwyczaj, gdy patrzymy na kod legacy, jest to w mniejszym lub większym stopniu spaghetti rozwijane bez większego zastanowienia nad sposobem długoterminowego utrzymania systemu. Podsumowując, Legacy Code charakteryzuje brak bądź złe pokrycie testami, co tworzy bałagan w kodzie i architekturze. Jeżeli dołączymy do tego łamanie dobrych praktyk, utrwalane często przez „pokolenia” developerów, to otrzymamy niezbyt przyjemny obrazek. Z tym wszystkim musimy sobie radzić my – niewinni całej sytuacji.

Czy naprawdę jest aż tak źle?

Nie ma oczywiście co wyolbrzymiać. W najgorszym przypadku, trafimy na spaghetti bez testów, w którym każda zmiana może przynieść zupełnie nieoczekiwane problemy, a to może okazać się naprawdę irytujące. W rzeczywistości jednak rzadko natrafiamy na taką sytuację. Systemy tego rodzaju nadal się utrzymuje, ponieważ cały czas działają i prawdopodobnie zarabiają pieniądze (z których część idzie na Twoją pensję). Reprezentują zatem realną wartość biznesową, lecz aby pieniądze się zgadzały, należy raz na jakiś czas zrobić w systemie generalne porządki. Może to być refactoring lub dopisanie kilku testów. Żaden biznes nie może sobie przecież pozwolić, żeby jego produkt przestał całkowicie działać. Jak się okazuje, przepisanie systemu od nowa w 99% przypadków nie jest dobrym rozwiązaniem, gdyż wygeneruje olbrzymie koszty, a nie ma gwarancji, że będzie lepszy od poprzedniego, zarówno pod względem biznesowym, jak również kosztów utrzymania. Wymienić można także więcej plusów pracy nad systemami legacy, o czym pisał niedawno Robert Pankowiecki na blogu Arkency (polecam lekturę).

Akcja!

Wyobraź sobie, że siadasz do pracy nad tego typu systemem i masz dopisać nowy feature. Zdajesz sobie sprawę, że gdy go zrobisz, jedyną metodą sprawdzenia czy działa, będzie ręczne przeklikanie nowej funkcji. Chyba że zmienisz kod w paru innych miejscach… Przy okazji analizujesz kod coraz głębiej i okazuje się, że pół systemu jest ze sobą splątane, dlatego pewnie kilkanaście procent istniejących testów trzeba będzie naprawić po dorobieniu feature’a. Musisz zdecydować, co zrobić.

Złe wybory przy pracy z Legacy code

Podczas pracy z Legacy code najczęściej popełniane są dwa podstawowe błędy. Pierwszy z nich dotyczy przepisywania od początku po swojemu całego kodu. Taka taktyka wymaga dogłębnej analizy i szczegółowej wiedzy o systemie, zaś koszt pozyskania tej wiedzy bywa bardzo wysoki. Trwa to bowiem dość długo i może kosztować sporo stresu. Gdy duża zmiana zostanie przeprowadzona na gorąco, to jest duże prawdopodobieństwo, że system przestanie działać poprawnie. Wcześniejsze pokolenia developerów pracowały nad rozwiązaniem konkretnych problemów, o których ty możesz nie mieć nawet pojęcia. Czas pokazał, że te rozwiązania zadziałały – nawet jeżeli są źle napisane. Wchodzi tu kwestia długu technologicznego, ponadto skokowa zmiana generuje dodatkowe koszty związane z dłuższym okresem stabilizacji i naprawy innych funkcji, których ta zmiana dotknęła. Zazwyczaj nie zgadza się na to projekt manager, a i dla nas taka sytuacja nie będzie zbyt przyjemna. 

Drugi popularny błąd to „zrobię to tak, jak zrobili to inni przede mną”. Jeżeli cenisz sobie jakość i programowanie, to rzadko będzie to satysfakcjonujące rozwiązanie. Z punktu widzenia systemu oznacza bowiem dalsze zaciąganie długu technologicznego. Należy przy tym pamiętać, że od tego długu płaci się odsetki. W pewnym momencie może to sparaliżować rozwój oprogramowania. Dobrym przykładem jest tu moje własne doświadczenie zawodowe. Pracowałem dla klienta, który w 2012 roku odziedziczył spory system, co prawda działał, ale nie był dobrze napisany. Od tego czasu nowy właściciel kodu napisał mnóstwo kodu, wzorując się na starym stylu. Moim zadaniem był upgrade rozwiązań najbardziej przestarzałych, ale miałem okazję podglądać pracę teamu pracującego nad rozwojem tego oprogramowania. Jakie były konsekwencje? Wydanie kolejnych wersji miało opóźnienie od 2 tygodni do ponad miesiąca (przy dwumiesięcznych wydaniach). Moment wgrania na produkcję był równoznaczny z gaszeniem pożaru, tickety były zamykane i otwierane na przemian po kilka razy, ze względu na problemy znalezione w późniejszym czasie. 

Jaka droga jest właściwa?

Kluczowe przy pracy z takim kodem jest nastawienie. Powtarzanie sobie, że ten kod jest słaby niczego nie zmieni, a jedynie obniży morale. Co zatem należy zrobić? Po pierwsze – skoncentruj się na zadaniu, które masz do wykonania. W przypadku problemów zastanów się, czy realnie możesz je rozwiązać w sensowny sposób. Zapytaj innych programistów, z czego wynika takie rozwiązanie. Często będą w stanie powiedzieć coś o historii tego kawałka kodu. Postawa pro-aktywna to podstawa, ale przygotuj się, że nie każdy pomysł będziesz w stanie zrealizować.

Z mojego doświadczenia wynika, że w większości przypadków najbardziej optymalne jest robienie małych usprawnień. Polega to na tym, że jeżeli pracuję nad jakąś częścią kodu, to za każdym razem staram się ją zostawić w lepszym stanie niż ją zastałem. Przykładem usprawnienia jest dopisanie testów na niesprawdzone przypadki, które w rzeczywistości mogą wystąpić. Lokalny refactoring klasy, tak by była bliżej SOLID (ale nie musi być od razu perfekcyjna!), czy wprowadzenie brakującej abstrakcji. Te małe zmiany są dopuszczalne tam, gdzie widzę i rozumiem zależności z innymi częściami systemu. Ważne jest pokrywanie tych zmian testami, dzięki którym kolejne pokolenia będą miały ułatwione zadania tego typu. Te usprawnienia procentują – często inny developer przyjmie podobne rozwiązanie, by rozwiązać taki sam problem, z czasem może wykiełkować z tego całkiem znaczna zmiana. Fakt, że była oddolna – czyli mniejsze komponenty uległy zmianie jako pierwsze – przekłada się później na niższy koszt kolejnych usprawnień.

To, o czym piszę nie zawsze jest łatwe, czasami może okazać się bardzo trudne lub wręcz niemożliwe. Trzeba mieć otwarty umysł i rozpatrywać różne rozwiązania. Warto również przeczytać wspomnianą książkę Michaela Feathersa, w której szczegółowo opisane są konkretne techniki radzenia sobie nawet z bardzo ciężkimi przypadkami.

Porady końcowe

Legacy code to nie koniec świata, więc nie ma czym się stresować. Warto robić swoje i czerpać z niego wiedzę, o tym, czego nie należy robić. Owszem, brzmi to ogólnikowo i takie jest w rzeczywistości. To właśnie o ogólne podjeście do problemu chodzi. Przy odrobinie szczęścia i inteligentej pracy już po paru miesiącach dostrzeżemy efekty naszych zmian. Kiedyś pracowałem przy projekcie, w którym przy każdym wydaniu musiała być dostępna grupa „ochotników” do gaszenia potencjalnego pożaru, jednak gdy odchodziłem nowa wersja i jej wgranie do klienta nie było już niczym nadzwyczajnym.