Showing posts with label refactoring. Show all posts
Showing posts with label refactoring. Show all posts

Thursday, September 15, 2016

Meet us at JDD 2016

This year you will see a special talk at JDD Conference.
Me and Łukasz Korczyński (Nordea Bank AB) prepared a summary of six months long work with pushing refactoring initiative at Nodea Bank AB (former Nordea IT Polska) in Gdynia. We are waiting for your attending!

Monday, June 24, 2013

Code Speaks 2U, czyli Test Ivony

Przyszło mi ostatnio do głowy następujące kryterium czytelności kodu: kod musi do Ciebie przemówić. I potraktowałem to bardzo dosłownie.

Zatem weź fragment kodu, który chcesz sprawdzić i:
  1. Operatory zamień na słowa
  2. Klamerkę "{" po ifie zamień na "then"
  3. Rozbij nazwy camel case na pojedyncze wyrazy
  4. Z wyrażeń przypisania bierz pod uwagę tylko prawą stronę, lewą ignoruj
  5. Każdy wiersz kodu traktuj jako osobne zdanie zakończone kropką.
  6. Tak powstały tekst wklej do syntezatora mowy ivona.com
  7. Jeśli to, co słyszysz (bez patrzenia na kod), jest w pełni zrozumiałe, to kod jest czytelny :)

Weźmy taki przykład na przykład:
public class List {

 private final static int DEFAULT_SIZE = 10;
 private Object[] elements;
 private boolean readOnly;
 private int size;

 public List() {
   elements = new Object[DEFAULT_SIZE];
   size = DEFAULT_SIZE;
 }
 
 public void add(Object element) {
   if (!readOnly) {
     int newSize = size + 1;
     if (newSize > elements.length) {
       Object[] newElements = new Object[elements.length+10];
       for (int i = 0; i < size; i++) {
         newElements[i] = elements[i];
       }
       elements = newElements;
     }
              elements[size++] = element;
   }
 }
}

Dla metody add tekst będzie następujący:
If not read only.
Size plus one.
If new size greater than elements length then.
New objects size of elements length plus one.
For each elements: i of elements.
New elements.
Element.

No, a teraz wklej do ivona.com i słuchaj :) Ma sens?

A teraz niewielki refaktoring metody add związany przede wszystkim nazywaniem i pierwszymi dwoma krokami Naturalnego Porządku Refaktoryzacji.

Metoda przybiera następującą postać:


public void add(Object anElement) {
  if (readOnly) {
    return;
  }
  
  if (atCapacity()) {
    grow();
  }
  
  put(anElement, into(elements));
}
 


private Object[] into(Object[] elements) {
  return elements;
}

private void put(Object anElement, Object elements[]) {
  elements[size++] = anElement;
}
 
private boolean atCapacity() {
  return size + 1> elements.length;
}

private void grow() {
  Object[] newElements = new Object[elements.length + 10];
  for (int i = 0; i < size; i++) {
    newElements[i] = elements[i];
  }

  elements = newElements;
}

Tym razem tekst dla Ivony jest następujący:
If read only then return.
If at capacity then grow.
Put an element into elements.

Ponownie odsłuchaj, co Twój kod ma Ci do powiedzenia. Lepiej? :)

Tuesday, February 19, 2013

Strategiczna refaktoryzacja

Dlaczego "strategiczna"? Ponieważ od kiedy po raz pierwszy zobowiązałem się do "Niech Pan coś zrobi z tym kodem", stało się całkowicie dla mnie jasne, że techniki refaktoryzacji to za mało. Refaktoryzacje refactoring.com, czy Refactoring to Patterns są genialne i precyzyjnie opisują jak transformować kod z jednej postaci w inną, lecz są to zbyt niskopoziomowe przekształcenia o zbyt małej "sile rażenia", aby mogły przynieść wymierny efekt w mocno zagmatwanym i wciąż zmieniającym się kodzie.

O czym dokładnie mówię? Mówię o projektach rozwijanych od dziesięciu lub więcej lat. Ilość wierszy kodu liczona jest wtedy w milionach. Wyobraź sobie ten projekt, a w nim plik *java (zawierajacy jedną klasę) o rozmiarze 2.8MB, co przekłada się na około 60k wierszy kodu w klasie. Dalej wyobraź sobie, że podobnych klas jest kilkaset w tym projekcie. W takim razie jaką konkretnie zmianę spowoduje chociażby Replace Conditional with Polymorphism? Albo inaczej: ile pieniędzy w kontekście najbliższego kwartału pozwoli to zaoszczędzić? Wydaje mi się, że takiego bilonu w Polsce nawet się nie bije. Wprowadzona zmiana utonie w odmętach wszechogarniającego spaghetti. A system wciąż się rozwija, kodu wciąż przybywa.

Kluczowe założenie: To ma sens

Szczegóły na ten temat umieściłem w poście Megasoftwarecraftsmanshipper. Moim zdaniem nie da się sensownie przeprowadzić znaczącej refaktoryzacji, jeśli w pierwszej kolejności nie zrozumiemy dokładnie przyczyn, dla których kod znajduje się obecnie w takiej a nie innej postaci. Bez takiej analizy najczęściej podejmowane są refaktoryzacje, które od dawana chodzą zespołowi po głowie, są ciekawe od strony technicznej, bywają czasochłonne i jednocześnie nie prowadzą do znaczącej poprawy.

Może jest to banalne, ale tego typu analizę najłatwiej przeprowadzić przy założeniu, że architektura kodu ma sens, że komuś kto ją tworzył, przyświecały racjonalne przesłanki do podjęcia decyzji, które podjął. Zamiast zadawać sobie pytania: co to $^&#@#$ jest!? , itp., stawiasz następujące: "co musiałbym myśleć, aby stworzyć taką architekturę? w jakiej sytuacji musiałbym się znajdować?". Ponoć indiańskie przysłowie mówi, że "możesz kogoś oceniać dopiero wtedy, gdy przejdziesz milę w jego mokasynach". Coś w tym jest.

Zidentyfikuj obszary refaktoryzacji

Nie wszystkie fragmenty systemu są jednakowo ważne. Refaktoryzuj te, które są najważniejsze. Jako atrybuty do klasyfikacji możesz wziąć pod uwagę:
  • najczęściej zmieniające się fragmenty
  • fragmenty, na które raportowanych jest najwięcej błędów
  • fragmenty, z którymi programiści mają najwięcej problemów
  • fragmenty najmniej/najbardziej pokryte testami

Pomocnym narzędziem do analizy w/w atrybutów są raporty z Twojego traca albo repozytorium. Kilka informacji znajdziesz w artykule Your VCS – the forgotten feedback loop

Najpierw zatrzymaj degradację kodu

Idę tę opisałem na na przykładzie radzenia sobie z workarounds w kodzie.
Innym, bardziej powszechnego użytku przykładem, może być skorzystanie z zasady Interface Paritioning.



Gdy mamy wielgachne (60k w pliku txt; rozmiar metod od 20 wierszy od 1k) klasy jak na rysunku, od których zależy wiele innych, to pierwszym problemem nie jest ich rozmiar, lecz to, że sytuacja wciąż się pogarsza ze względu na trwające prace, a więc stale przyrasta ilość klas zależnych od naszego potwora. Jeśli zabierzesz się za rozkładanie tej klasy na mniejsze kawałki, to może to zająć tyle czasu, że w końcu spotkasz się z Jeźdźcami Apokalipsy. Jeśli jednak na najpierw posegregujemy metody z potwora pomiędzy interfejsy, to spowoduje to zatrzymanie postępującego pogarszania się kodu.



Skupiamy się, na zatrzymaniu powstawania nowych zależności. Empirycznie sprawdziłem, że poszatkowanie na interfejsy klasy zawierającej ok 1k metod, dwóm osobom, które w miarę często z niej korzystają zajmuje jakieś 4-6 godzin. Po takim zabiegu można sobie już zaplanować spokojne wydzielanie mniejszych komponentów z potwora.

Drugim przykładem na powstrzymanie postępującego bałaganu w kodzie jest Replace Method with Method Object. Ten refaktoring kojarzy mi się z pączkowaniem drożdży.



