12 wniosków i dobrych praktyk z refaktoryzacji kodu

Niedawno dostałem do przyspieszania i uproszczenia aplikację. Pomyślałem sobie: refaktoryzacja – super! W szczególności, że miałem okazję pracować z tym kodem i wiedziałem, że nie jest z nim różowo.

Cezary Łysiak. Programista fullstack od początku związany z technologiami Microsoft. Obecnie pracuje w YieldPlanet. Uwielbia pracę kompleksową wykraczającą poza samo kodowanie tj. uczestnictwo w procesie wytwarzania oprogramowania od pozyskania projektu i architektury po wdrożenie i utrzymanie.
Od jakiegoś czasu wymądrza się na swoim blogu https://pozakodem.pl/ oraz aktywnie działa w inicjatywie https://fabrykatestow.pl/.
Prywatnie mąż, ojciec. Pływa, czyta i… próbuje grać na ukulele.

Użytkownicy skarżyli się na powolne działanie, project owner skarżył się na powolne powstawanie nowych funkcjonalności, wreszcie programiści skarżyli się na to, że bardzo trudno pracuje się z kodem. Oczywiście taka sytuacja trwała już od jakiegoś czasu, ale (jak to zwykle bywa w takich przypadkach) skala problemów z rozwojem kodu przeważyła szalę potrzeby gruntowanych zmian.

