Przejdź do głównej zawartości

Rozdział 16: Przebudowa klasy SerialDate

Rozdział 16: Przebudowa klasy SerialDate – Praktyczne Streszczenie

W rozdziale 16 Robert C. Martin (Uncle Bob) przeprowadza „sekcję zwłok” na działającej, profesjonalnej bibliotece JCommon, a konkretnie na klasie SerialDate autorstwa Davida Gilberta. To lekcja pokory i rzemiosła, która pokazuje, że nawet dobry, przetestowany kod można (i należy) ulepszyć, aby stał się bardziej czytelny i łatwiejszy w utrzymaniu.

1. O co chodzi w tej sekcji

Główną tezą autora jest to, że żaden kod nie jest nietykalny, a profesjonalizm programisty objawia się w gotowości do poddawania własnej i cudzej pracy konstruktywnej krytyce. Martin pokazuje proces iteracyjnej poprawy struktury kodu (refaktoryzacji), wychodząc od zwiększenia pokrycia testami, a kończąc na całkowitej zmianie architektury klasy.

2. Kluczowe zasady i reguły

Uncle Bob podczas przebudowy klasy stosuje szereg heurystyk „czystego kodu”:

  • Testy przede wszystkim: Zanim dotkniesz kodu, musisz mieć testy o wysokim pokryciu. Martin odkrył, że oryginalne testy pokrywały tylko 50% instrukcji, co pozwoliło na ukrycie realnych błędów w logice.
  • Wyeliminuj „zazdrość o funkcje” (Feature Envy): Metody powinny operować na zmiennych klasy, w której się znajdują. Jeśli metoda klasy A używa głównie getterów klasy B, powinna prawdopodobnie zostać przeniesiona do klasy B.
  • Stosuj Typy Wyliczeniowe (Enums): Zamiast statycznych stałych typu int (np. public static final int JANUARY = 1), używaj enum. Zapewnia to bezpieczeństwo typów i eliminuje potrzebę walidacji „magicznych liczb”.
  • Nazewnictwo na właściwym poziomie abstrakcji: Nazwa SerialDate była zła, ponieważ sugerowała implementację (numer seryjny). Martin zmienił ją na DayDate, co lepiej oddaje istotę klasy (data operująca na dniach, a nie na czasie).
  • Ukrywaj szczegóły implementacji: Stałe specyficzne dla konkretnego sposobu przechowywania danych (np. EARLIEST_DATE_ORDINAL) powinny znajdować się w klasach pochodnych, a nie w klasie bazowej.
  • Unikaj zależności bazy od pochodnych: Klasa bazowa (DayDate) nie powinna wiedzieć o swoich potomkach (SpreadsheetDate). Martin rozwiązał to, wprowadzając wzorzec Fabryki Abstrakcyjnej (DayDateFactory).

3. Przykłady kodu

Naprawa błędnego algorytmu

Oryginalna metoda getNearestDayOfWeek zawierała błąd logiczny, który Martin wykrył dzięki testom jednostkowym.

Przed (Błędna logika):

// Wiersz 717 w SerialDate
int adjust = -Math.abs(targetDOW - baseDOW);
if (adjust >= 4) {
adjust = 7 - adjust;
}
if (adjust <= -4) {
adjust = 7 + adjust;
}
return SerialDate.addDays(adjust, base);

Po (Poprawna logika):

// Uproszczony i poprawny algorytm
int delta = targetDOW - base.getDayOfWeek();
int positiveDelta = delta + 7;
int adjust = positiveDelta % 7;

if (adjust > 3)
adjust -= 7;
return SerialDate.addDays(adjust, base);

Zamiana statycznych stałych na Enum

Wprowadzenie typu wyliczeniowego dla miesięcy pozwoliło na usunięcie zbędnych metod walidujących.

Przed (Stałe i walidacja):

public static final int JANUARY = 1;
// ...
public static boolean isValidMonthCode(final int code) {
switch(code) {
case JANUARY: // ...
return true;
default: return false;
}
}

Po (Enum wewnątrz DayDate):

public static enum Month {
JANUARY(1), FEBRUARY(2), MARCH(3), APRIL(4), MAY(5), JUNE(6),
JULY(7), AUGUST(8), SEPTEMBER(9), OCTOBER(10), NOVEMBER(11), DECEMBER(12);

Month(int index) { this.index = index; }
public final int index;

public static Month make(int monthIndex) {
for (Month m : Month.values()) {
if (m.index == monthIndex) return m;
}
throw new IllegalArgumentException("Niewłaściwy indeks miesiąca " + monthIndex);
}
}

Zmiana metod statycznych na instancyjne

Metody modyfikujące datę sugerowały zmianę obiektu w miejscu, co było mylące dla klasy niezmiennej (immutable).

Przed (Statyczna i niejasna):

// Użycie: SerialDate nowaData = SerialDate.addDays(7, staraData);
public static SerialDate addDays(final int days, final SerialDate base) {
final int serialDayNumber = base.toSerial() + days;
return SerialDate.createInstance(serialDayNumber);
}

Po (Instancyjna i opisowa):

// Użycie: DayDate nowaData = staraData.plusDays(7);
public DayDate plusDays(int days) {
return DayDateFactory.makeDate(getOrdinalDay() + days);
}

4. Praktyczne wnioski dla programisty

  1. Stosuj zasadę skauta: Zawsze pozostawiaj kod nieco czyściejszym, niż go zastałeś. Nawet zmiana jednej nazwy zmiennej przy okazji naprawy błędu ma znaczenie.
  2. Ufaj testom, nie intuicji: Jeśli Clover (lub inne narzędzie pokrycia) pokazuje, że jakiś fragment kodu nie jest testowany, to znaczy, że prawdopodobnie zawiera błąd, o którym nie wiesz.
  3. Wypychaj logikę do Enumów: Jeśli masz dużo instrukcji switch operujących na stałych liczbowych, przenieś tę logikę do metod wewnątrz enum.
  4. Używaj zmiennych tymczasowych: Zamiast budować skomplikowane jednolinijkowe wyrażenia, rozbij je na kroki pośrednie z opisowymi nazwami zmiennych (np. thisMonthAsOrdinal, resultYear).
  5. Usuwaj martwy kod: Jeśli metoda nie jest używana (co wykaże analiza statyczna lub testy), usuń ją bez litości. System kontroli wersji (Git) i tak ją zapamięta.
  6. Unikaj prefiksów: W nowoczesnych IDE prefiksy typu f (dla pól) czy m_ są zbędnym szumem. Usuń je, by kod czytało się jak prozę.

Refaktoryzacja to proces iteracyjny. Martin przyznaje, że podczas pracy nad SerialDate wielokrotnie cofał własne decyzje, by w kolejnym kroku znaleźć jeszcze lepsze rozwiązanie. To dążenie do doskonałości jest fundamentem profesjonalizmu w programowaniu.