Friday, December 14, 2012

Workarounds

Kiedy ostatni raz widziałeś kod w stylu:
if ( param == 4 ) { //workaround, do not touch!

  position.setX( position.getX() + 1 );

}
Workarounds mnożą się niczym wirusy na pożywce i po pewnym czasie potrafią zapaskudzić niezły kawałek kodu. Jest to zmutowana odmiana starego dobrego goto, czyli reagowanie na przypadki szczególne, zamiast spójnej struktury algorytmu.

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
W konsekwencji zaczynają pisać workarounds podobne do tych przedstawionych wyżej. W miarę upływu czasu bajpasów jest coraz więcej, a zatem jeśli nawet klasę poprawisz i zacznie działać prawidłowo, to problem pojawia się w kodzie klienckim, gdyż jest on przygotowany na wadliwe działanie klasy. Dodajmy do tego, że o niektórych błędach w kodzie klienckim dowiesz się dopiero po paru, parunastu dniach, gdy już trochę podziała. Zatem skoro nie wiadomo, co z tym zrobić, to najczęściej nie robimy nic. A workarounds przybywa. Taki mamy punkt wyjścia.

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();
    }
}
Jeśli coś nazwiesz, to można o tym mówić (parafrazując Erica Evansa). Zamiast myśleć w kategoriach workarounds, programiści zaczną myśleć w kategoriach patches.

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:
  1. Nazwij błąd wprowadzając dekoratora z poprawkami
  2. 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
  3. Stwórz przypadki testowe, dla wszystkich miejsc w kodzie klienckim, w których występują obejścia
  4. Zastąp workarounds w kodzie klienckim użyciem klasy z poprawką.
  5. Uruchom testy kodu klienckiego
  6. Napisz przypadki testowe do wadliwie działającego fragmentu kodu usługi
  7. Przenieś patch z dekoratora do klasy usługowej
  8. Uruchom testy klasy usługowej
  9. Uruchom testy kodu klienckiego
  10. Zdejmij dekoratory z klasy usługowej
  11. 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.