Zanim przejdziemy do wniosków, ustalmy zastaną sytuację:

  • stack: Angular.JS (pisany w TS), .NET (C#),
  • brak dostępu do dwóch, pierwszych autorów kodu,
  • kod rozwijany przynajmniej przez trzech programistów, bez spójnych i wspólnie stosowanych ustaleń architektonicznych,
  • brak dokumentacji (oprócz szczątkowej – częściowo nieaktualnej),
  • bardzo oszczędne komentarze w kodzie.

Nie będę Was okłamywał. Nie wyglądało to za dobrze. W moim przypadku była to jednak zachęta niż „podcięcie skrzydeł”. Wyzwania to jest to wyrywa z codziennego „klepania” w klawiaturę.

Co się udało osiągnąć:

  • stosunkowo prosty kod,
  • prostszy interfejs API,
  • aplikacja szybsza 2-3 razy,
  • 50% mniej kodu (!) przy tej samej funkcjonalności.

12 wniosków, dobrych praktyk i technicznych przypominaczy, o których warto wiedzieć przy refaktoryzacji kodu

1. Zrozum kod!

Kuszące jest, aby od razu zatopić się w meandry kodu i poprawiać „wszystko co się rusza”. Jeżeli refaktoryzacja ma przynieść długoterminowe korzyści należy rozpocząć od zrozumienia, jak i dlaczego kod działa i wygląda. Zazwyczaj w takim przypadku radziłabym zaczynać kolejno od:

  • architekta rozwiązania,
  • twórcy kodu,
  • dokumentacji,
  • komentarzy w kodzie.

W moim przypadku jednak wszystkie te rzeczy były dostępne w bardzo ograniczonej formie. Pomimo, że przyniosły część odpowiedzi – moją najlepszą bronią była inżynieria wsteczna (https://pl.wikipedia.org/wiki/In%C5%BCynieria_odwrotna). Dużo debugowania. Dużo rysowania zależności na kartkach. Kilka diagramów UML  np. przepływu, aktywności – wybierz swoje ulubione.

Nie zrozum mnie źle. Nie twierdzę, że należy wpadać w ekstremum i rozpisywać wszystko do najmniejszego bool’a w kodzie. Zrozumienie kodu powinno być jednak stosunkowo pełne w zakresie kluczowych funkcjonalności.

2. Zaplanuj i zaprojektuj zmiany (dokumentacja na bieżąco)

The sooner you start to code, the longer the program will take. – Roy Carlson, University of Wisconsin

Zakładam, że już teraz jest to Twój nawyk, a więc tylko przypominam. Zanim coś napiszesz – pomyśl. W przypadku refaktoryzacji jest to szczególnie ważne, ponieważ często zmieniasz zamysł architektoniczny kodu. Konieczne jest zaprojektowanie zmiany w formie dokumentacji.

W moim przypadku dokumentacji prawie nie było, więc zacząłem ją tworzyć od początku. Gdy dokumentacja jest dostępna, należy ją uaktualnić.

Trzy dobre rady:

  • swój plan skonsultuj z innym programistą – druga para oczu to nieoceniona pomoc w szukaniu potencjalnych problemów
  • dokumentację prowadź systematycznie – nie ma nic gorszego niż 3 dni pisania dokumentacji: gdy będziesz to robił na bieżąco, to ta niezbyt przyjemna czynność powinna być mniej dokuczliwa.
  • zanim zaczniesz, a zasady czystego kodu nie są jeszcze mocno „wyryte” w Twoim sercu – wróć do lektury „Czystego kodu” Wujka Boba.

3. Dużo testuj – pisz testy

Brak testów w przypadku projektów „legacy” nie jest szokiem. Zrób sobie przysługę i od razu napisz kilka (jednostkowych, integracyjnych, być może wydajnościowych). Podczas refaktoryzacji dojdzie do regresji. Zapewniam Cię, że tylko dzięki testom wykryjesz ją odpowiednio szybko.

Dodatkowo sporo testuj manualnie i często wypuszczaj wersje do sprawdzenia przez innych członków zespołu. Nie zwalnia Cię to oczywiście z samodzielnego testowania, ale po pewnym czasie nie sposób rzetelnie sprawdzić jakość wytworzonych przez siebie funkcjonalności.

I argument ostateczny. W przypadku, gdy zostawisz napisanie testów „na później”, to na 90% nie powstaną one nigdy.

4. Dziel i rządź

Stara z brodą zasada, ale w przypadku refaktoryzacji bardzo ważna. Staraj się odizolować od siebie funkcjonalności, które zmieniasz, i zajmuj się nimi pojedynczo. Przy pracy nad każdą sprawdzaj, czy zmiana:

  • jest spójna z nowym planem architektonicznym,
  • nie wprowadziła regresji w tej i innych funkcjonalnościach (testy),
  • upraszcza maksymalnie dany fragment programu.

5. Sporo commituj

W szczególności przy refaktoryzacji to istotne. Jeżeli do tej pory nie zaprzyjaźniłeś się ze swoim systemem kontroli wersji, to przyszedł na to najwyższy czas. To, co najważniejsze, to łatwość powrotu z niezbyt przemyślanych ścieżek i śledzenie swojej ścieżki myślenia w przypadku bardziej skomplikowanych zmian.

6. Jak rozumiesz funkcjonalność, a kodu nie, to go wykasuj

One of my most productive days was throwing away 1000 lines of code. – Ken Thompson, early developer of UNIX OS

Ot tak. Po prostu. Nie staraj się przerabiać kodu, którego nie możesz pojąć w skończonym czasie. Jeżeli wiesz jak funkcjonalność działa to wiesz wszystko, aby wytworzyć nowy, prostszy kod. Być może w skład funkcjonalności muszą wejść skomplikowane obliczenia – skomentuj je. Ja wiem, że dobrze napisany kod sam się dokumentuje. Postaw się jednak w sytuacji innego programisty – czasami dwa zdania pomogą mu zrozumieć Twój zamysł zdecydowanie szybciej.

7. Logika na backend

Nowe frameworki frontendowe pozwalają coraz szybciej pisać coraz bardziej złożony kod. Fakt ten sprawia, że coraz więcej osób zapomina o tym, w jakim celu faktycznie powstały.

Po co Angular nie został stworzony:

  • odciążenie backend,
  • miejsce na dalszą transformację danych (pierwszy etap na backend – drugi na frontend).

Po co Angular powstał:

  • tworzenie i renderowanie aplikacji klienckich,
  • dynamiczne i automatyczne zmiany w DOM.

Co ciekawe, powyższe fakty znajdowały się kiedyś na oficjalnej stronie Angular.js. Teraz ich nie ma albo nie potrafię ich znaleźć. Tak czy inaczej – kierując się tą wiedzą, warto się zastanowić, co powinno się zaleźć po stronie backendu i wszystko bez litości tam przenosić. Oczywiście część logiki powinna zostać na frontendzie. Nie bądźmy purystami, ale zalety posiadania logiki sensownie umieszczonej na backendzie są oczywiste.

8. Uprość interfejs API

Z doświadczenia tego i innych projektów wiem, że z upływem czasu API rozrasta się. Zazwyczaj jest to spowodowane niedostateczną wiedzą (i dociekliwością) programistów. Po prostu, gdy czegoś potrzebuje, dodaje kolejny endpoint („końcówkę”). Warto się temu przyjrzeć podczas refaktoringu – znacząco uprości to kod i logikę projektu.

Ciekawą techniką jest próba scalenia podobnych wywołań w jedną końcówkę, aby potem się zastanowić, jak to sensownie podzielić, a czasami okazuje się, że tak jest wystarczająco prosto. Nie zawsze to działa, ale to ciekawy eksperyment.

9. Uważaj na dyrektywy Angular.js

Możliwości, jakie dają nam takie frameworki jak Angular, kuszą, aby korzystać z nich bez opamiętania. Należy jednak zachować umiar (jak w każdym innym przypadku w programowaniu). Jako przykład, który napotkałem w refaktoryzowanej aplikacji, przedstawiam niepozorną dyrektywę ng-class.

Dla niewtajemniczonych, w dużym skrócie, dodaje lub usuwa style CSS z elementu HTML. Bardzo wygodna rzecz. W zależności od jakiegoś parametru przycisk ma być zielony lub czerwony – nic prostszego.

W przypadku, z którym się zetknąłem dyrektywa ng-class wywoływała funkcję, która sprawdzała czy dany dzień jest dniem weekendowym i na tej podstawie ustalała kolor komórki w tabeli. Aby było troszkę ciekawiej, data była trzymana jako string. I teraz, jak już pewnie sobie wyobrażacie, w tabeli z dziesiątkami komórek, przy każdym odświeżeniu DOM każdy string był parsowany do daty (konkretnie momentu) i było sprawdzane czy to dzień weekendowy czy nie. Tych operacji były dziesiątki tysięcy. I po co? Przecież 13 maja 2020 zawsze okaże się tym samym dniem tygodnia – zawsze! A przecież można to sprawdzić raz na backend’zie i zwrócić tabelę (komórki), które już wiedzą czy są reprezentacją soboty czy niedzieli🙂

To tylko jeden z możliwych przykładów. Trzeba brać pod uwagę cykl życia DOM w aplikacji Angular.js i zastanowić się, czy niektóre zabiegi są rzeczywiście konieczne.

10. Używaj $q

Ponownie krótki wstęp dla niewtajemniczonych. $q to serwis (w Angular), który pomaga wywoływać asynchroniczne funkcje i używać ich wyników, kiedy skończą się procesować.

W szczególności ciekawe jest użycie, gdy wywołujemy kilka funkcji asynchronicznych, a do dalszego procesowania potrzebne nam są rezultaty wszystkich z nich. Użycie $q bardzo upraszcza kod, bo (w pewnym sensie) synchronizuje asynchroniczne wywołania. Nie zagnieżdżamy wywołań, dzięki czemu kod jest czytelny, prosty i zrozumiały dla potomnych.

11. Ustal (lub wyczuj) kiedy pora skończyć

Pokusa nieustannego poprawiania jest ogromna. Warto mieć ustalony punkt zakończenia na podstawie:

  • kryteria (wydajnościowe, pokrycie testami, uproszczenie interfejsu),
  • wykorzystany czas i/lub zasoby,
  • „uczucie”, że kod jest wystarczająco prosty.

Oczywiście najlepiej zastosować obiektywne kryteria, ale głównym celem jest nie wpadnięcie w wir dążenia do perfekcjonizmu (który nie istnieje). Jeżeli ja Ciebie nie namówiłem, to może posłuchasz premiera Wielkiej Brytanii:

Perfection is the enemy of progress. – Winston Churchill

12. Wyłącz emocje: nie wyżywaj się – zrozum i rób swoje

Ostatni punkt, ale możliwe, że najważniejszy. Nie analizuj, co się działo w głowie autora kodu. Nie wyżywaj się na nim w myślach ani tym bardziej słownie. Na pewno istnieją powody, dla których kod doszedł do tego poziomu, w którym go zastałeś. Oczywiście czasami jest to kwestia braku wiedzy czy doświadczenia. Czasami jest to pośpiech, w szczególności ten odgórnie narzucony.

Nie ma to znaczenia.

Potraktuj to zadanie jako wyzwanie dla siebie. Refaktoryzacja to proces, w którym możesz bardzo wiele się nauczyć na temat:

  • zasad czystego kodu,
  • prawidłowej konstrukcji API,
  • dogłębniej zrozumieć framework, w którym pracujesz i poznać jego dobre praktyki,
  • poznać ciekawe przypadki kodu optymalnego (i tego mniej optymalnego).

Łatwo jest wpaść w „spiralę nienawiści”, ale zaufaj mi – niczemu to nie służy.

Podsumowanie

W czasie, gdy pracowałem nad refaktoryzacją kodu w opisywanym na początku projekcie, niejednokrotnie miałem dość. Kilka razy musiałem wrócić do etapu projektowania. Swoim kodem powodowałem regresje w na pierwszy rzut oka niepowiązanych miejscach w systemie. I notorycznie chciałem poznać osobę, która doprowadziła kod do takiego stanu.

Z perspektywy czasu jednak zauważyłem, że miło wypominam ten okres. Wykazałem się – pomimo, że nie była to moja pierwsza refaktoryzacja. Sporo się nauczyłem – pomimo sporego doświadczenia w branży. W końcu codziennie się czegoś uczymy!

Zostawiam Ci te rady, bo mam nadzieję, że jak sam staniesz przed takim wyzwaniem, podejdziesz do tematu procesowo i przypomnisz sobie chociaż jedną z nich. Jeżeli chociaż trochę Ci to pomoże, to super! Trzymaj się i powodzenia!

 

Zdjęcie ilustrujące artykuł pochodzi z pexels.com

Zapraszamy do dyskusji

Patronujemy

 
 
More Stories
Ścieżka kariery Senior Technical Leadera