Chodzi tu przede wszystkim o to, aby "wypchnąć" dużą ilość kodu poza wielgachną klasę i tam sobie już spokojnie rozkminiać dany fragment i go porządkować.

Te dwa sposoby działania bardzo dobrze ze sobą współpracują:
  1. Interface Partitioning - tworzy w okół potwora skorupę, dzięki której nie może on już wpływać na inne klasy
  2. Replace Method with Method Object - pomaga rozczłonkować potwora na kwałki



Zarządzaj refaktoryzacją również na poziomie mikro

Oczywiście umiejętności programistów są jednym z kluczowych blokad przeciw pogarszaniu się jakości kodu.



Mam tu na myśli powtarzalny proces pracy z kodem, który nazywam Naturalnym Porządkiem Refaktoryzacji. Szczegółowo możesz się zapoznać z tą koncepcją tutaj:

Hipoteza o pewnym Punkcie:)

Przez analogię do Promienia Schwarzschilda mam taką hipotezę, że dla każdego systemu istnieje taki poziom bałaganu w kodzie, po przekroczeniu którego, zespół nie jest już w stanie kompetencyjnie podołać refaktoryzowaniu systemu i każde podjęte działanie pogarsza tylko jakość kodu.

Żeby znaleźć rozwiązanie w tej sytuacji, należy zacząć inaczej myśleć o refaktoryzowaniu kodu oraz o jego jakości. Z mojego punktu widzenia architektura jest emanacją relacji pomiędzy biznesem a IT. Jeśli relacja ta jest nieekologiczna, to żadna magiczna sztuczka nie poprawi designu. Dalej, refaktoryzacja jest dla mnie problem organizacyjnym, a nie technicznym, czy kompetencyjnym.

Co z wynika z powyższych dywagacji. Bardzo prosta rzecz. Możliwe staje się zastosowanie wytycznej: nie szukaj rozwiązania problemu na poziomie, na którym się on rozgrywa, przejdź poziom wyżej. Zatem po przekroczeniu wymienionego w H3 Punktu, jedyne co może poprawić jakość kodu to refaktoryzacja na poziomie organizacyjnym. Właśnie tak, zmień sposób zorganizowania prac nad projektem, a przełoży się to na poprawę architektury i jakości kodu. Możesz zacząć od przyjrzenia się następującym relacjom:
  • współpraca między zespołami deweloperskimi
  • współpraca między zespołami deweloperskimi a zespołami testującymi
  • współpraca między zespołami deweloperskimi a biznesem

Podam przykład, w pewnej firmie postanowiono wprowadzić Zespół ds. Trudnych i Beznadziejnych. Zajmowali się oni naprawianiem problemów, których nikt inny nie potrafił naprawić. Przez pewien czas rozwiązanie sprawdzało się świetnie. Przez pewien czas, bo po ośmiu miesiącach zaczęły pojawiać się następujące objawy:
  • najlepsi programiści znaleźli się w Zespole
  • najlepsi programiści nie mieli czasu na uczenie innych jak rozwiązywać trudne problemy
  • zwykli programiści przykładali mniejszą wagę do jakości kodu, bo wiedzieli, że Najlepsi wychwycą każde niedociągnięcie
  • Zespół Najlepszych miał coraz więcej pracy
  • zwykli programiści coraz mniej dbali o jakość kodu
  • jakość kodu drastycznie spadła
  • Najlepsi programiści nie mieli czasu na refaktoryzację, zwykli programiści nie potrafili jej przeprowadzić
Paradoksalnie można stwierdzić, że kod był pisany po to, aby Zespół ds. Trudnych i Beznadziejnych miał co robić:)


Najważniejsze założenia przestawione w tym artykule

  • Pierwszym celem strategicznej refaktoryzacji jest powstrzymanie postępującej degradacji w kodzie; drugim celem jest poprawa jego jakości
  • Aby zaproponować sensowny plan refaktoryzacji trzeba w pierwszej kolejności zrozumieć przyczyny powstania legacy code
  • Umiejętności programisty to kluczowa rzecz, która poprawia jakość kodu
  • Refaktoryzację można prowadzić na poziomie technicznym tylko do pewnego momentu, potem należy przenieść się na poziom organizacyjny

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.

Friday, September 30, 2011

Dług techniczny, opcja call czy lokata długoterminowa?

Standardowy kłopot z refaktoryzacją: my chcemy, a z góry leci głośnie NIE! Łaj? W moim odczuciu są dwie główne przyczyny. Pierwsza wynika z definicji. Skoro refaktoryzacja, to "ulepszenie wewnętrznej struktury oprogramowania, bez zmiany jego funkcjonalności", to skoro nie dodaje funkcjonalności, a zatem nie dodaje wartości biznesowej, a zatem nie pozwala efektywniej zarabiać, a zatem nie warto jej wykonywać. Podsumowując - kasa.
Drugi problem jest związany z przekonaniem, że jak już refaktoryzować, to rewolucyjnie i musi to zająć mnóstwo czasu. Skoro czasu to i pieniędzy. Za drogo i nie warto tego robić. Znów kasa. Nie zbłądzę zbytnio, gdy stwierdzę, że tarcia o refaktoryzację obijają się o pieniądze.

Kłopot z metaforami

Aby zdobyć choć trochę posłuchu u biznesu community wykombinowało jak na razie dwie metafory: Długu technicznego oraz (najświeższa) Niezabezpiecznonej opcji typu call.
Gdy się w nie wczytać, to każda z nich mówi mniej więcej coś takiego: Pamiętaj biznes-ludku, że jak nie będziemy refaktoryzować, to stanie się COŚ strasznego!. Obie metafory odwołują się do rzeczy, która jak nam się zdaje, najlepiej do biznesu przemówi, czyli do pieniędzy.

Dług techniczny mówi: zabraniając refaktoryzacji zaciągasz duże zobowiązanie, w kolejnym wydaniu rollujesz ten dług kolejnym długiem, i kolejnym, i kolejnym. W końcu zbankrutujesz i klops.
Niezabezpieczona opcja call mówi: nie pozwoliłeś na refaktoryzację, nabyłeś najbardziej ryzykowny instrument finansowy we Wszechświecie. Jedyne co możesz robić, to się modlić.

Skoro te metafory tak dokładnie wyłuszczają konsekwencje zaniedbywania jakości kodu, to dlaczego nic się nie zmienia? Nieco światła rzuca na to psychologia społeczna, wg której bardziej preferujemy natychmiastową gratyfikację niż długoterminowe korzyści oraz bardziej obawiamy się nieprzyjemności bliższych i namacalnych niż odległych i nieco abstrakcyjnych.

Podsumowując, twierdzę, że obojętność na prorefaktoryzacyjne błagania ma następujące przyczyny
  • korzyść z refaktoryzacji jest zbyt odroczona w czasie
  • korzyść z refaktoryzacji bardzo trudno wykazać jest w pieniądzach
  • zaniedbanie refaktoryzacji ma negatywne skutki w bliżej nieokreślonym kiedyś; a dopóki programiści mogą ratować projekt, to go ratują; czasem bardzo długo
  • robienie tak, "aby działało" ma szybkie odczuwalne korzyści
Powyższe fakty sprawiają, że na refaktoryzację przychodzi zgoda, gdy już w zasadzie jest za późno. I wtedy to już na prawdę jest od cholery roboty.

Lokata długoterminowa

Dobitna metafora dla refaktoryzacji powinna charakteryzować się następującymi cechami:
  • korzyści można łatwo wyrazić w pieniądzach
  • korzyści finansowe są szybko odczuwalne, powiedzmy w perspektywie tego samego albo najbliższego wydania
Zauważmy, że jest to metafora pozytywna. Dług techniczny oraz opcja call były metaforami negatywnymi w takim sensie, że straszyły tym, co złego się wydarzy jeśli nie będziesz grzeczny. Nasza nowa metafora jest sformułowana pozytywnie, bo pokazuje korzyści jakie uzyskasz, gdy będziesz się dobrze sprawować.

