3 triki refaktoringu. Techniki refaktoryzacji kodu w praktyce [przykłady]

Pociąga Cię refaktoryzacja kodu, ale nie za bardzo wiesz, jak się za nią zabrać? W niniejszym artykule omawiamy trzy techniki refactoringu, z którymi przywracanie czystego kodu staje się łatwiejsze: refaktoryzację zduplikowanego kodu zgodnie z DRY, refaktoryzację złożonych warunków przez polimorfizm i refaktoryzację do pojedynczej odpowiedzialności.

Transparent Data
Blog Transparent Data
7 min readApr 8, 2021

--

Refaktoryzacja kodu: Co, gdzie i kiedy (intro)

Refaktoryzacja, tudzież z angielskiego refactoring lub z naszego ulubionego spolszczonego angielskiego refaktoring kodu, od jakiegoś już czasu bije rekordy popularności. Z uwagi na to, że polega na stopniowym odświeżeniu małych fragmentów kodu bez zaburzania logiki ich działania, to perfekcyjnie sprawdza się w bieżącym przeciwdziałaniu zaciągania długu technologicznego oraz w radzeniu sobie z wyjściem z legacy systems. Dlaczego?

Refaktoryzacja kodu ma między innymi takie niezaprzeczalne zalety:

  • jest łatwa w testowaniu i bezpieczna we wdrożeniu (skoro logika działania pozostaje bez zmian, wówczas ryzyko wywalenia się całego systemu jest nikłe),
  • można ją stosować nawet w codziennych sytuacjach, np. przy okazji dodawania jakiejś nowej funkcjonalności czy przeprowadzania Code Review (nie trzeba sobie na refactoring zaklepywać od razu kilku tygodni w przód; wystarczy często dzień lub dwa),
  • jako, że nie opiera się na zmianie całej infrastruktury i nie wymaga mapowania wszystkich procesów na nowo tak, jak się to dzieje w przypadku code rewrite, jest bardzo optymalna kosztowo.

Poniżej omawiamy trzy triki-techniki dobrego refaktoringu kodu.

Refactoring Trik nr 1: Refaktoryzacja zduplikowanego kodu zgodnie z DRY

Jeżeli jesteś programistą, to z pewnością słyszałeś już wiele razy o zasadzie DRY, czyli Don’t Repeat Yourself. Wie o niej każdy, nawet początkujący. Wiedzieć a stosować, to jednak gigantyczna różnica. Niby wszyscy wiedzą, że nie powinno się duplikować kodu, ale finalnie każdemu się zdarza czasem nieuważnie ulec przeciwności zasady DRY i zastosować WET (We Enjoy Typing / Write Everything Twice).

Z uwagi na to, że w praktyce bardzo często mamy do czynienia z powielaniem tego samego kodu w wielu miejscach, refaktoryzacja zgodnie z DRY jest jednym z najczęściej stosowanych trików na ulepszenie zastanego kodu. Dlaczego? Bo dzięki niej niesłychanie łatwo jest namierzyć fragmenty programistycznego kodu, który trzeba refaktoryzować. Wystarczy jedynie zastosować ułatwiającą wszystko zasadę Rule of Three.

Rule of Three: do trzech razy sztuka i robimy refaktoring kodu!

  1. Kiedy robisz coś po raz pierwszy, po prostu piszesz kod.
  2. Za drugim razem, gdy programujesz coś podobnego, kopiujesz kod.
  3. Za trzecim razem, gdy znów robisz coś podobnego, możesz to wyodrębnić i zastosować refactoring.

Zasada ta pomaga podjąć decyzję, kiedy warto jest wyodrębnić zduplikowany kod. Co więcej, warto ją stosować nawet w swojej własnej, codziennej pracy, aby odświeżyć kod i zapewnić mu czystość. W skrócie chodzi w niej o to, że gdy zobaczysz duplikację kodu po raz trzeci, to pora ją wyodrębnić. To już jest refaktoryzacja.

Refaktoryzacja kodu zgodnie z DRY — przykład

Jako przykład weźmy taki fragment kodu:

Refaktoryzacja kodu z DRY — przykład 1a

