Ucząc się programowania, spędzasz dużo czasu na pisaniu kodu. Większość początkujących programistów uważa, że właśnie tym będą się zajmować w przyszłości. Jest to częściowo prawda, ale praca programisty obejmuje również konserwację i refaktoryzację kodu. Dzisiaj porozmawiamy o refaktoryzacji.
Przykład nieprawidłowej hierarchii:

Refaktoryzacja w CodeGym
Refaktoryzacja jest omawiana dwukrotnie na kursie CodeGym:- Duże zadanie na poziomie 5 zadania Wielowątkowość
- Lekcja na temat refaktoryzacji w IntelliJ IDEA na poziomie 9 zadania Java Collections.
Co to jest refaktoryzacja?
Jest to zmiana struktury kodu bez zmiany jego funkcjonalności. Załóżmy na przykład, że mamy metodę, która porównuje 2 liczby i zwraca prawdę , jeśli pierwsza jest większa, i fałsz w przeciwnym razie:
public boolean max(int a, int b) {
if(a > b) {
return true;
} else if (a == b) {
return false;
} else {
return false;
}
}
To raczej nieporęczny kod. Nawet początkujący rzadko by coś takiego napisali, ale jest szansa. Po co używać if-else
bloku, jeśli można bardziej zwięźle napisać metodę 6-wierszową?
public boolean max(int a, int b) {
return a > b;
}
Teraz mamy prostą i elegancką metodę, która wykonuje tę samą operację, co w powyższym przykładzie. Tak działa refaktoryzacja: zmieniasz strukturę kodu bez wpływu na jego istotę. Istnieje wiele metod i technik refaktoryzacji, którym przyjrzymy się bliżej.
Dlaczego potrzebujesz refaktoryzacji?
Jest kilka powodów. Na przykład, aby osiągnąć prostotę i zwięzłość kodu. Zwolennicy tej teorii uważają, że kod powinien być jak najbardziej zwięzły, nawet jeśli do jego zrozumienia potrzeba kilkudziesięciu linijek komentarzy. Inni programiści są przekonani, że kod powinien być refaktoryzowany, aby był zrozumiały przy minimalnej liczbie komentarzy. Każdy zespół przyjmuje swoje stanowisko, ale pamiętaj, że refaktoryzacja nie oznacza redukcji . Jego głównym celem jest poprawa struktury kodu. Do tego ogólnego celu można zaliczyć kilka zadań:- Refaktoryzacja poprawia zrozumienie kodu napisanego przez innych programistów.
- Pomaga znaleźć i naprawić błędy.
- Może przyspieszyć tempo tworzenia oprogramowania.
- Ogólnie rzecz biorąc, poprawia projektowanie oprogramowania.
„Pachnie kodem”
Kiedy kod wymaga refaktoryzacji, mówi się, że ma „zapach”. Oczywiście nie dosłownie, ale taki kod naprawdę nie wygląda zbyt zachęcająco. Poniżej omówimy podstawowe techniki refaktoryzacji na etapie początkowym.Nieracjonalnie duże klasy i metody
Klasy i metody mogą być uciążliwe, niemożliwe do efektywnej pracy właśnie ze względu na ich ogromny rozmiar.Duża klasa
Taka klasa ma ogromną liczbę linii kodu i wiele różnych metod. Deweloperowi zwykle łatwiej jest dodać funkcję do istniejącej klasy niż stworzyć nową, dlatego klasa się rozrasta. Z reguły w takiej klasie mieści się zbyt wiele funkcji. W takim przypadku pomocne jest przeniesienie części funkcjonalności do osobnej klasy. Porozmawiamy o tym bardziej szczegółowo w części poświęconej technikom refaktoryzacji.Metoda długa
Ten „zapach” pojawia się, gdy programista dodaje nową funkcjonalność do metody: „Dlaczego miałbym umieszczać sprawdzanie parametrów w osobnej metodzie, skoro mogę napisać kod tutaj?”, „Dlaczego potrzebuję osobnej metody wyszukiwania, aby znaleźć maksimum element w tablicy? Zostawmy to tutaj. W ten sposób kod będzie bardziej przejrzysty” i inne tego typu nieporozumienia.Istnieją dwie zasady refaktoryzacji długiej metody:
- Jeśli masz ochotę dodać komentarz podczas pisania metody, powinieneś umieścić tę funkcjonalność w osobnej metodzie.
- Jeśli metoda zajmuje więcej niż 10-15 wierszy kodu, należy zidentyfikować zadania i podzadania, które wykonuje, i spróbować umieścić podzadania w osobnej metodzie.
Istnieje kilka sposobów na wyeliminowanie długiej metody:
- Przenieś część funkcjonalności metody do osobnej metody
- Jeśli zmienne lokalne uniemożliwiają przeniesienie części funkcjonalności, możesz przenieść cały obiekt do innej metody.
Używanie wielu prymitywnych typów danych
Ten problem zwykle występuje, gdy liczba pól w klasie rośnie w czasie. Na przykład, jeśli przechowujesz wszystko (waluta, data, numery telefonów itp.) w prymitywnych typach lub stałych zamiast małych obiektów. W takim przypadku dobrą praktyką byłoby przeniesienie logicznego zgrupowania pól do osobnej klasy (wyodrębnij klasę). Możesz także dodać metody do klasy, aby przetwarzać dane.Zbyt wiele parametrów
Jest to dość częsty błąd, szczególnie w połączeniu z długą metodą. Zwykle występuje, gdy metoda ma zbyt dużą funkcjonalność lub jeśli metoda implementuje wiele algorytmów. Długie listy parametrów są bardzo trudne do zrozumienia, a używanie metod z takimi listami jest niewygodne. W rezultacie lepiej przekazać cały obiekt. Jeśli obiekt nie ma wystarczającej ilości danych, należy użyć bardziej ogólnego obiektu lub podzielić funkcjonalność metody tak, aby każda metoda przetwarzała logicznie powiązane dane.Grupy danych
W kodzie często pojawiają się grupy logicznie powiązanych danych. Na przykład parametry połączenia z bazą danych (adres URL, nazwa użytkownika, hasło, nazwa schematu itp.). Jeśli z listy pól nie da się usunąć ani jednego pola, wówczas należy przenieść te pola do osobnej klasy (klasy ekstraktu).Rozwiązania naruszające zasady OOP
Te „zapachy” pojawiają się, gdy programista narusza właściwy projekt OOP. Dzieje się tak, gdy nie rozumie on w pełni możliwości OOP i nie potrafi ich w pełni lub właściwie wykorzystać.Nieużycie dziedziczenia
Jeśli podklasa używa tylko małego podzbioru funkcji klasy nadrzędnej, to pachnie niewłaściwą hierarchią. Kiedy tak się dzieje, zwykle zbędne metody po prostu nie są zastępowane lub zgłaszają wyjątki. Jedna klasa dziedziczy inną, co oznacza, że klasa potomna wykorzystuje prawie całą funkcjonalność klasy nadrzędnej. Przykład prawidłowej hierarchii:

Instrukcja przełączania
Co może być nie tak zswitch
oświadczeniem? Źle jest, gdy staje się to bardzo skomplikowane. Podobnym problemem jest duża liczba zagnieżdżonych if
instrukcji.
Alternatywne klasy z różnymi interfejsami
Wiele klas robi to samo, ale ich metody mają różne nazwy.Pole tymczasowe
Jeśli klasa ma tymczasowe pole, którego obiekt potrzebuje tylko od czasu do czasu, gdy jego wartość jest ustawiona, i jest ono puste lub, nie daj Boże,null
przez resztę czasu, to kod śmierdzi. To wątpliwa decyzja projektowa.
Zapachy utrudniające modyfikację
Te zapachy są poważniejsze. Inne zapachy głównie utrudniają zrozumienie kodu, ale uniemożliwiają jego modyfikację. Kiedy próbujesz wprowadzić jakieś nowe funkcje, połowa programistów rezygnuje, a połowa wariuje.Hierarchie dziedziczenia równoległego
Ten problem objawia się, gdy podklasa klasy wymaga utworzenia innej podklasy dla innej klasy.Równomiernie rozłożone zależności
Wszelkie modyfikacje wymagają wyszukania wszystkich zastosowań (zależności) klasy i wprowadzenia wielu drobnych zmian. Jedna zmiana — edycje w wielu klasach.Złożone drzewo modyfikacji
Ten zapach jest przeciwieństwem poprzedniego: zmiany dotyczą dużej liczby metod w jednej klasie. Z reguły taki kod ma zależność kaskadową: zmiana jednej metody wymaga poprawienia czegoś w innej, potem w trzeciej i tak dalej. Jedna klasa — wiele zmian.„Zapach śmieci”
Dość nieprzyjemna kategoria zapachów, która powoduje bóle głowy. Bezużyteczny, niepotrzebny, stary kod. Na szczęście współczesne IDE i lintery nauczyły się ostrzegać przed takimi zapachami.Duża liczba komentarzy w metodzie
Metoda ma wiele komentarzy wyjaśniających w prawie każdym wierszu. Zwykle wynika to ze złożonego algorytmu, dlatego lepiej podzielić kod na kilka mniejszych metod i nadać im objaśniające nazwy.Zduplikowany kod
Różne klasy lub metody używają tych samych bloków kodu.Leniwa klasa
Klasa ma bardzo małą funkcjonalność, chociaż planowano, że będzie duża.Niewykorzystany kod
Klasa, metoda lub zmienna nie jest używana w kodzie i jest martwa.Nadmierna łączność
Ta kategoria zapachów charakteryzuje się dużą liczbą nieuzasadnionych zależności w kodzie.Metody zewnętrzne
Metoda wykorzystuje dane z innego obiektu znacznie częściej niż własne dane.Niewłaściwa intymność
Klasa zależy od szczegółów implementacji innej klasy.Długie rozmowy klasowe
Jedna klasa wywołuje inną, która żąda danych od trzeciej, która otrzymuje dane od czwartej i tak dalej. Tak długi łańcuch wezwań oznacza dużą zależność od obecnej struktury klasowej.Klasa dealera zadań
Klasa jest potrzebna tylko do wysłania zadania do innej klasy. Może należałoby go usunąć?Techniki refaktoryzacji
Poniżej omówimy podstawowe techniki refaktoryzacji, które mogą pomóc wyeliminować opisane zapachy kodu.Wyodrębnij klasę
Klasa wykonuje zbyt wiele funkcji. Część z nich trzeba przenieść do innej klasy. Załóżmy na przykład, że mamyHuman
klasę, która również przechowuje adres domowy i ma metodę, która zwraca pełny adres:
class Human {
private String name;
private String age;
private String country;
private String city;
private String street;
private String house;
private String quarter;
public String getFullAddress() {
StringBuilder result = new StringBuilder();
return result
.append(country)
.append(", ")
.append(city)
.append(", ")
.append(street)
.append(", ")
.append(house)
.append(" ")
.append(quarter).toString();
}
}
Dobrą praktyką jest umieszczenie informacji adresowych i powiązanej metody (zachowania przetwarzania danych) w osobnej klasie:
class Human {
private String name;
private String age;
private Address address;
private String getFullAddress() {
return address.getFullAddress();
}
}
class Address {
private String country;
private String city;
private String street;
private String house;
private String quarter;
public String getFullAddress() {
StringBuilder result = new StringBuilder();
return result
.append(country)
.append(", ")
.append(city)
.append(", ")
.append(street)
.append(", ")
.append(house)
.append(" ")
.append(quarter).toString();
}
}
Wyodrębnij metodę
Jeśli metoda ma pewną funkcjonalność, którą można wyizolować, należy umieścić ją w osobnej metodzie. Na przykład metoda, która oblicza pierwiastki równania kwadratowego:
public void calcQuadraticEq(double a, double b, double c) {
double D = b * b - 4 * a * c;
if (D > 0) {
double x1, x2;
x1 = (-b - Math.sqrt(D)) / (2 * a);
x2 = (-b + Math.sqrt(D)) / (2 * a);
System.out.println("x1 = " + x1 + ", x2 = " + x2);
}
else if (D == 0) {
double x;
x = -b / (2 * a);
System.out.println("x = " + x);
}
else {
System.out.println("Equation has no roots");
}
}
Każdą z trzech możliwych opcji obliczamy osobnymi metodami:
public void calcQuadraticEq(double a, double b, double c) {
double D = b * b - 4 * a * c;
if (D > 0) {
dGreaterThanZero(a, b, D);
}
else if (D == 0) {
dEqualsZero(a, b);
}
else {
dLessThanZero();
}
}
public void dGreaterThanZero(double a, double b, double D) {
double x1, x2;
x1 = (-b - Math.sqrt(D)) / (2 * a);
x2 = (-b + Math.sqrt(D)) / (2 * a);
System.out.println("x1 = " + x1 + ", x2 = " + x2);
}
public void dEqualsZero(double a, double b) {
double x;
x = -b / (2 * a);
System.out.println("x = " + x);
}
public void dLessThanZero() {
System.out.println("Equation has no roots");
}
Kod każdej metody stał się znacznie krótszy i łatwiejszy do zrozumienia.
Mijanie całego obiektu
Kiedy metoda jest wywoływana z parametrami, możesz czasami zobaczyć taki kod:
public void employeeMethod(Employee employee) {
// Some actions
double yearlySalary = employee.getYearlySalary();
double awards = employee.getAwards();
double monthlySalary = getMonthlySalary(yearlySalary, awards);
// Continue processing
}
public double getMonthlySalary(double yearlySalary, double awards) {
return (yearlySalary + awards)/12;
}
Posiada employeeMethod
2 całe linie poświęcone pobieraniu wartości i przechowywaniu ich w zmiennych pierwotnych. Czasami takie konstrukcje mogą zająć do 10 linii. Znacznie łatwiej jest przekazać sam obiekt i użyć go do wydobycia niezbędnych danych:
public void employeeMethod(Employee employee) {
// Some actions
double monthlySalary = getMonthlySalary(employee);
// Continue processing
}
public double getMonthlySalary(Employee employee) {
return (employee.getYearlySalary() + employee.getAwards())/12;
}
Prosto, krótko i zwięźle.
Logiczne pogrupowanie pól i przeniesienie ich do osobnegoclassDespite
faktu, że powyższe przykłady są bardzo proste i patrząc na nie, wielu z was może zapytać: „Kto to robi?”, wielu programistów popełnia takie błędy strukturalne z powodu nieuwagi, niechęć do refaktoryzacji kodu lub po prostu postawa „to wystarczy”.
Dlaczego refaktoryzacja jest skuteczna
Dzięki dobremu refaktoryzacji program ma czytelny kod, perspektywa zmiany jego logiki nie jest straszna, a wprowadzanie nowych funkcji nie staje się piekłem analizy kodu, ale przyjemnym doświadczeniem na kilka dni . Nie powinieneś refaktoryzować, jeśli łatwiej byłoby napisać program od zera. Załóżmy na przykład, że Twój zespół szacuje, że praca potrzebna do zrozumienia, przeanalizowania i refaktoryzacji kodu będzie większa niż wdrożenie tej samej funkcjonalności od podstaw. Lub jeśli kod do refaktoryzacji ma wiele problemów, które są trudne do debugowania. Wiedza o tym, jak poprawić strukturę kodu, jest niezbędna w pracy programisty. A nauka programowania w Javie jest najlepsza na CodeGym, kursie online, który kładzie nacisk na praktykę. Ponad 1200 zadań z natychmiastową weryfikacją, około 20 mini-projektów, zadania w grze — to wszystko sprawi, że poczujesz się pewnie w programowaniu. Najlepszy czas by zacząć jest teraz :)Zasoby umożliwiające dalsze zagłębienie się w refaktoryzację
Najbardziej znaną książką na temat refaktoryzacji jest „Refactoring. Improving the Design of Existing Code” autorstwa Martina Fowlera. Jest też ciekawa publikacja na temat refaktoryzacji, oparta na poprzedniej książce: „Refactoring using Patterns” autorstwa Joshua Kerievsky. Mówiąc o wzorcach... Podczas refaktoryzacji zawsze bardzo przydatna jest znajomość podstawowych wzorców projektowych. Pomogą w tym te doskonałe książki: Mówiąc o wzorcach... Podczas refaktoryzacji zawsze bardzo przydatna jest znajomość podstawowych wzorców projektowych. Pomogą w tym te doskonałe książki:- „Wzorce projektowe” autorstwa Erica Freemana, Elizabeth Robson, Kathy Sierra i Berta Batesa z serii Head First
- „Sztuka czytelnego kodu” Dustina Boswella i Trevora Fouchera
- „Code Complete” Steve'a McConnella, który określa zasady pięknego i eleganckiego kodu.
GO TO FULL VERSION