Szukałem tego typu metafory również w świecie finansów (no, bo dlaczego nie?) i znalazłem ja w postaci lokaty długoterminowej. Korzystając z tej metafory można w stronę Biznesu perorować, że:
  • refaktoryzacja jest jak wpłata pieniędzy na lokatę, już po pierwszym okresie rozliczeniowym następuje kapitalizacja odsetek i otrzymujesz swój procent
  • im dłużej oszczędzasz tym więcej zyskujesz
  • żeby osiągnąć maksymalną korzyść musisz wpłacać regularnie
  • jeśli zerwiesz lokatę, właściwie tracisz większość zarobku
  • no i w przypadku refaktoryzacji nie ma podatku Belki:), same korzyści!

A jak pokazać korzyści finansowe?

Tu popuszczam trochę wodze fantazji. Można spokojnie założyć, że na refaktoryzacyjnej lokacie odsetki procentują razem z początkowym kapitałem. Weźmy sobie zatem wzorek:
gdzie: V - obecna wartość, V0 - początkowy kapitał, r - stopa procentowa, n - liczba okresów rozliczeniowych.

No, to teraz spróbujmy zinterpretować wzór w kontekście refaktoryzacji. Vx - liczmy dalej w pieniądzach, n - liczba iteracji, a r? Skoro finansowo odpowiada to zyskowności (potencjałowi wzrostu kapitału?), to co jest tego odpowiednikiem w projekcie? Mam feeling, że coś z jakością kodu, ale nie bardzo wiem co.

Tuesday, August 9, 2011

Interfejs budowniczego

Na przykład tutaj można dowiedzieć się o niezwykłych korzyściach stosowania wzorca Builder (na przykład do wypiekania pizzy:)). Używam zatem, ale ten budowniczy mi więcej przeszkadza niż pomaga. Można pokusić się o stwierdzenie, że to tylko dodatkowa i nad wyraz rozdmuchana warstwa pośrednia, która niczego użytecznego nie wnosi. Ot, tworzy kilka obiektów i już.

Jak wspomniałem plusy dodatnie i ujemne budowniczego były już nie raz szeroko omawiane. Mnie interesuje odpowiedź na inne pytanie: W jaki sposób zaprojektować interfejs budowniczego?. To znaczy:
  • Jak zdecydować jakie metody powinien mieć budowniczy?
  • Jak go sensownie używać (pragmatycznie, nie sztuka dla sztuki), aby pomagał, a nie przeszkadzał?


Ponieważ debugowałem blog Sławka, żeby podłączyć syntaxhighlightera, to pożyczyłem sobie Order i OrderItem do przykładu:).

Koncept jest następujący: mamy aplikację do składania zamówień na produkty. Zamówienie składa się z grup produktowych, a te składają się z produktów, jak na rysunku.

Dla uproszczenia załóżmy, że jest to aplikacja konsolowa. Właściciel oczekuje, że będzie pracował z aplikacją następująco:
Podaj identyfikator zamówienia: Paczka-E112
Podaj priorytet zamówienia: 1
Podaj nazwy grup produktowych: spożywcze chemia zabawki
Podaj ilość porduktów dla grupy 'spożywcze': 2
Dodaj produkt dla grupy 'spożywcze': sałata 2,45zł 2
Dodaj produkt dla grupy 'spożywcze': morele 6,79zł 1
Podaj ilość porduktów dla grupy 'chemia': 3
...


Startuję z następującym zestawem klas:
public class Order {
private String id;
private int priority;
private Set itemGroups = new HashSet();
}
public class ItemGroup {
private String name;
private Set items = new HashSet();
}
public class Item {
private String name;
private Money price;
private int quantity;
}


Za wykonanie zadania Dodawanie zamówienia, odpowiedzialną uczyńmy klasę:
public class UserInterface { 
private Set orders = new HashSet();
public void addOrder() {
// TOIMPL
} 
public static void main(String[] args) {
new UserInterface().addOrder();
}
}

W pierwszym podejściu kod, który działa, mógłby wyglądać na przykład tak:
public void addOrder() {
Scanner scanner = new Scanner( System.in );
System.out.print( "Podaj identyfikator zamówienia: " );
String orderId = scanner.nextLine();
System.out.print( "Podaj priorytet zamówienia: " );
int orderPriority = Integer.parseInt( scanner.nextLine() );
Order order = new Order( orderId, orderPriority );
System.out.print( "Podaj nazwy grup produktowych: " );
String[] itemGroupsNames = scanner.nextLine().split( " " );
for ( int i = 0; i < itemGroupsNames.length; ++i ) {
ItemGroup itemGroup = new ItemGroup( itemGroupsNames[ i ] );
System.out.print( "Podaj ilość produktów dla grupy '" + itemGroupsNames[ i ] + "' : " );
int itemsAmount = Integer.parseInt( scanner.nextLine() );
for( int j = 0; j < itemsAmount; ++j ) {
System.out.print( "Dodaj produkt dla grupy '" + itemGroupsNames[ i ] + "' : " );
String[] itemParams = scanner.nextLine().split( " " );
String itemName = itemParams[ 0 ];
Money itemPrice = Money.parseMoney( itemParams[ 1 ] );
int itemQuantity = Integer.parseInt( itemParams[ 2 ] ); 
Item item = new Item( itemName, itemPrice, itemQuantity );  
itemGroup.addItem( item );
}
order.addItemGrup( itemGroup );
}
orders.add( order ); 
}
Po pobieżny przyjrzeniu się temu rozwiązaniu, można zauważyć, że nastąpiło tu pewne pogmatwanie. Komunikowanie się z użytkownikiem (wyświetlanie komunikatów i odbieranie wpisanych danych) zostało zmiksowane z kodem biznesowym (budowanie zamówienia). W pierwszym zachwycie nad wzorce Builder mam ogromną ochotę go użyć! UWAGA jeśli przychodzi ci go głowy napisać klasę OrderBuilder z jedną metodą build mającą kilkanaście parametrów, to nie tędy droga. Wyszedł by z tego nie budowniczy ale jakieś fabrykoniewiadomoco. Wracając do tematu: wprowadźmy zatem ad hoc budowniczego (bo ponoć to dobra praktyka). Budowniczy ten mógłby wyglądać następująco:
public class OrderBuilder {
private Order order;
public void newOrder( String orderId, int orderPriority ) {
order = new Order( orderId, orderPriority );
}
public Order getOrder() {
return order;
}
public void addItemGroup( String name ) {
order.addItemGrup( new ItemGroup( name ) );
}
public void addItem( String groupName, String name, Money price, int qty ) {
ItemGroup group = order.findItemGroup( name );
group.addItem( new Item( name, price, qty ) );
}
}

Kluczowe pytanie: Czy ten budowniczy jest pomocny?

Już zerkając na kod budowniczego, można mieć wątpliwości. Właściwie nie robi nic ponad, enkaspulację wywołań konstruktorów oraz metod dodających. Ok, można go sobie mokować i dodawać nowe implementacje, ale zobaczmy jak wygląda kod, który użytkuje tego budowniczego:
public void addOrder() {
Scanner scanner = new Scanner( System.in );
System.out.print( "Podaj identyfikator zamówienia: " );
String orderId = scanner.nextLine();
System.out.print( "Podaj priorytet zamówienia: " );
int orderPriority = Integer.parseInt( scanner.nextLine() );
OrderBuilder builder = new OrderBuilder();
builder.newOrder( orderId, orderPriority );
System.out.print( "Podaj nazwy grup produktowych: " );
String[] itemGroupsNames = scanner.nextLine().split( " " );
for ( int i = 0; i < itemGroupsNames.length; ++i ) {
builder.addItemGroup( itemGroupsNames[ i ] );
System.out.print( "Podaj ilość produktów dla grupy '" + itemGroupsNames[ i ] + "' : " );
int itemsAmount = Integer.parseInt( scanner.nextLine() );
for( int j = 0; j < itemsAmount; ++j ) {
System.out.print( "Dodaj produkt dla grupy '" + itemGroupsNames[ i ] + "' : " );
String[] itemParams = scanner.nextLine().split( " " );
String itemName = itemParams[ 0 ];
Money itemPrice = Money.parseMoney( itemParams[ 1 ] );
int itemQuantity = Integer.parseInt( itemParams[ 2 ] ); 
builder.addItem( itemGroupsNames[ i ], itemName, itemPrice, itemQuantity );
}
}
orders.add( builder.getOrder() );
}
Jest tak samo brzydki jak był! Budowniczy nie wniósł znaczącego wkładu do tego kodu. Moim zdaniem interfejs budowniczego należy projektować z perspektywy klienta. Pisząc klient mam na myśli usługę, która akurat budowniczego będzie wykorzystywać - w tym przypadku jest to klasa UserIterface. Interfejs budowniczego powinien być wygodny dla klienta.

