Kiedy ostatni raz widziałeś kod w stylu:
if ( param == 4 ) { //workaround, do not touch!
position.setX( position.getX() + 1 );
}
Skupię się na refaktoryzacji pewnego rodzaju workaround wynikającego z wadliwie działającej klasy.
Wyobraź sobie taką sytuację, że w Twoim projekcie dość intensywnie używana używana jest pewna klasa, np.: związana z jakimś rodzajem obliczeń (finansowych, współrzędnych geograficznych, matematycznych, daty lub czasu itp). Projekt się rozrastał, klasa była używana w kolejnych modułach, kolejni ludzie odwoływali się do niej w swoim kodzie. W konsekwencji mamy taką sytuację, że duża ilość kodu zależy od działania tejże klasy. Tak, można się rozwodzić nad tym, czy ta sytuacja jest ok, czy nie ok. To jest teraz sednem sprawy. Sęk w tym, że z takimi sytuacjami mamy do czynienia.
Nagle okazuje się, że w pewnym przypadku, po wywołaniu pewnej metody z pewnymi parametrami, otrzymujesz niepoprawny wynik. Pomyślisz, że naturalnym krokiem będzie naprawienie błędu. Nic bardziej mylnego! Być może to logiczne, ale ludzie nie działają logicznie (serio!). Jeśli programiści pracują z kodem rozwijanym od lat, a niektórzy z nich "weszli" w temat niedawno, to najważniejszą dla nich rzeczą nie jest ulepszać, lecz nie zepsuć. Ludzie nie biorą się za refaktoryzowanie wadliwie działającego kodu z następujących powodów (w kolejności od najbardziej ważkich):
- Nie są pewni, że nie popsują czegoś w innym miejscu
- Brakuje testów (niekoniecznie automatycznych, lecz w ogóle sposobu na przetestowanie czy wciąż jest ok)
- Obawiają się wziąć odpowiedzialność za skutki swoich działań
- Wiedzą z grubsza co nie działa i co należy zrobić, lecz nie wiedzą jakie kroki po kolei należy wykonać, aby na końcu wszystko wciąż trzymało się kupy
Kontrola - główny problem refaktoryzacji
Dochodzimy do kluczowego problemu refaktoryzacji. Zespół z reguły wie, co i gdzie jest nie tak z kodem (albo przynajmniej ma feeling). Zazwyczaj wie, jaka powinna być postać docelowa i co konkretnie należy zrobić. Działania nie są podejmowane ponieważ nie wiadomo, jak przeprowadzić przekształcenie w kontrolowany sposób.Zatrzymaj degradację kodu
Aby nie zapędzić się w pogoń za własnym ogonem, warto uświadomić sobie pewne priorytety. Przede wszystkim najważniejsze jest, aby zatrzymać postępującą degradację kodu. Samo refaktoryzowanie ma drugi priorytet. Jeśli zaczynam refaktoryzować, a w innych miejscach przybywa smellsów, to, jak mówią, lipa.W jaki sposób zatrzymać degradację kodu? Przede wszystkim nazwij błąd. Programiści najczęściej sami odkrywają wadliwe działanie klasy i sami piszą workarounds. W świadomości zespołu istnieje schemat, że przy użyciu tej a tej metody należy dodać 2 do wyniku. Ludzie uczą się, że należy pisać workarounds. Przede wszystkim trzeba zmienić ten nawyk poprzez zmianę ich myślenia o błędzie. Jeszcze raz, bo to kluczowa sprawa: błąd trzeba nazwać. Na przykład następująco (kontynuuję przykład ze wstępu):
public InvalidCartesianXPatch extends Position {
private Position patchedPosition ;
public InvalidCartesianXPatch( Position patchedPosition ) {
this.patchedPosition = patchedPosition;
}
@Override
public int getX() {
return param == 4 ? patchedPosition.getX() + 1 : patchedPosition.getX();
}
}
W każdym nowym kodzie klienckim powinien być już używany obiekt z poprawkami. Oprócz nazywania i informowania można zachęcić do tego programistów na przykład następująco;
public Position {
public static Position createPosition(int x, int y) {
return new InvalidCartesianXPatch( new Position(x, y) );
}
@Deprecated
public Position(int x, int y) {
//...
}
@Deprecated
public Position( ... ) { }
}
Stwórz testy do kodów klienckich
Gdy degradacja kodu została zatrzymana, można rozpocząć akcję refaktoryzowania już istniejących workarounds. W pierwszej kolejności stwórz przypadki testowe. Testujemy te aspekty zachowania metod w kodzie klienckim, który dotyczy istniejącego workaround. Jeśli metoda jest zbyt duża, wygodnie będzie (nieco sztucznie) ją zdekomponować tak, aby wyizolować fragment z workaround. Zanim ruszysz dalej stwórz testy do wszystkich miejsc, w których występują bajpasy. Tak tak, there is no free lunch.Zrefaktoryzuj kod kliencki
Zastąp workarounds poprzez łatę i uruchom testy.Przenieś kod z patcha do klasy docelowej
Oczywiście po napisaniu testów, o ile nie istnieją. Przenieś poprawkę z patcha do klasy, która działała wadliwie. Następnie uruchom wszystkie testy. Jeśli wciąż działa, to możesz pozbyć się wrapperów. W przypadku nowego kodu wystarczy zmodyfikować kod metody createPosition. W poprawianym kodzie trzeba je ręcznie pozdejmować. Ponownie uruchom testy i jeśli wciąż wszystko działa, proces można uznać za zakończony.Podsumowując
Gdyby wszystko w/w ująć w pojedyncze kroki, to mamy następującą procedurę podstępowania:- Nazwij błąd wprowadzając dekoratora z poprawkami
- Zadbaj, aby w nowym kodzie programiści używali klasy z poprawką np.: stwórz nazwany konstruktor w postaci prostej metody fabrykującej, a istniejące konstruktory oznacz przez @Deprecated
- Stwórz przypadki testowe, dla wszystkich miejsc w kodzie klienckim, w których występują obejścia
- Zastąp workarounds w kodzie klienckim użyciem klasy z poprawką.
- Uruchom testy kodu klienckiego
- Napisz przypadki testowe do wadliwie działającego fragmentu kodu usługi
- Przenieś patch z dekoratora do klasy usługowej
- Uruchom testy klasy usługowej
- Uruchom testy kodu klienckiego
- Zdejmij dekoratory z klasy usługowej
- Uruchom testy
Przeprowadzenie całego procesu trzeba by liczyć raczej w tygodniach niż w godzinach. Szybkie numerki akcja refaktoryzacja często sprawiają więcej kłopotów niż korzyści. Bardziej strategiczne podejście do refaktoryzacji i przeprowadzanie jej według uporządkowanego procesu jest bezpieczniejsze.