Dwa obiekty, Car i Home, mają tu taką samą metodę cleanWindows. Jest ona zatem zduplikowana i nadaje się do refaktoringu.

Refaktoryzację tego fragmentu możemy zrobić m.in. przez wydzielenie klasy abstrakcyjnej i wygląda to wówczas tak:

Refaktoryzacja kodu z DRY — przykład 1b

To rozwiązanie nie jest jednak idealne. Wprawdzie udało nam się wyeliminować duplikację kodu, jednak należy zaznaczyć, że hierarchia zależności nie jest tu do końca poprawna. Samochód i dom, czyli nasze obiekty Car i Home, są zupełnie różnymi obiektami w rozumieniu biznesowym. Jeżeli program pisany jest na przykład dla firmy sprzątającej, która musi być w stanie ocenić ile ma jeszcze okien do umycia w autach a w domach, i na tej podstawie ocenić liczbę potrzebnych w danym dniu pracowników, to taka refaktoryzacja wcale nie zostawi naszego kodu czystszym.

W takim przypadku, zdecydowanie lepiej jest sięgnąć po mechanizm ponownego wykorzystania kodu, który jednocześnie pozwala ominąć limity dziedziczenia, czyli trait:

Refaktoryzacja kodu z DRY — przykład 1c

Jak widać, poprzez zastosowanie mechanizmu ponownego wykorzystania kodu, klasy Car i Home nie dziedziczą już po tym samym rodzicu, więc hierarchia obiektów jest w tej chwili prawidłowa.

Refactoring Trik nr 2: Refaktoryzacja złożonych warunków za pomocą polimorfizmu

Jak to mawiał Grady Booch:

Czysty kod jest prosty i bezpośredni.

Stąd, skomplikowane, złożone warunki logiczne, to kolejne fragmenty kodu, które zawsze warto jest przepuścić przez refactoring. Natykamy się na nie nadzwyczaj często szczególnie w starszych systemach typu legacy systems. Łatwo jest rozpoznać je po tym, że główny obiekt, w zależności od posiadanego atrybutu (np. typu), wykonuje różne obliczenia lub akcje. Tak napisany kod programistyczny jest szalenie trudny do rozwijania i mocno komplikuje późniejszy development. Coś z nim trzeba zatem zrobić.

Techniką refaktoryzacji, która jest piekielnie przydatna w upraszczaniu złożonych warunków jest polimorfizm. Pozwala on wyabstrahować wyrażenia od konkretnych typów, dzięki czemu nadal możemy używać zmiennych i wartości na rozmaite sposoby. Jego zaletami jest to, że umożliwia sprawne pozbycie się masowych duplikatów, a co więcej, sprawia, że dodanie nowego wariantu wykonania ogranicza się tylko do dodania nowej podklasy, co nie wymaga ingerencji w już istniejący kod.

W zrozumieniu na czym polega ta technika refaktoryzacji pomaga znajomość zasady Tell-Don’t-Ask. Właśnie na niej opiera się polimorfizm. Nie pyta on obiektu o jego stan, a później na podstawie odpowiedzi wykonuje jakieś działania. Nie. On po prostu mówi obiektowi, co ten ma zrobić, przy czym obiekt może sobie sam wybrać w jaki sposób chce to zrobić.

Na czym polega refaktoryzacja złożonych warunków przez polimorfizm?

Gdyby spróbować stworzyć mini przewodnik po polimorfizmie, wyszłoby nam mniej więcej coś takiego:

  1. Utwórz podklasy pasujące do gałęzi tego złożonego warunku, czyli przygotuj hierarchię klas.
  2. W stworzonych klasach utwórz jedną współdzieloną metodę, a następnie przenieś kod z odpowiedniej gałęzi warunku do tej nowej metody.
  3. Zamień warunek na odpowiednie wywołanie metody.

Rezultat? Właściwa implementacja zostanie osiągnięta poprzez polimorfizm zależący od klasy obiektów.

Refaktoryzacja złożonych warunków za pomocą polimorfizmu — przykład

Załóżmy, że mamy klasę pojazd (Vehicle), która w zależności od posiadanego typu (‘’bus”, “truck”, “car”) musi posiadać różne dokumenty do kontroli (VehicleDocuments).