Krok 1: Dlaczego ItemGroup i Item trzeba dodawać pojedynczo?

Wcale nie trzeba...Przecież budowniczy może mieć metody, które przyjmą input, który przyszedł od użytkownika i samodzielnie go zinterpretuje. Pytanie: Czy czasem znów nie dojdzie do pomieszania warstw? Nie nie dojdzie. Przeanalizujmy odpowiedzialności poszczególnych klas:
  • Order, ItemGroup, Item - to model dziedziny, reprezentuje rzeczywistość
  • UserInterface - komunikuje się z użytkownikiem; odbiera od niego żądania (wpisy z konsoli) i przekazuje je do odpowiednich elementów niżej; prezentuje użytkownikowi wynik działania systemu
  • OrderBuilder - służy do złożenia zamówienia z mniejszych elementów
Zatem jeśli budowniczy może spokojnie przyjąć stringi podane przez użytkownika, a potem je zinterpretować. Dopóki budowniczy nie zacznie pisać na konsolę, to odpowiedzialność klas zostanie zachowana. Zobaczmy jak to mogłoby wyglądać:
public class OrderBuilder {
private Order order;
public void newOrder( String orderId, int orderPriority ) {
order = new Order( orderId, orderPriority );
}
public void addItemGroups( String itemsGroupsInput ) {
String[] itemGroupsNames = itemsGroupsInput.split( " " ); 
for ( int i = 0; i < itemGroupsNames.length; ++i ) {   
order.addItemGrup( new ItemGroup( itemGroupsNames[ i ] ) );
}
}
public void addItem( String itemParamsInput ) {
String[] itemParams = itemParamsInput.split( " " );
String groupName = itemParams[ 0 ];
ItemGroup group = order.findItemGroup( groupName );
Money price = Money.parseMoney( itemParams[ 1 ] );
int qty = Integer.parseInt( itemParams[ 2 ] );   
group.addItem( new Item( groupName, price, qty ) );
}
public Order getOrder() {
return order;
}
}
Jak widać budowniczy zmądrzał nieco, potrafi zrobić coś więcej niż proste dodawanie. A jak wygląda korzystanie z niego?
public void addOrder() {
Scanner scanner = new Scanner( System.in ); 
System.out.print( "Podaj identyfikator zamówienia: " );
String orderId = scanner.nextLine();
System.out.print( "Podaj priorytet zamówienia: " );
int orderPriority = Integer.parseInt( scanner.nextLine() );
OrderBuilder builder = new OrderBuilder();
builder.newOrder( orderId, orderPriority );
System.out.print( "Podaj nazwy grup produktowych: " );
String itemsGroupsInput = scanner.nextLine();
builder.addItemGroups( itemsGroupsInput );
String[] itemGroupsNames = itemsGroupsInput.split( " " );
for ( int i = 0; i < itemGroupsNames.length; ++i ) {
System.out.print( "Podaj ilość produktów dla grupy '" + itemGroupsNames[ i ] + "' : " );
int itemsAmount = Integer.parseInt( scanner.nextLine() );
for( int j = 0; j < itemsAmount; ++j ) {
System.out.print( "Dodaj produkt dla grupy '" + itemGroupsNames[ i ] + "' : " );
String itemParamsInput = scanner.nextLine();
builder.addItem( itemParamsInput );
}
}
orders.add( builder.getOrder() );
}
Choć nie idealnie to jednak jest nieco lepiej - metoda mieści się na jednym ekranie:)

Krok 2: Chodzenie po strukturze zamówienia

To, co nam bruździ to fakt, trze trzeba dodać Items do każdej z ItemGroup. Więc najpierw za pomocą OrderBuilder.addItemGroups tworzone są wszystkie grupy, ale jeszcze dodatkowo UserInterface przetrzymuje sobie jeszcze tablicę nazw grup produktowych, że by po nich przeiterować i dodać pozycje zamówienia. Wydaje się to trochę nienaturalne, ponieważ ta sama informacja jest przechowywana w dwóch różnych formach (String[] i Set) w dwóch różnych miejscach (UserInterface i OrderBuilder). Skoro chcemy tylko chodzić po wewnętrznej strukturze zamówienia (przesuwać się do kolejnych grup i dodawać produkty), to dlaczego nie wyposażyć budowniczego w tę możliwość. Niech budowniczy udostępni zestaw metod do chodzenia po grupach produktowych. Zobaczmy:
public class OrderBuilder {
private Order order;
private Iterator groupIterator; 
private ItemGroup currentGroup;
public void newOrder( String orderId, int orderPriority ) {
order = new Order( orderId, orderPriority );
}
public void addItemGroups( String itemsGroupsInput ) {
String[] itemGroupsNames = itemsGroupsInput.split( " " );  
for ( int i = 0; i < itemGroupsNames.length; ++i ) {   
order.addItemGrup( new ItemGroup( itemGroupsNames[ i ] ) );
}
groupIterator = order.getItemGroups();
}
public void addItem( String itemParamsInput ) {
String[] itemParams = itemParamsInput.split( " " );
String groupName = itemParams[ 0 ];
Money price = Money.parseMoney( itemParams[ 1 ] );
int qty = Integer.parseInt( itemParams[ 2 ] );   
currentGroup.addItem( new Item( groupName, price, qty ) );
}
public Order getOrder() {
return order;
} 
public boolean hasNextItemGroup() {
if ( groupIterator == null ) {
return false;
}  
return groupIterator.hasNext();
}
public void moveNextItemGroup() {
currentGroup = groupIterator.next();
} 
public String getCurrentItemGroupName() {
return currentGroup.getName();
} 
}
Natomiast korzystanie z budowniczego wygląda następująco:
public void addOrder() {
Scanner scanner = new Scanner( System.in ); 
System.out.print( "Podaj identyfikator zamówienia: " );
String orderId = scanner.nextLine();
System.out.print( "Podaj priorytet zamówienia: " );
int orderPriority = Integer.parseInt( scanner.nextLine() );
OrderBuilder builder = new OrderBuilder();
builder.newOrder( orderId, orderPriority );
System.out.print( "Podaj nazwy grup produktowych: " );
builder.addItemGroups( scanner.nextLine() );
while ( builder.hasNextItemGroup() ) {
builder.moveNextItemGroup();
System.out.print( "Podaj ilość produktów dla grupy '" +       builder.getCurrentItemGroupName() + "' : ");
int itemsAmount = Integer.parseInt( scanner.nextLine() );
for( int j = 0; j < itemsAmount; ++j ) {
System.out.print( "Dodaj produkt dla grupy '" + builder.getCurrentItemGroupName() + "' : " );
builder.addItem( scanner.nextLine() );
}
}
orders.add( builder.getOrder() );
}
Teraz już klasa UserInterface zajmuje się wyłącznie komunikacją z użytkownikiem, natomiast budowniczy zajmuje się składaniem zamówienia do kupy. Każda z klas zachowała swoją odpowiedzialność, a z używanie budowniczego rzeczywiście przynosi korzyści klientowi większe niż tylko enkapsulacja konstruktorów i dodawania do list. Dzięki wykorzystaniu budowniczego kod kliencki staje się prostszy i bardziej przejrzysty. Dla porządku trzeba dodać, że odpowiedzialność budowniczego została zwiększona. Potrafi on teraz również chodzić po strukturze zamówienia. Jest więc jednocześnie Iteratorem. Dalej warto metody iterator wydzielić do osobnego interfejsu, ale to już nieco inna historia.

