Pojęcie refaktoryzacji może być interpretowane dwojako. Z jednej strony może być rozumiane jako czynność – czasownik. Wtedy przez refaktoryzację rozumiemy ulepszenie kodu, który już powstał, pamiętając, że nie zmieniamy działania kodu, który już został napisany. Innymi słowy, nie powinniśmy być zaskoczeni, że funkcjonalność kodu nie uległa zmianie, ponieważ nie dodajemy nowych mechanizmów. Pojęcie refaktoryzacji może być także rzeczownikiem, oznaczającym zmianę, której dokonujemy.
Zanim podejmiemy się refaktoryzacji, powinniśmy rozważyć kilka kroków. Bardzo często przez refaktoryzację rozumiany jest skrajny przypadek – siadamy, przepisujemy pół systemu, moduł lub być może n modułów. Z czasem zauważono, że znacznie lepszą, bezpieczniejszą, poprawną opcją jest zupełnie inne podejście, czyli przeprowadzanie wielu bardzo małych zmian najczęściej w większej ilości.
Zmiana nazwy zmiennej, nazwy metody są to podstawy, które każdy wielokrotnie realizował, jednak te kroki również są uznawane za refaktoryzację. Lepiej wprowadzić n małych, bezpiecznych zmian niż rozgrzebać połowę systemu, co może doprowadzić do jego zatrzymania. Należy podejmować kroki techniką asekuracyjną. Trzymając się tej zasady, poprawia się nam nawet estymacja czasu. Ryzyko niepotrzebnego spalenia dużej ilości czasu będzie mniejsze.
Refaktoryzacja – kiedy jest potrzebna?
Niejednokrotnie trudno jest odpowiedzieć sobie na pytanie, kiedy powinniśmy zacząć coś poprawiać. Każdy z nas musi nauczyć się, kiedy i jak podejmować się refaktoryzacji. Problemem jest tutaj to, że nie da się tego wyjaśnić w książkowy, zero-jedynkowy sposób. Jest to coś, co poznajemy z czasem, ucząc się szacować korzyści i straty. Inaczej mówiąc, każda zmiana na poziomie kodu, wprowadzenie wzorca projektowego, dodatkowej abstrakcji, zmiana API ma swoje konsekwencje, które zmuszą nas do wprowadzenia zmian na poziomie kodu, np. zmiany kompatybilności.
Z drugiej strony, z każdą zmianą możemy coś zyskać. Być może większą elastyczność, większą łatwość wymiany implementacji, większą generyczność, która pozwoli nam w przyszłości łatwiej wprowadzić nową funkcjonalność. Dobrym przykładem może być zamiana instrukcji warunkowych na enumy.
Przykład
Przeanalizujmy przykład, w którym mamy do obsługi nowe API. W ramach przykładu API ma dostępne tylko metody typu CRUD, które przyjmują odpowiedni obiekt implementujący InformationType w zależności od rodzaju operacji. Załóżmy, że w tym momencie jesteśmy na etapie implementacji uzupełniania danych do metod API. Najprostszym rozwiązaniem byłoby obsłużenie go za pomocą ifów/switchów.
Nie są to jednak zbyt dobre rozwiązania. Dodajemy nowy kod oparty na instrukcjach warunkowych –ifach, switach, co nie jest najlepszym i czytelnym podejściem, ponieważ taki kod prawdopodobnie będzie się rozrastał w przyszłości. Osoba pisząca kod będzie dodawać kolejne instrukcje warunkowe, co pogorszy jakość, czytelność i skomplikowanie logiki, jak i testów. Innym możliwym rozwiązaniem może być zastosowanie np. wzorca Chain of Responsibility.
Często zdarza się, że firma wystawiająca API wprowadza nowe wersje, modyfikując przy tym m.in. wejściowe struktury danych. Powyższy kod da się zastąpić przy pomocy enuma, nie używając instrukcji warunkowych. W przyszłości pozwoli na łatwiejsze ponowne wykorzystanie zaimplementowanego mechanizmu lub rozszerzenie go na nowe wersje, funkcjonalności danego API.
Całe wywołanie będzie wyglądać następująco:
Cały kod można znaleźć tutaj. W tym miejscu warto zatrzymać się na chwilę i zastanowić, czy oczekiwany rezultat z zastosowania enuma nie jest tak naprawdę odpowiedzą, dlaczego decydujemy się poświęcić czas na refaktoryzację. Dlaczego to robimy? Odpowiedź jest prosta – tak, to właśnie główny cel refaktoringu.
Refaktoryzacja – ważne kwestie
Zastanawiając się dalej nad refaktoringiem, zapewne pojawią się kolejne wątpliwości:
- Kiedy dokonujemy refaktoryzacji?
- Czy zatrzymujemy się, chcąc dokonać refaktoryzacji i przez długie dni/tygodnie nie robimy nic nowego, aby móc wykonać refaktoryzację? Czy może jednak jest to coś, co powinniśmy wykonywać na bieżąco?
- Kto powinien robić refaktoryzację? Wyznaczone osoby, czy może wszyscy?
- Czy jest “lepszy” moment, aby refaktoryzować?
- Czy wzorce projektowe to wszystko, czego potrzebujemy do refaktoryzacji?
Odpowiedzi na te pytania mogą nie być zawsze jasne, jednak w dzisiejszych czasach się ustabilizowały. W większości przypadków jako dobrą praktykę refaktoryzacji, powinniśmy ją przeprowadzać w sposób ciągły. Powinien ją robić każdy w zespole, każdego dnia.
Kiedy jest najlepszy czas na refaktoryzację?
Gdy planujemy dodać do naszego kodu nową funkcję
Możemy wtedy zastanowić się, czy nie dałoby się go wprowadzić łatwiej/szybciej, gdyby najpierw wprowadzić poprawki w kodzie. Lepszy kod pozwoliłby napisać “łatwe” testy. Takie podejście proponują między innymi Martin Fowler i Kent Beck w jednej ze swoich książek.
Po zakończeniu implementacji danej funkcji
Ten moment wydaje się być zupełnie naturalny i prawdopodobnie jest to podejście najczęściej stosowane przez osoby implementujące dane rozwiązanie. Jest on integralnym krokiem TDD. Możemy się wtedy na chwilę zatrzymać i poprawić napisany przed chwilą kod. Mamy wtedy gotową i przetestowaną logikę nowej funkcjonalności. Nieraz możemy zauważyć, że część moglibyśmy zrobić lepiej/ciekawiej pod kątem przyszłego utrzymania.
Gdy poznajemy nowy kod
Z czasem nawet utarły się słowa, że osoby tworzące oprogramowanie częściej czytają kod niż go tworzą lub – dokładniej mówiąc – rozwijają. Brak zrozumienia i utrzymania istniejącego kodu jest receptą na katastrofę!
Gdy cały zespół prowadzi refaktoring przez kilka dni, a nawet dłużej
Taki moment powinien być jednak wyjątkiem od reguły. Może się zdarzyć np. gdy planujemy migrację pomiędzy narzędziami. Kolokwialnie mówiąc, o kod powinniśmy dbać tak, jak dbamy o porządek w swoich domach, gdzie wynosimy śmieci, sprzątamy co 2-3 dni.
Dodatkowe wskazówki
Codzienne podnoszenie jakości, podejmowanie pewnych kroków przełoży się na to, że dodawanie nowych funkcjonalności będzie dużo szybsze, łatwiejsze, sprawniejsze, co ostatecznie wpłynie na jakość kodu. Tutaj widzimy, że trudno mówić o refaktoryzacji, mając duże sprzężenie i niską spójność, czyli zły podział odpowiedzialności.
Idąc dalej, możemy powiedzieć, że już na tym poziomie mogą być naruszone niektóre z podstawowych dobrych praktyk pisania kodu – zasady SOLID[i], KISS[ii]czy TDA[iii]. Bez odpowiedniego podziału odpowiedzialności nie mamy czystego kodu, czyli już na tym poziomie ciężko mówić o refaktoryzacji.
Dodatkowo, jeśli kod nie jest czysty, to zwykle nie mamy do niego testów będących naszym zapleczem bezpieczeństwa. Tak naprawdę nie da się pominąć żadnego z tych trzech elementów –
- czystego kodu,
- refaktoryzacji,
- testowania.
Należy wykonywać wszystko razem inaczej szybko można się zniechęcić, bo któryś z elementów może być trudny do poprawnego zaimplementowania.
Najważniejsze jest to, aby osoba pracująca z kodem to wszystko wyważyła, ponieważ czasami wartość korzyści będzie niewielka, nawet niezauważalna. Mówiąc inaczej, po co robić bardziej generyczny kod teraz, skoro być może w przyszłości nikt nie będzie go potrzebował. Należy uważać, aby nie popaść w pułapkę robienia zbyt elastycznego rozwiązania. Należy robić rzeczy iteracyjnie, gdyż często warto poczekać, ze zmianami, aż pewne rzeczy staną się jasne. Dokładniej mówi nam o tym zasada YAGNI[iv].
Należy jednak pamiętać, że wprowadzając refaktoryzację, nie wprowadzamy nowych funkcjonalności, co od razu stawia zasadę, że refactoring musi być bezpieczny. Refaktoryzacja w oderwaniu od testów, czystego kodu, dobrej architektury i wielu innych czynników będzie miała bardzo mały sens. Receptą na niezłamanie wspomnianej przed chwilą zasady jest istnienie dobrych testów. W przeciwnym wypadku refaktoryzacja może stać się bardzo trudna i niebezpieczna. Może się też okazać, że będzie miała ona niewielki sens oraz będzie dużo bardziej kosztowna niż gdybyśmy się jej nie podjęli.
Syndromy Code Smells
Wiemy już, przynajmniej w teorii, jak i kiedy wykonywać refaktoryzację, ale z drugiej strony mamy syndromy, na które musimy być wyczuleni. Potocznie zwane code smell, które powinny być ostrzeżeniem, że coś jest nie tak. Wiele z nich jest zupełnie naturalnych, stosowanych na co dzień, np. złe nazewnictwo, przeładowanie odpowiedzialności.
Zapamiętanie wszystkich code smells może być problematyczne. Znacząco lepiej jest najpierw się z nimi zapoznać, a gdy pojawi się problem odszukać dany code smell i sprawdzić, jak najlepiej sobie z nim poradzić. Więcej szczegółów można przeczytać na stronie sourcemaking. Pisał o tym nawet autor wymienionych artykułów i książki.
Wymienienie i opisanie wszystkich code smells to temat na osobny artykuł. Dla dokładnego zapoznania się z poniższymi polecam artykuł o technikach refaktoringu i bad code smells.
Zestaw najczęstszych code smells/przyczyn niskiej jakości kodu
- Brak trzymania się zasad SOLID, KISS – przesada z abstrakcjami, YAGNI, DRY[v], TDA itp.
- Obsesja na punkcie typów prymitywnych, co powoduje, że kod jest mniej obiektowy. Może przypominać podejście proceduralne co często przekłada się na ifowanie, switchowanie zamiast polimorfizmu. Przykład, jak tego unikać był omówiony wcześniej, np. MovieType.
- Złe zaprojektowanie systemu na poziomie obiektów. Zbyt długa lista argumentów. Maksymalna rekomendowana ilość to 3.
- Może się zdarzyć, że mamy duplikację kodu w różnych częściach kodu, czyli mamy różne klasy, różne kontrakty, ale powtórzoną logikę w metodach klas.
- Nieodpowiednia segregacja odpowiedzialności. Wprowadzenie jednej funkcji powoduje zmiany w wielu miejscach, klasach, typach.
- Brak dobrych testów, brak pokrycia kodu testami.
- Brak wstrzykiwania zależności.
- Nadużywanie programowania imperatywnego, a dokładniej przeładowanie ifami, switachami.
- Instancyjne pola tymczasowe zamiast zmiennych lokalnych w metodach.
- Duże sprzężenie i niska spójność:
- Sprzężenie – inaczej mówiąc siła z jaką współpracują elementy. Nie ważne czy to metoda, komponent czy może mikroserwis. Im większe sprzężenie, powiązanie tym większe kłopoty patrząc pod kątem refaktoryzacji, zmian, utrzymania, testowania. Należy dążyć do jak najmniejszego sprzężenia.
- Spójność – kohezja, czyli dążymy do tego, aby komponent, serwis skupiał się na jednej dobrze zdefiniowanej rzeczy. Im większa spójność tym lepiej.
- Błędy popełniane na poziomie analizy, projektowania oraz implementacji.
- Nieodpowiednie zarządzanie zasobami – presja czasu, zbyt mało ludzi w zespole, zła estymacja.
- Złożoność problemów – dodawanie nowych funkcjonalności, każdy rozwój bez refaktoringu będzie wprowadzać chaos.
- Brak dbałości o czytelność kodu:
- Złe nazewnictwo.
- Zbyt długie funkcje/metody.
- Niepoprawne funkcje/metody:
- Przyjmowanie/zwracanie nulli z metod zamiast opakowań zwracanych typów w Optionale – kłamanie na poziomie kodu innych programistów.
- Niepoprawne dopasowanie wyjątków do warstwy aplikacji, np. IOException znajduje się w warstwie biznesowej, restowej.
- Brak programowania przez kontrakty.
- Stosowanie mutowanych obiektów, które można zastąpić niemutowanymi.
- Stosowanie statycznych pól, klas, metod w nieodpowiedni sposób.
Wzorce projektowe
Należy pamiętać, że znajomość wzorców projektowych do refaktoryzacji nie wystarczy. Wzorce są jedynie otoczką, którą widzimy, co również jest bardzo ważne. Można powiedzieć, że wzorce są realizacją założeń opisanych powyżej, co można łatwo zaobserwować na przykładzie wzorca Proxy.
Możemy sobie wyobrazić, że mamy metodę o nazwie transferFunds. Obsługuje ona przelew między dwoma kontami zgodnie z logiką biznesową. Jednak w przypadku takiej metody bardzo często może się zdarzyć, że musimy otworzyć, zamknąć transakcje, sprawdzić jakieś reguły bezpieczeństwa, zalogować, że nastąpił przelew. Nagle okazuje się, że nazwa metody nie odpowiada temu, co w środku się dzieje. Mamy nadużycie, przedobrzenie, robimy więcej niż powinniśmy. Mówiąc dosadniej, to kłamiemy już na etapie nazwy metody.
Aby tego uniknąć, należy dokonać separacji odpowiedzialności, opakować kod poprzez ustanowienie pośrednika – Proxy.
Przykłady grup najczęściej spotykanych wzorców projektowych z krótkim opisem
- Kreacyjne – pokazują sposób tworzenia metod, klas oraz typów danych.
- Singleton,
- Builder,
- Factory.
- Strukturalne – zależności powiązanych ze sobą obiektów.
- Decorator,
- Proxy (pełnomocnik),
- Facade,
- Chain of Responsibility.
- Behawioralne – zachowania współpracujących obiektów.
- Visitor,
- Observer,
- Strategy,
- Adapter.
Przykłady z dokładniejszym opisaniem wzorców projektowych.
Na koniec zachęcam Was do zrobienia zadania będącego samodzielną próbą refaktoringu.
Źródła
- Refactoring: Improving the Design of Existing Code by Martin Fowler, Kent Beck
- bykowski.pl/wzorce-projektowe
- Sourcemaking.com
- refactoring.guru
[i] podstawowych zasad, którymi należy się kierować podczas programowania obiektowo. Skrót pochodzi od pierwszych liter poszczególnych zasad, są to: single responsibility, open/closed, liskov substitution, interface segregation oraz dependency inversion
[ii] keep it simple stupid – nasz kod powinien być tworzony i utrzymany w taki sposób, aby był dla wszystkich jak najbardziej zrozumiały i jasny
[iii] tell don’t ask – zasada mówi o konkretnym podziale obowiązków pomiędzy naszymi klasami i obiektami, a ich zadaniami
[iv] you ain’t gonna need it – zasada mówi, że w naszym programie powinniśmy umieszczać najistotniejsze funkcjonalności, które w danej chwili będą nam potrzebne
[v] don’t repeat yourself – należy unikać powtarzania tych samych części kodu w różnych miejscach
***
Jeżeli temat refaktoryzacji jest dla Ciebie interesujący, polecamy również inny artykuł naszego eksperta: Podejście do refaktoryzacji w projekcie ze starymi technologiami
Zostaw komentarz