Refaktoryzacja złożonych warunków przez polimorfizm — przykład 1a

W jednym z fragmentów tej klasy zastosowano metodę ze złożonymi warunkami Switch:

Refaktoryzacja złożonych warunków przez polimorfizm — przykład 1b

Dlaczego warunek Switch jest tu problematyczny? Cóż, należy on do tzw. code smells. Zauważmy, że w tym konkretnym przypadku sprawia on, iż nie możemy dalej rozwijać klasy bez modyfikacji tego warunku. Zaburza więc on wzorzec OCP. Co za tym idzie, aż się tu prosi o refaktoring kodu za pomocą polimorfizmu, czyli wstawienie zamiast Switch pewnych klas dedykowanych do konkretnych typów:

Refaktoryzacja złożonych warunków przez polimorfizm — przykład 1c
Refaktoryzacja złożonych warunków przez polimorfizm — przykład 1d

Teraz, po zastosowaniu refaktoryzacji za pomocą polimorfizmu, można już dodawać nowe podklasy, a nie za każdym razem modyfikować kod.

Refactoring Trik nr 3: Refaktoryzacja do pojedynczej odpowiedzialności (SRP)

Jeżeli jakiś klasa posiada wiele odpowiedzialności, na przykład dwie: generowanie biletów do muzeum i ich drukowanie, to innymi słowy odpowiada ona za dwa procesy, które niezależnie od siebie mogą ulec zmianie w przyszłości. Zmianie może ulec treść biletów, jak i podobnie ich wydrukowany format. W takim przypadku, zdecydowanie lepiej (i bardziej przyszłościowo dla czystego kodu) jest rozdzielić te dwie odpowiedzialności, tak aby działały niezależnie od siebie.

Według sformułowanej przez Roberta C. Martina zasady pojedynczej odpowiedzialności (SRP):

Nigdy nie powinno być więcej niż jednego powodu do istnienia klasy bądź metody.

A zatem, gdy widzisz fragment z więcej niż jedną odpowiedzialnością — czas na refaktoring.

Refaktoryzacja do pojedynczej odpowiedzialności — przykład

Jako przykład kodu, który trzeba refaktoryzować, weźmy ten tutaj:

Refaktoryzacja do pojedynczej odpowiedzialności — przykład 1a

Jak widać, klasa Post posiada wiele odpowiedzialności.

Gdy przeprowadzimy jej refaktoryzację zgodnie z zasadą pojedynczej odpowiedzialności, Post nadal zostaje w naszym kodzie, ale odpowiada już tylko za generowanie treści. Z kolei, za generowanie JSON’a i pdf’ów odpowiadają osobne klasy JsonPostGenerator i PdfPostGenerator:

Refaktoryzacja do pojedynczej odpowiedzialności — przykład 1b

Po refaktoringu, każda z klas posiada osobną odpowiedzialność. Dodatkowo, można by się pokusić nawet o stworzenie wspólnego interface’u dla generatorów PostGeneratorInterface, który wymagałby zaimplementowania metody generate w klasach go implementujących.

Techniki refaktoryzacji — podsumowanie

Jak się zapewne domyślasz, powyższe trzy techniki refaktoryzacji (DRY, polimorfizm i SRP) nie wyczerpują tematu w pełni. Przydatnych trików, które pomogą nam przywrócić czysty kod i zredukować dług technologiczny jest znacznie więcej. O ile zadaniem Twojego zespołu nie jest akurat stopniowy refactoring kodu jakiegoś legacy system dla zewnętrznego klienta, gdzie koniecznym jest założyć sobie wielomiesięczny plan odświeżenia najbardziej newralgicznych punktów, to warto jest stosować refaktoring kodu na co dzień, przede wszystkim w trzech sytuacjach:

  • podczas naprawy bugów w systemie,
  • podczas dodawania nowych funkcjonalności,
  • przy okazji Code Review.

Dzięki takiemu nawykowi, nasz kod znowu stanie się czystym kodem programistycznym — przechodzącym wszystkie testy, wolnym od błędów, czytelnym dla innych programistów i tańszym w utrzymaniu.

--

--