Thursday, June 16, 2011

Obiektowość dla embedded?

Często zdarza mi się wypełzać poza Javę, z której wyrosłem i ze zdziwieniem stwierdzam, że są inne obszary, w których ludzie mają podobne do naszych problemy i z utęsknieniem spoglądają w stronę gadżetów którymi dysponujemy.

Jakiś czas temu, konstruktywnie krytykując mój wpis, Sławek powiedział mniej więcej coś takiego: "Kto w obecnych czasach podaje w książkach przykłady w C++? Od razu widać, że old school"

Rzeczywiście, większość koncepcji związanych z: refaktoryzacją, TDD, clean code itd. nabrała obecnego kształtu w okół Javy i C# i ich community (tak, tak, pamiętam: Small Talk, C++, GoF - byli pierwsi :)). Jakby te języki były jedyne, dominujące, a inne to tylko marginalne ekstrawagancje. Zerknijmy na ten ranking. Jak widać Java i C# mają towarzystwo i to całkiem spore.

RTS, Entertainment
Mówię o systemach embedded, ale nie telefonach, grach i smartach, gdyż im (wg programistów embedded) bliżej raczej do PC-tów. Mówię o: systemach czasu rzeczywistego, systemach sterowania produkcją, oprogramowaniu obsługującym stacje bazowe w sieciach komórkowych, o komputerach pokładowych w samochodach, o sofcie który jest zainstalowany na naszych kartach kredytowych. Kto by pomyślał, że na takiej karcie jest procesor, a na nim system operacyjny, a na nim dziarsko działa maszyna wirtualna Javy? (@see Java Card)

C rządzi!
We wspomnianych wyżej domenach często króluje język C. Po pierwsze z powodów historycznych - ktoś zaczął pisać w C i tak już zostało.

W miarę rozrostu tego typu systemów, pojawiają poważne problemy z ich utrzymaniem. Miliony wierszy kodu, gęsto pociętych przez dyrektywy kompilatora, kompilacje warunkowe, ifdefy, makra tam, gdzie to tylko możliwe oraz specyficzny sposób oszczędnego nazywania zmiennych zdecydowanie utrudniają rozwijanie tego kodu. Te problemy powodują, że środowisko embedded coraz częściej rozgląda się za podejściem obiektowym, które pomoże tworzyć łatwiejszy w utrzymaniu kod. Napotykają jednak na pewne przeszkody, które budują przekonanie, że obiektowość nie jest dla embedded. Zebrałem nieco informacji nt. tych przeszkód. Niektóre z nich są rzeczywiscie zaskakujące, inne to raczej przekonania.

Brak kompilatorów
Do pewnego momentu nie było zwyczajnie technicznej możliwości skomplikowania kodu. Jeśli już jakieś istniały, to były tak zabugowane, że dyskwalifikowały się same przez się. Sytuację poprawił gcc, który popchnął sprawę nieco do przodu.

Koszt wejścia
O ile maszyna wirtualna Javy jest dla PC-tów darmowa, to za wersję na embedded trzeba płacić grube pieniądze. Doświadczenia zaprzyjaźnionych firm pokazują, że ta implementacja JVM jest koszmarnie wolna. Istnieją sprzętowe implementacje (tak! sprzętowe) maszyny wirtualnej, ale niewiele o nich wiem.

Niedoskonałość sprzętu
W porównaniu do procesorów z kategorii ix86, procesory dla systemów wbudowanych są dość proste (tylko w porównaniu :)). Pisząc usługę EJB nie zastanawiamy się, czy aby procesor działa poprawnie, czy nie będzie błędu w działaniu urządzenia. W aplikacjach embedded niedoskonałość sprzętu jest częstym kłopotem.

Trudności w poszukiwaniu błędów
Tak powszechne zadanie jak poszukiwanie błędów również może nastręczać trudności, gdyż odbywa się to poprzez debugowanie oraz analizę kodu asemblera. Taka analiza jest zdecydowanie łatwiejsza dla kodu napisanego w C niż w C++.

Koronny argument - wydajność
Hmmm, trochę prawda, trochę mit. Tak myślę. Przeanalizujmy.

Po pierwsze:
Jak wspomniałem JVM jest rzeczywiście wolna. Z drugiej strony istnieje przekonanie, że kod napisany w C++ będzie działa duuuuużo wolniej. Tylko czy ktoś to rzeczywiście zmierzył? Czy ktoś napisał dwie tożsame implementacje, jedną w C, drugą w C++ i stwierdził to jednoznacznie?

Po drugie:
Przyczyną mogą być ograniczenia programowe i sprzętowe. Sporo zależy od tego jak jest napisany sam kompilator, czy potrafi wykorzystywać automagiczne sztuczki optymalizacyjne. Innym i często skutecznym sposobem zwiększania wydajności jest zmiana sprzętu. Ten jednak przenika do branży dosyć wolno. Powód jest trywialny - koszty. Nowy sprzęt - większe pieniądze, kolejny kompilator, kolejne bugi itd.

Po trzecie:
W rozwiązaniach embedded rzadko kiedy brakuje np. 5%, 10%, 20% wydajności. Jeśli już trzeba zwiększyć wydajność, bo powiedzmy, nie nadążamy z pomiarem temperatury pieca i grozi wybuchem, to trzeba ją zwiększyć kilkukrotnie. W takim przypadku obniżenie wydajności o parę, paręnaście procent, w związku z konstrukcjami obiektowymi, jest do przyjęcia. Jeśli efektem tego narzutu będzie łatwość utrzymania i rozbudowywania dylemat jest wart uwagi. Tak doszliśmy to wysłużonego już hasła: czytelność ponad wydajność. Nie oznacza to bynajmniej nonszalancji w szafowaniu pamięcią, lecz to aby w pierwszej kolejności koncentrować się na tworzeniu czytelnego kodu, a dopiero w drugiej mierzyć, profajlować i optymalizować.

Co dalej?
Jakie wyzwania stoją przed technikami refaktoryzacji i wzorcami, które w swoim najbardziej światowym wydaniu, utknęły gdzieś pomiędzy Javą a C#?
  • Jak pracować z hybrydowym legacy code, pisanym trochę w C trochę w C++?
  • Jak bezpiecznie przepisywać kod proceduralny na obiektowy?
  • Jakie standardy kodowania przyjąć dla C++, aby uniknąć pułapek wynikających z bogactwa języka
  • Jak sensownie rozwinąć narzędzia wspierające refaktoryzację. Sorki, ale VC++ i wsparcie dla refaktoryzacji to porażka. Visual Assist X jest super, ale to jeszcze nie to. Eclipse C/C++ nie próbowałem, Wind River Workbench jest ponoć dobry, ale i kosztuje słono
  • i cała reszta bajerów, których się w okół Javy i C# dorobiliśmy

Za różnymi problemami oraz przekonaniami kryją się ludzie, którzy poszukują skutecznych rozwiązań swoich problemów. Na pewno coś da się zrobić.

Tuesday, February 15, 2011

Myjcie swoje kubki!

Widziałem w kuchni jednej z firm taki napis:

Umyj po sobie kubek. Tobie łatwiej umyć jeden kubek niż nam sto.

Dziękujemy,
Serwis Sprzątajacy


Niniejszym ogłaszam ludzi z Serwisu Sprzątającego mistrzami refaktoryzacji. Przecież o to właśnie tu chodzi.

Łatwiej poradzić sobie z syfiastym kodem, gdy jest go jeszcze mało. Nawet jeśli jest i 200 kubków w zlewie, to dokładanie kolejnego z nadzieją, że może jeszcze dziś się nie rozleci, jest najgorszą rzeczą jaką można zrobić. Tak wiem: a bo czas, a bo pieniądze, a bo biznes, a bo babcia...

Być może biznes nie chce dać kasy na porządny Serwis Sprzątający, ale o swój kubek można z pewnością zadbać bez większego uszczerbku na zdrowiu.

