Ostatnio w zespole prowadziliśmy dyskusję na temat refaktoryzacji kodu. Z ust jednego programisty padła opinia, że „kod który jest commitowany do repozytorium podczas refaktoryzacji, musi być w 100% poprawny”. Nie jest tutaj istotne, że np. wyodrębniliśmy istniejącą metodę do oddzielnej klasy. Kod, który widnieje jako zmiana, w jego opinii musi być dobry w 100%, koniec kropka.
Ja z tym się nie zgadzam i właśnie to stwierdzenie zainspirowało mnie do napisania artykułu. Tradycyjnie sięgnąłem do różnych źródeł, aby nie przekazywać Wam treści opartej tylko na swoich doświadczeniach, ale również posłuchać głosu innych.
Artykuł jest dosyć długi, zatem podzieliłem go na dwie sekcje:
- pierwsza dotyczy osobistych przemyśleń i doświadczeń oraz potencjalnych problemów, z którymi możemy się spotkać,
- druga zaś pokazuje możliwy sposób pracy nad refaktorowanym kodem.
Na samym początku chciałbym przytoczyć cytat z książki Roberta C. Martina „Czysty Kod”:
Skauci amerykańscy mieli prostą zasadę, jaką można zastosować w naszym zawodzie. Pozostaw obóz czyściejszym, niż go zastałeś
Robert C. Martina „Czysty Kod”
Czy to oznacza zatem, że zostawiony kod musi być w 100% poprawny? Niekoniecznie… Czyściejszym nie znaczy czystym. Natomiast jeżeli każdy z nas będzie zostawiał kod lepszy, kiedyś stanie się idealny (przynajmniej w teorii 😉 )
W związku z tym, że w programowaniu do wielu rzeczy można podciągnąć stwierdzenie „to zależy”, przyjmijmy założenie, że rozmawiamy o złożonym projekcie i skomplikowanym kodzie, z większą liczbą zależności.
Jakie zagrożenia widzę, kiedy uparcie będziemy próbować zostawić kod w 100% poprawny, a osoba która sprawdza twój kod za żadne skarby nie będzie chciała zaakceptować Twoich zmian, bo przecież można poprawić jeszcze kilka rzeczy…?
Cechy osobowościowe i preferencje
Zacznę od kwestii mniej technicznych. Myślę, że każda osoba ma swoja granicę bólu pisania w bardzo brzydkim kodzie. W związku z tym zrzucanie na plecy jednej osoby większego refaktoringu, który ma trwać kilka tygodni, może nie być dobrym pomysłem. Osoba taka, zamiast myśleć o refaktoringu zaczyna myśleć o urlopie.
Oczywiście, to o czym napisałem jest uzależnione od danych preferencji. Pracowałem z osobami, które lubiły dłubać różne dziwne rzeczy i nie męczyło ich całodniowe kopiowanie INSERTów do bazy danych. Inni z kolei wolą widzieć od razu efekt pracy. Zmieniasz coś w kodzie, klikasz F5 i masz owoc swojej zmiany. Bardzo często podczas większego refaktoringu na taki efekt jednak trzeba poczekać.
Zmieniające się potrzeby biznesowe
Kwestia kolejna i tutaj muszę odnieść się do refaktoringu, podczas gdy nasz system żyje wraz z logiką biznesową. Musimy wtedy zarówno refaktorować kod jak i wprowadzać zmiany „na już”. Zapewne w takiej sytuacji jeden programista wprowadzi zmiany w istniejącym kodzie, które trafią one na produkcję. Druga osoba, która odpowiada za refaktoring pobierze zmiany, rozwiąże konflikty i tak aż do końca procesu refaktoringu.
Co jeżeli takich zmian jest więcej?
Ja osobiście spotkałem się kilka razy z taką sytuacją i zdarzało się, że rozwiązanie w ogóle na produkcję nie trafiło. Wszystko zmieniało się tak dynamicznie, że wyrównywanie zmian było niemożliwe. Finalnie straciliśmy masę roboczogodzin, zaangażowania po stronie testerów, programistów, analityków, a efekt końcowy był porażką.
Można powiedzieć, że jest to kwestią planowania, świadomości product ownerów itd. Jasne, ale czy nigdy nie zdarzyło się Wam, że dane rozwiązanie musiało trafić na produkcję „na już”, bo Pan X i Pani Y tak sobie zażyczyli?
Jak zatem możemy podejść do kwestii refaktoringu?
Powyższe przemyślenia skłaniają mnie do przyjęcia zasady małych kroków.
Odniosę się jednak do „Piramidy Refaktoringu”, którą zaczerpnąłem od Pana Włodka Krakowskiego (https://www.refactoring.pl/). Podejście to składa się na 5 głównych kroków i wydaje się być bardzo przemyślane, dlatego bez zawahania polecę ten sposób. Szczególnie, że Pan Włodek ma 19 lat doświadczenia w tym co robi…
Krok 1. Jak działa i co robi nasz kod? Uprośćmy logikę i algorytmy
Bardzo często przy istnieniu dużych metod, okazuje się że jest w niej jakiś mający sens algorytm, jednak metoda ta robi kilka czynności, które możemy podzielić na mniejsze kroki. W początkowym etapie możemy poprawić niektóre drobne rzeczy, takie jak np. nazewnictwo zmiennych, złożoność warunków itd.
Krok 2. Przekształć kod na mniejsze metody
Podział metod na mniejsze pozwala nam na lepszą pracę z kodem poprzez szybsze zrozumienie logiki, uproszczony proces szukania błędów itd. Wyobraź sobie, że masz jedną dużą metodę oblicz()
, na którą składa się 400 linii kodu, a sam proces obliczeń składa się z 3 kroków. Załóżmy, że dzielimy 400 linii kodu na 3 mniejsze metody.
oblicz() { // 400 linii kodu } oblicz() { obliczProwizje(); odejmijProwizje(); obliczZysk(); }
Czy teraz nie łatwiej będzie ten kod czytać i debugować? Jeżeli pojawi się błąd w metodzie oblicz()
, możemy prześledzić wyniki każdej z poszczególnych metod i na etapie konkretnej metody znaleźć błąd. Oczywiście metoda obliczProwizje()
, odejmijProwizje()
, obliczZysk()
również może być podzielona na mniejsze części.
Krok 3. Wyodrębnij klasy
Jeżeli wyodrębniliśmy metody, zapewne okaże się że naszą logikę możemy przypisać do jakiś obiektów. Zauważ, że w naszym przykładzie metoda oblicz, liczy zarówno prowizję jak i zysk. Czy to nie jest dobry moment na stworzenie obiektów? Czy nie można wyodrębnić czegoś odpowiedzialnego za prowizję i czegoś odpowiedzialnego za zysk?
Krok 4. Wprowadź wzorce projektowe
Kolejnym krokiem może być użycie wzorców projektowych. Możemy zastanowić się w jaki sposób klasy się ze sobą komunikują i jak to wszystko można poskładać na wyższym poziomie.
Krok 5. Architektura
Wróćmy teraz do problemu… Czy wyodrębniając metodę musimy ją przepisać i zachować 100% czysty kod? W mojej opinii nie.
Samo wydzielenie metody, czy zmiana jej nazwy jest już refaktoringiem, a przecież ciało metody może w początkowym etapie zawierać skopiowany kod. Tutaj już poszliśmy do przodu. Z wielkiej metody oblicz()
wyodrębniliśmy trzy mniejsze. Kiedy inny programista będzie musiał zmienić coś w algorytmie liczenia prowizji, najprawdopodobniej będzie szukał jej w meodzie obliczProwizje()
, a nie w starej złożonej metodzie.
Zamiast poświęcać tydzień na refaktoring metody oblicz()
i wszystkich mniejszych metod, mogliśmy poświęcić pół dnia na wyodrębnienie fragmentu logiki. Dodatkowo zmiany mogą szybciej trafić na produkcję. Zmniejszamy tym samym ryzyko modyfikacji tej samej logiki w istniejącym jak i refaktorowanym kodzie, w sytuacji gdy pojawią się nowe potrzeby biznesowe, dotyczące tego samego fragmentu kodu.
O takich drobnych zmianach pisze też Robert Martin w pozycji „Czysty Kod”:
Czyszczenie nie musi być duże. Wystarczy zmiana jednej nazwy zmiennej na lepszą, podział funkcji, która była nieco za duża, wyeliminowanie małego powtórzenia, wyczyszczenie jednej złożonej instrukcji if.
Punkt widzenia „biznesu” na refaktoring
Chciałbym poruszyć jeszcze kwestię punktu widzenia naszego „biznesu” na problem refaktoringu.
Dla naszego właściciela produktu, bardzo często jakość kodu jest mało istotna. Dla niego liczy się zysk i to jak działa program. Kto z Waszych właścicieli biznesowych na pytanie, czy mogę poświęcić 3 tygodnie na refaktoring z uśmiechem na twarzy powie „proszę bardzo, daje Ci nie trzy tygodnie, a cztery”? Żyjemy w ciągłym gonieniu deadlinów, braku zasobów, zmianach wymagań itd. Taki drobny refaktoring jest w stanie wejść do waszego projektu niezauważonym. Przyjmijmy, że w naszym przykładzie i metodzie oblicz()
musisz wprowadzić drobną zmianę. Łatwiej będzie nam przy okazji drobnej zmiany w logice wydzielić metody i oddać daną funkcjonalność do testów, niż otrzymać zgodę na przygotowanie tej zmiany przez dwa tygodnie, przepisując przy tym cały kod. Tym samym każda kolejna modyfikacja logiki w wyodrębnionej funkcji, może ją delikatnie usprawniać i metodą małych kroków osiągniemy większy sukces!
Pamiętajcie, aby stosując taki „ukryty refaktoring”, zaznaczyć osobom odpowiedzialnym za testy, aby skupiły się również na sprawdzeniu zmienionych przez nas obszarów.
Jak zacząć?
Patrząc od strony programistycznej, w mojej opinii warto podejść do tego z pomysłem. Może jakieś spotkanie, zebranie problemów, poszukania rozwiązań, przyjęcie wspólnego schematu działania lub standardów, będzie pierwszym krokiem do rozpoczęcia refaktoringu. Na pewno nie zabierałbym się do tego na zasadzie „robimy refaktoring”, bez żadnych ustalonych wspólnie zasad. Warto też po kilku iteracjach zastanowić się co zrobiliśmy dobrze, a co jeszcze można poprawić. Cykliczne spotkania, które będą poruszały kwestie refaktoringu na pewno będą pomocne.
U mnie to nie zadziała, czyli każdy projekt jest inny…
Jak to w programowaniu – zwrot „to zależy” pasuje bardzo często. Tak również w przypadku refaktoringu to stwierdzenie znajdzie zastosowanie. Nie musicie się zgadzać ze wszystkim co jest napisane w artykule, bo być może specyfika Waszego projektu jest zupełnie inna i niektóre rzeczy będą działały super, a niektóre w ogóle. Mam jednak nadzieję, że artykuł przyda się niektórym i jeżeli cokolwiek dzięki tej treści ruszy do przodu w Waszym projekcie, będę czuł satysfakcję. 🙂
Źródła:
Przedstawiony flow przy refaktoringu został przygotowany na podstawie piramidy refaktoryzacji, ze strony Pana Włodka Krakowskiego. Osoba ta zajmuje się zawodowo refaktoryzacją. Przy okazji odsyłam do jego strony https://refactoring.pl/, gdzie znajduje się też kilka przykładów kodu jak również piramida refaktoringu i garść innych ciekawych informacji.
Robert C. Martin – „Czysty Kod” ISBN: 978-83-283-0234-1
Obraz: https://unsplash.com/photos/1LLh8k2_YFk