Saturday, February 12, 2011

Pączkowanie kodu

Znalazłem analogię do Naturalnego porządku refaktoryzacji, o którym pisze Mariusz. Otóż kod pączkuje zupełnie jak drożdże.




Zaczynamy pisać od kawałka kodu, aby zadziałało, potem wyodrębnienie zmiennych, wyodrębnienie metod, przegląd odpowiedzialności, pojawiają się nowe klasy. Naturalny porządek refaktoryacji pozwala projektowy rosnąć i rozwijać się w odpowiednim tempie, z zachowaniem sensownej struktury.

Kłopot pojawia się, gdy próbujmy przyśpieszyć wzrost drożdżowego kodu. Zostawiamy zahardkodowane rozwiązania, na w projekcie pojawiają się brzydkie rozwiązania. Tego typu rzeczy się zdarzają, kłopot w tym, że zostają na zawsze. Wtedy zamiast wypieczonego projektu, dostajemy zakalec albo spaleniznę.

Każda idea ma swój czas inkubacji. Wydaje mi się, że nie ma dróg na skróty.

Tuesday, October 12, 2010

Kto płaci za dobry kod?

Oświeciło mnie ostatnio...przynajmniej trochę.
Gdy pracuję z innymi programistami nad tematami związanymi z jakością kodu bardzo często uderza mnie fakt, że pod względem technicznym żadnego z nich nie jestem w stanie nic, ale to nic nowego nauczyć!

Programiści zazwyczaj wiedzą:
  • jakie są słabe punkty ich kodu
  • jak powinien wyglądać ich kod
  • co dokładnie należy zrobić, aby ich kod wyglądał tak jak

Wiedzą to wszystko i...nic się nie dzieje! Nic się nie zmienia. Ciekawe jak to jest możliwe, że mimo tej ogromnej wiedzy, świadomości dobrych rozwiązań, znajomości wzorców projektowych, umiejętności tworzenia czytelnego kodu wciąż jest jak jest - czyli kod woła o pomstę do nieba, a my narzekamy.

Przeprowadziłem małe śledztwo, polegające głownie na zadawaniu pytań i nakłanianiu ludzi do wypełniania ankiet.

Podsumowując: nic się nie dzieje, ponieważ:
  • za działający kod płaci każdy, za dobry nikt
  • nacisk na jakość kodu powinien iść z góry, od menadżerów; programiści-idealiści w końcu zniechęcają
  • długotrwała praca ze kodem o złej jakości, skutkuje tendencją do dopasowywania się do otoczenia; równamy w dół i tworzymy kod, którego jakość jest nie lepsza niż jego otoczenie
  • obawiamy się, że radykalne decyzje projektowe okażą się niesłuszne w przyszłości

Wednesday, October 29, 2008

Uwaga na Immutable + JPA

Ostatnio użyłem wzorca Immutable w tradycyjnej implementacji

public class Team {

public Member getMember() {
return new Member( this.member );
}

}


Po zmapowaniu klasy adnotacjami JPA sporo głowiłem się dlaczego dostaję albo zbyt wiele wierszy w bazie albo wyjątek z informacją, że nastąpiła próba zapisu obiektu transient...rzut oka na powyższy kod wyjaśnia sprawę ;) ech...

Monday, July 21, 2008

TDD: O co właściwie chodzi?



Szczerze mówiąc nie wiem jak jest w polskich firmach z TDD. Wiem, że testy się pisze, pokrycie się bada, ale jak z samym TDD sprawy się mają – pojęcie mam bliskie zeru. Wszak pisanie testów i TDD to nie to samo.

Testy jednostkowe


Wiadomo co to są testy jednostkowe i jak działają – w gruncie rzeczy, chodzi o to, aby rozpocząć testowanie możliwie wcześnie na najbardziej elementarnym poziomie – na poziomie obiektów i ich metod. Co i jak testować – na tym skupię się innym razem. Teraz chodzi mi raczej o wyszczególnienie sytuacji z jakimi można zetknąć się podczas pisania testów jednostkowych.
  1. Testy dopisywane są po zakończeniu implementacji – cóż, jeśli w projekcie do tej pory testów nie praktykowano, to nie ma innej rady. Trzeba pamiętać tylko o jednej rzeczy: jeśli istniejąca metoda zawiera buga, który jeszcze się nie objawił, to napisany do niej test traktuje go jako poprawne działanie metody. Jest tak właśnie dlatego, że test pisany jest do istniejącej metody, przy założeniu że działa ona poprawnie. Trzeba się więc przygotować na niespodzianki.
  2. Najpierw pisany jest cały test, a następnie cała implementacja – jest to kłopotliwe ponieważ: trudno jest od razu zaplanować kompletny test dla metody, po implementacji często okazuje się, że nawet jeśli jest ona poprawna i tak otrzymujemy green bar. Często z tego powodu, że pomyłka była w teście. I co wtedy, o zgrozo, się dzieje? Zmieniany jest test, a to przecież to on miał być naszą ostoją i gwarantem poprawności implementacji. Jak się za chwilę przekonamy, testy i implementację piszemy przyrostowo – po kawałku.
  3. Pisane są zbędne testy w celu podniesienia współczynnika pokrycia – to taki przejaw instynktu samozachowawczego. Narzędzia do badania pokrycia po części wykrywają takie sytuacje. Ocenę tych praktyk pozostawiam Czytelnikowi.
  4. Wykrycie błędów nie powoduje dodania nowego przypadku testowego – jeśli testy nie ewoluują wraz z kodem, to osłabiana jest tkwiąca w nich siła. Każdy błąd wykryty w kodzie powinien spowodować, że: zostanie dodane nowy przypadek testowy wykrywający dany błąd, a następnie implementacja zostanie poprawiona. Dodanie nowego testu zabezpiecza przed ponownym wystąpieniem danego błędu.
  5. Test odpowiada klasie tylko z nazwy – czasami, w magiczny sposób, implementacja znacznie oddala się od testów. Dzieje się to zwłaszcza wtedy, gdy programiści nie mają nawyku rozpoczynania programowania od testu, pozornie brak czasu na testowanie, a jednocześnie w projekcie nie istnieje kontrola kodu.
  6. Zapomina się, że testy również podlegają refaktoringowi oraz wypracowano dla nich stosowne wzorce projektowe – wiadomo, że entropia wzrasta z upływem czasu. Nic dziwnego zatem, jeśli w pewnym momencie kod klasy testującej jest tak obrzydliwy i ciężki, że wcale nie chce się go czytać. Wzorce i refaktoring testów to temat na osobny artykuł.


Filozofia TDD


Całe TDD można zamknąć w powiedzeniu: „Kapitanowi, który nie wie dokąd płynie, każdy wiatr jest na rękę”. TDD stwierdza wprost: najpierw postaw cel, a potem do niego zmierzaj – najpierw test, potem implementacja. Ma to dawać następujące korzyści: tworzony jest tylko niezbędny kod – ten który jest istotny dla osiągnięcia celu, rozwiązanie jest przemyślane, ze względu na konieczność rozpoczynania od testu, TDD wymusza dobry projekt obiektowy, gdyż testowanie kodu luźno traktującego inżynierię oprogramowania, to droga przez mękę. Dodatkowo posiadanie zestawu dobrych testów to rzecz absolutnie konieczna jeśli chce się myśleć o bezpiecznym refaktoringu kodu.
Zatem jeśli najpierw należy napisać test, to jak ma wyglądać cały proces?
Wspomniałem wcześniej, że pisanie kompletnego testu, a następnie kompletnej implementacji nie jest dobrą praktyką. Zatem jak? Otóż, przyrostowo, spiralnie – kawałek testu, kawałek implementacji.
  1. Napisz fragment testu
  2. Napisz najprostszy możliwy kod, który spełnia test
  3. Zrefaktoruj implementację do pożądanego stanu
  4. Czy implementacja wciąż spełnia test?
    1. Tak: Jeśli implementacja nie zakończona idź do 1
    2. Nie: Idź do 3


Powyżej znajduje się ramowy algorytm tworzenia oprogramowanie poprzez TDD. Warto podkreślić istotność punktu 2. Dlaczego piszemy najprostszą możliwą implementację spełniającą test? Aby przekonać się czy test jest poprawny. Dlatego właśnie pisanie kompletnego testu od razu jest niewskazane – trudno jest zweryfikować jego poprawność.

Red-Green-Refactor: przykład Eclipse'a wzięty


Załóżmy, że tworzony jest sklep internetowy. Pierwszą funkcjonalnością, którą warto się zająć jest koszyk, z którego będzie korzystał użytkownik. Zaczniemy od odnajdywania produktów w koszyku.
  1. Tworzę szkielet klas:
    public class CartManager {
    public Product findProduct( String name ) {
    return null;
    }
    }
    
    public class Product {
    private String name;
    //...
    }
    

  2. Tworzę fragment testu jednostkowego jednostkowego:
    public class CartManagerTest extends TestCase {
    public void testFindProduct() {
    CartManager cartManager = new CartManager();
    
    Product product = cartManager
    .findProduct( "myProduct" );
    
    assertNotNull( product );
    }    
    }
    

  3. Uruchamiam test: Red Bar
  4. Refaktoruję implementację metody (najprostsza możliwa implmentacja!):
    public Product findProduct( String name ) {
    return new Product();
    }
    

  5. Uruchamiam test: Green Bar
  6. Rozbudowuję test:
    public void testFindProduct() {
    CartManager cartManager = new CartManager();
    Product product = cartManager.findProduct( "myProduct" );
    
    assertNotNull( product );
    assertEquals( "myProduct" , product.getName() );
    }
    

  7. Uruchamiam test: Red Bar
  8. Refaktoruję implementację metody (najprostsza możliwa implmentacja!):
    public Product findProduct( String name ) {
    Product product = new Product();
    product.setName( "myProduct" );
    return product;
    }
    

  9. Uruchamiam test: Green Bar
  10. Refaktoruję implementację klasy:
    public class CartManager {
    private Map<String, Product> cartMap
    = new HashMap<String, Product>();
    
    public Product findProduct( String name ) {
    Product product = new Product();
    product.setName( "myProduct" );
    
    cartMap.put( "myProduct" , product );
    return cartMap.get( "myProduct" );
    }    
    }
    

  11. Uruchamiam test: wciąż Green Bar
  12. 1.Refaktoruję test:
    public void testFindProduct() {
    CartManager cartManager = new CartManager();
    final String PRODUCT_NAME = "myProduct"; 
    
    Product putProduct = new Product();
    putProduct.setName( PRODUCT_NAME );
    
    Map<String, Product> cartMap
    = new HashMap<String, Product>();
    cartMap.put( PRODUCT_NAME , putProduct );
    
    cartManager.setCartMap( cartMap );
    
    Product product = cartManager.findProduct( PRODUCT_NAME );
    
    assertNotNull( product );
    assertEquals( PRODUCT_NAME , product.getName() );
    }
    

  13. Uruchamiam test: wciąż Green Bar
  14. Dodaję do testu nową asercję:
    public void testFindProduct() {    
    //...    
    assertNotNull( product );
    assertEquals( PRODUCT_NAME , product.getName() );
    assertSame( putProduct , product );
    }
    

  15. Uruchamiam test: Red Bar
  16. Refaktoruję implementację metody:
    public Product findProduct( String name ) {
    return cartMap.get( "myProduct" );
    }
    

  17. Uruchamiam test: Green Bar
  18. 1.Refaktoruję implementację metody:
    public Product findProduct( String name ) {
    return cartMap.get( name );
    }
    

  19. Uruchamiam test: Green Bar

To wszystko jeśli chodzi o zasadniczą funkcjonalność wyszukiwania w koszyku. Zadanie domowe brzmi następująco: jeśli w koszuku nie ma produktu o danej nazwie metoda findProduct powinna rzucić wyjątek. Dodaj tę funcjonalność pracując zgodnie z TDD. Podpowiedź: aby wymusić red bar użyj metody fail().

Czyli...


Gdybym chciał wybrać jedną rzecz do zapamiętania z tego artykułu to powiedziałbym: kawałek testu, kawałek implementacji...

Friday, June 13, 2008

Strategie decouplingu



W inżynierii oprogramowania występuje masowe dążenie do tworzenia systemów informatycznych z komponentów wielokrotnego użytku. Komponenty te mogą występować na różnym poziomie abstrakcji oraz złożoność. Artykuł poświęcony jest komponentom najniższego poziomu jakimi są klasy oraz ich obiekty.

Swobodna twórczość


O co właściwie chodzi z tym całym decouplingiem1? W gruncie rzeczy o przejrzystość, elastyczność i elegancję kodu. Wyobraźmy sobie następującą sytuację:

Pewien interfejs, SecurityManager, jest odpowiedzialny za zrealizowanie operacji autentykacji oraz autoryzacji. Po krótkiej analizie stwierdzamy, że chociaż przeprowadzenie operacji autentykacji należy do interfejsu SecurityManager, to już sama czynność sięgnięcia do bazy danych w celu pobrania potrzebnych informacji leży poza tą odpowiedzialnością. Na diagramie widać, że działania na bazie danych delegowane są do pomocniczego interfejsu UserCredentialsDAO.
Pojęcie decouplingu dotyczy powiązań pomiędzy poszczególnymi klasami/interfejsami w systemie, np.: powiązania pomiędzy implementacjami interfejsów SecurityManager i UserCredentialsDAO. Jak zatem zapewnić to powiązanie? Najprostszym możliwym sposobem – w konstruktorze.

public class JdbcSecurityManager implements SecurityManager {

public UserCredentialsDAO userCredentialsDAO;

public JdbcSecurityManager() {

userCredentialsDAO = new JdbcUserCredentialsDAO();

}


Szybko jednak okazuje się, że dana klasa wymaga więcej zależności, aby wykonać swoje zadanie. Dodatkowo chcemy mieć możliwość decydowania, które konkretne implementacje klas zależnych zostaną dostarczone do obiektu. W takim przypadku można zastosować parametryzowane konstruktory.
public class JdbcSecurityManager implements SecurityManager {

public UserCredentialsDAO userCredentialsDAO;

public SecurityHolder securityHolder;

public JdbcSecurityManager( UserCredentialsDAO credentialsDAO, 
SecurityHolder securityHolder ) {

this.userCredentialsDAO = credentialsDAO;
this.securityHolder = securityHolder;
}



Rozwiązanie to sprawia kłopot, gdy pisane są testy jednostkowe wykorzystujące klasę JdbcSecurityManager lub jej mocka. Niedogodność polega na tym, że czasem , teście jednostkowym chcemy mieć możliwość podmiany implementacji interfejsów UserCredentialsDAO oraz SecurityHolder. Z tego względu warto do tworzonej klasy , prócz parametryzowan/ch konstruktorów dodać również konstruktor domyślny oraz setery i getery do odpowiednich pól. W klasie wykorzystującej interfejs SecurityManager można wstawić następujący kod:

UserCredentialsDAO credentialsDAO = new JdbcUserCredentialsDAO();
SecurityHolder securityHolder = new SecurityHolder();

JdbcSecurityManager securityManager = new JdbcSecurityManager();
securityManager.setUserCredentialsDAO( credentialsDAO );
securityManager.setSecurityHolder( securityHolder );



Stosując opisaną powyżej strategię zapewniamy utworzenie powiązań pomiędzy obiektami w systemie. Jednocześnie sprawiamy, że obiekty są silnie uzależnione od siebie. Jest tak dlatego, że instancje poszczególnych klas są tworzone w kodzie – klasa nadrzędna tworzy instancje klas z nią współpracujących. O klasach, w których powiązania między nimi są realizowane w ten sposób mówimy, że są coupled. Konsekwencją takiego podejścia jest to, że jakakolwiek podmiana poszczególnych implementacji wiąże się z ingerencją w kod źródłowy. Taki kod jest nieelastyczny oraz trudny w utrzymaniu i rozwoju.

Przeciwwagą kodu, w którym klasy są coupled, jest kod w którym są one decoupled, to znaczy w maksymalnym stopniu od siebie niezależne. Współpracują ze sobą, ale można w łatwy sposób wymieniać poszczególne implementacje, a ingerencja w projekt jest ograniczona do minimum. Czynność zapewniania takiego stanu rzeczy nazywa się decoupling (lub po lekkim spolszczeniu decouplingiem) i jest przedmiotem niniejszego artykułu.

Fabryka


Ponownie odwołując się do pojęcia odpowiedzialności obiektów można zapytać, czy tworzenie nowych obiektów należy do odpowiedzialności klasy JdbcSecurityManager? Nie, gdyż jej zadaniem jest przeprowadzać operacje związane z bezpieczeństwem. Obiekt tej klasy nie może wykonywać swoich działań gdy nie ma obiektów pomocniczych, do których deleguje odpowiednie operacje. Remedium przychodzi w postaci scentralizowanego miejsca, w którym tworzone będą obiekty używane w systemie. Można zaimplementować to rozwiązanie w postaci wzorca SimpleFactory.

public final class ObjectFactory {
public static SecurityManager createSecurityManager( 
String type ) {

SecurityManager securityManager = //...  
return securityManager;
}
public static UserCredentialsDAO createUserCredentialsDAO( 
String type ) {
//... 
}
}



Metody fabryki muszą zdecydować, w jaki sposób należy zbudować dany obiekt, np. którą implementację należy utworzyć. Decyzja zostanie podjęta na podstawie parametru type metod fabryki.

public class BankTransferManager {
private SecurityManager securityManager;

public BankTransferManager() {
securityManager = ObjectFactory.createSecurityManager(                                                             
"securityManager" );
} 
}


W powyższym rozwiązaniu tworzenie klas i powiązań pomiędzy nimi zostało oddelegowane do osobnego obiektu fabryki. Dzięki temu nie klasa decyduje o użytych implementacjach lecz fabryka. Rozwiązanie zapewnia decoupling kodu, gdyż konkretne klasy nie są ze sobą trwale związane (np. poprzez tworzenie zależności w konstruktorach), zatem można w dowolnym momencie podmieniać implementacje modyfikując odpowiednio działanie klasy ObjectFactory. Oczywiście ingerencja w kod jest nieodzowna, lecz dotyczy tylko jednego obiektu, nie wszystkich.

Singleton

Czasem zachodzi konieczność, aby dany obiekt występował tylko raz w systemie, np. SecurityManager jeśli nie chcemy, próba autentykacji użytkownika była podejmowana przez dwa byty.
Może być też tak, że nie ma sensu tworzyć wielu bezstanowych obiektów zajmujących się tylko logiką, np. UserCredentialsDAO, gdyż w zupełności wystarczy tylko jeden.
W powyższych wypadkach stosuje się wzorzec Singleton, aby zapewnić, że dany obiekt zostanie utworzony tylko raz. Poniżej znajduje się przykładowa implementacja Singletonu.

public class LdapSecurityManager implements SecurityManager {

private static LdapSecurityManager instance;

private static LdapSecurityManager getInstance() {
if ( instance == null ) {
instance = new LdapSecurityManager();
}
return instance;
}
} 


Klasa ObjectFactory musi skorzystać z metod getInstance() do pobrania obiektu. Jeśli singleton ma być używany w aplikacji wielowątkowej musi być thread-safe. Za Williamem Pughem podaję taką implementację:

public class LdapSecurityManager implements SecurityManager {

private static class SingletonHolder {
private final static LdapSecurityManager instance
= new LdapSecurityManager();
}

private static LdapSecurityManager getInstance() {
return SingletonHolder.instance;
}
}


Należy pamiętać, że jeśli system działa na wielu maszynach wirtualnych, to klasa singletonu będzie ładowana na każdej z nich. Z tego względu zaleca się unikać tego rozwiązania w systemach rozproszonych.

Service Locator

Uogólnieniem ObjectFactory jest wzorzec Service Locator, którego zadaniem jest dostarczenie klientowi żądanej klasy usługowej, z tą różnicą, że klient nie wie skąd pochodzi dana usługa – czy jest lokalna, czy też zdalna.

Kontekst aplikacji


Alternatywą dla ObjectFactory jest zastosowanie obiektu, w którym przechowywany jest tzw. kontekst aplikacji. Kontekst zawiera w sobie wszystko to, czego system potrzebuje do poprawnego działania, np. obiekty realizujące konkretne usługi.

public class LdapSecurityManager implements SecurityManager {

public void authenticate( UserCredentials credentials,
AppContext context ) {

UserCredentialsDAO credentialsDAO
= context.get( "userCredentialsDAO" );
}
}


Każda metoda, w której potrzeba użyć usługi przechowywanej w kontekście będzie przyjmować dodatkowy parametr – AppContext, który umożliwia pobranie potrzebnego obiektu. Oczywiście podczas startu aplikacji należy najpierw zbudować odpowiedni kontekst.

Kiedy zatem używać obiektu kontekstu, a kiedy fabryki? Obiekt kontekstu umożliwia przechowywanie aktualnego stanu aplikacji do którego powinny mieć dostęp wszystkie obiekty (przechowywanie stanu w zewnętrznym obiekcie dostępnym poprzez metody statyczne jest mało eleganckie i nieintuicyjne). Fabryka skupia się tylko na tworzeniu obiektów. Konkretny wybór zależy o bieżących potrzeb. Kontekst, podobnie jak fabryka, zapewnia decoupling obiektów.

Dependency Injection


Konsekwencją stosowania zarówno fabryki jak i kontekstu aplikacji jest sytuacja, w której dana klasa musi zażądać obiektów pomocniczych, których chce użyć, od fabryki lub pobrać je z kontekstu aplikacji. Zatem dany obiekt dba o to, aby odnaleźć potrzebne mu obiekty współpracujące za pomocą fabryki, lokatora lub konktestu. Obiekt dba o swoje zależności.

Przeformułujmy problem w następujący sposób: żądamy takiej architektury aplikacji, w której dostarczony zostanie nam obiekt gotowy do użycia z już rozwiązanymi zależnościami. Takie podejście nosi nazwę Dependency Injection. Obiekty są zarządzane przez tzw. kontener. Przed użyciem należy zdefiniować obiekty oraz powiązania pomiędzy nimi, a następnie pobierać obiekty z kontenera i używać ich w systemie.



Na rysunku widać schemat Dependency Injection. Wszystkie obiekty oraz ich wzajemne relacje zdefiniowane są w zewnętrznym pliku XML, np:

<bean id="userCredentialsDAO" 
class="mbartyzel.decoupling.JdbcUserCredentialsDAO" />
<bean id="securityManager" 
class="mbartyzel.decoupling.JdbcSecurityManager">
<property name="credentialsDAO" ref="userCredentialsDAO" />
</bean>

Natomiast w systemie pobierane są obiekty z kontenera:
SecurityManager securitManager = BeanFactoryHolder
.getBean( "securityManager" );


Przykłady frameworków dostarczających kontenera DI to np.: SpringFramework, PicoContainer, Google-Guice.

Aby kod aplikacji był decoupled od frameworka oczekujemy, aby nie narzucał zarządzanym przez siebie obiektom implementowania specyficznych interfejsów. Gdyby tak było, kod stałby się zbyt zależny od samego framewokra. Niemożliwa byłaby wtedy sprawna zamiana jednego kontenera na inny, a testowanie utrudnione.

Podsumowanie


W artykule omówione zostały strategie decouplingu w systemach informatycznych takie jak: fabryka, Service Locator, kontekst aplikacji, kontener Dependency Injection. Autor ma nadzieje, że podane przykłady implementacji pozwolą Czytelnikom poprawić jakość tworzonego kodu. Nadrzędnym celem było wskazanie kilku możliwości osiągnięcia tego samego efektu. Mając wybór możemy, w danej sytuacji, świadomie decydować o przewadze jednego rozwiązania nad innym. Pamiętajmy, że jeśli jednym dostępnym narzędziem jest młotek, wszystko zaczyna wyglądać jak gwóźdź...