niedziela, 30 czerwca 2013

Jak rodzi się złożoność kodu - księga trzecia

Kiedy rozpoczynaliśmy pracę nad obecnym projektem mieliśmy już trochę doświadczeń i przemyśleń odnośnie tego co czyni kod niezwykle nieczytelnym i trudnym w rozwoju. Wśród głównych przyczyn leżały ogromne klasy opakowujące kod proceduralny, długie metody czy masa zagnieżdżonych warunków - a wszystko przypieczętowane zmiennymi o słodko brzmiących nazwach temp czy zzz.

Na tym polu odnieśliśmy zauważalny sukces gdyż nasza największa klasa ma chyba 400 linii, żadna metoda nie przekracza 30 linii a 90% kodu nie ma więcej niż jednego poziomu zagnieżdżenia. Klęskę ponieśliśmy za to na innym froncie, którego istnienia kilka lat tamu nawet się nie domyślałem. Otóż kiedy tworzyliśmy nasze małe czytelne klasy zawsze musieliśmy je gdzieś umieścić. DAO szło zazwyczaj do paczki costam.dao kontrolery do paczki costam.controlers itd.

Istnieje specjalna metryka w sonarze, która bada tego typu spierdoliny : I chociaż nie mam jeszcze w pełni intuicyjnego wyczucia tej metryki to wskazane wartości pokazują, że może być lepiej.

A co da nam to, że paczki nie będą tak poprzeplatane? Aby to zrozumieć złamiemy (ale tylko pozornie) jedną z zasad dobrego kodu obiektowego

Jak OOP tworzy Klasę Blob/Good

Klasa Money będzie dobrym przykładem bo w dzisiejszych czasach każdy potrzebuje pieniędzy. Na początek zobaczmy co się stanie kiedy przedstawimy pieniądze jako zwykłą strukturę danych:

 

public class Money {

    private final BigDecimal amount;
    
    private final Currency currency;

    public Money(BigDecimal bigDecimal, Currency currency) {
        this.amount = bigDecimal;
        this.currency = currency;
    }

    public BigDecimal getAmount() {
        return amount;
    }

    public Currency getCurrency() {
        return currency;
    }
}

Niestety powyższa forma jest zaproszeniem do tworzenia klas typu Utils czy Helper. A pod tym linkiem ----> http://nemo.sonarsource.org/ możecie zobaczyć, że klasy utils to jedne z bardziej złożonych tworów.

No to spróbujmy, żeby było bardziej obiektowo :

 

public class Money {
    private final BigDecimal amount;

    private final Currency currency;

    public Money(BigDecimal bigDecimal, Currency currency) {
        this.amount = bigDecimal;
        this.currency = currency;
    }
    
    public Money add(Money moneyToAdd){
        checkTheSameCurrency(moneyToAdd);
        return new Money(moneyToAdd.amount.add(this.amount),currency);
    }
    
    public boolean isGreaterThan(Money moneyToCompare){
        checkTheSameCurrency(moneyToCompare);
        return this.amount.doubleValue()> moneyToCompare.amount.doubleValue();
    }

    private void checkTheSameCurrency(Money moneyToAdd) {
        if(moneyToAdd.currency!=this.currency){
            throw new RuntimeException("zle");
        }
    }
}

Wydaje się być ok. Jest enkapsulacja, nie zdradzamy wewnętrznej implementacji, jest "tell don't ask" i inne takie dobre praktyki.

A co gdy domena się rozwija i muszę obliczyć podatek? "Tell don't ask" ?

 

public class Money {

    private final BigDecimal amount;

    private final Currency currency;

    public Money(BigDecimal bigDecimal, Currency currency) {
        this.amount = bigDecimal;
        this.currency = currency;
    }
    
    //!!!!!!
    public Tax calculateTax(double percentage){
        return new Tax(amount.multiply(BigDecimal.valueOf(percentage)));
    }

No i pytanie co tak naprawdę symbolizuje klasa Money? Jeśli reprezentuje pieniądz to polityka podatkowa średnio tam pasuje. Oczywiście można powiedzieć - "no to dodajmy tam operacje mnożenia czy też wyciągania procentu". Tak a za chwilę dodamy operacji całki i obliczania pola. A co jeśli będę chciał przeliczać waluty - czy w ramach enkapsulacji dodać funkcjonalność kantoru do klasy Money albo Currency?

Jeśli mówić o smrodzie w kodzie to powyższe opcje dla mnie niezwykle śmierdzą. A co dyby tak wykorzystać modyfikator dostępu, którego nie ma?

 

public class Money {

    private final BigDecimal amount;

    private final Currency currency;

    public Money(BigDecimal bigDecimal, Currency currency) {
        this.amount = bigDecimal;
        this.currency = currency;
    }
    
    BigDecimal getAmount() {
        return amount;
    }

...
}
public class TaxCalculator {

    private static final double standardTaxPercentage=0.23;
    
    public Tax calculateTax(Money money){
        return new  Tax(money.getAmount().multiply(BigDecimal.valueOf(standardTaxPercentage)));

    }
}
Dla wielu osób wynik tego niezwykle wciągającego eksperymentu myślowego może być czymś oczywistym ale ja na swojej drodze napotykam wiele osób, które potrzebują takiej wiedzy (w tym mnie). Ładujemy Money i TaxKalkulator do jednej paczki i mamy enkapsulację pieniędzy w ramach tej paczki. Powinno nam to pomoc w uniknięciu problemu klasy BLOBa ale co jeśli owa paczka niezwykle nam się rozrośnie? Tutaj trudno mi określić granicę kiedy "jest już za dużo".

Bonus - skąd się bierze fundamentalizm obiektowy

Znam pewnych ludzi, którzy kłócili by się, że powyższy przykład można rozwiązać jakimś wizytatorem czy innymi cudami. Problem polega na tym, że czasem mogą mieć rację a czasem nie ale sam fakt uznawania zasady rodem z wikipedii typu "tell don't ask" czy "DRY" za święte i nienaruszalne doprowadza mnie do szalonego płaczu. A w ogólności dobija mnie traktowanie OOP jako panaceum na wszystko - czyli zarzuty typu "ten kod jest zły bo nie obiektowy".

Jeśli ktoś ma niech zerknie sobie do rozdziału nr 6 "Czystego kodu" pod tytułem "struktury danych". Są tam dwa przykłady obliczania pól figur - jeden obiektowy a drugi proceduralny. Nie chce mi się kopiować tutaj źródeł ale wniosek postawiony przez autora jest jasny: wersja bardziej proceduralna z ifami jest lepsza jeśli dojdą nam nowe funkcje (jedno miejsce do zmiany vs wszystkie klasy figur do zmiany) - ale wersja z polimorfizmem jest lepsza jeśli spodziewamy się nowych kształtów - wniosek: obiektowy nie znaczy najlepszy.

Bo nawet czy jest sens toczyć spory odnośnie tego czy kod z ogromną klasą ale idealna enkapsulacją jest bardziej obiektowy niż kod z małymi klasami, które jednak nie starają się robić wszystkiego jednocześnie udostępniając gdzieniegdzie szczegóły implementacyjne?

A może jednak przychodzi czas aby nie ograniczać się do racjonalizacji swoich wyborów prostymi zasadami z wikipedii ale zgłębić dokładniej jakie te wybory niosą konsekwencje? Są różne modele rozwoju zdolności : Shu Ha Ri , SU HA RY , SŁU CHAJ STARY - opisujące jak to od prostych reguł należy przechodzić do zrozumienia zasad poprzez magiczną transcendencję. Na samym początku jest bagno, totalnie zjebany kod i tutaj jest miejsce na stosowanie zasad "Tell don't ask" czy "DRY" bez myślenia bo cokolwiek się nie zrobi to będzie lepiej. Ale jeśli zostanie się na tym etapie to jesteśmy na prostej ścieżce do Obiektowego Fundamentalizmu

O a teraz złamię jeszcze jedną zasadę - święte DRY

 

class PracownikUczelni{
private String tytuł;
}

class Film{
private String tytuł;
}

No i DRY jest złamane. Można oczywiście zrobić też tak:
 

class abstract PosiadaczTytułu{
private String tytuł;
} 

class PracownikUczelni extends PosiadaczTytułu{}

class Film extends PosiadaczTytułu{}

No i DRY jest spełnione a przy okazji mamy zjebaną hierarchię klas. NA początek zasady z wikipedii są dobre ale później trzeba zacząć używać mózgu - nie ma wyjścia. A jeśli ktoś potrzebuje potwierdzenia od jakiejś gwiazdy światowego kalibru to Dan North w jednej ze swoich prezentacji napisał "DRY is an enemy of decoupled" (A jeśli dla kogoś Dan North nie jest gwiazdą światowego kalibru albo zastanawia się "kto to k***a jest?" to niech delektuje się moimi wnioskami)

niedziela, 9 czerwca 2013

Jak rodzi się złożoność kodu - sezon drugi

W poprzednim odcinku opowiadaliśmy sobie o złożoności cyklomatycznej i również w tamtej części znajduje się rozbudowany wstęp epicko opowiadający o tym dlaczego należy się interesować jakością kodu i takie tam. Aby nie tracić teraz miejsca i czasu idziemy prosto do kolejnych metryk.

Spójność klasy - LCOM i LCOM4

Gdy po raz pierwszy zobaczyłem metrykę LCOM4 w sonarze do głowy przyszło mi, że to musi mierzyć ilość materiałów wybuchowych w kodzie albo coś takiego. W zasadzie to wysokie wskazania tej metryki mogą objawiać nieuchronną eksplozję projektu gdyż mierzy ona jak spójne są nasze klasy. Niech kod przemówi ...

ProductManager

Manager, Helper, Procesor - podobno to są skróty od zdania "nie mam pojęcia co dokładnie ma robić ta klasa i dlatego nie wiem jak ją nazwać". Niestety widziałem na własne oczy jak takie klasy rozrastają się do ponad 10000 linii bo jak coś nazywa się "Manager" to w zasadzie wszystko tam pasuje.
Na początek mamy :

 

public class ProductManager {
private List products = newArrayList();

 public List showProducts() {
  List currentlyAvailableProducts = newArrayList();
  for (Product product : products) {
   addProductIfAvailable(currentlyAvailableProducts, product);
  }
  return currentlyAvailableProducts;
 }

 private void addProductIfAvailable(
   List currentlyAvailableProducts, Product product) {
  if (product.isAvailable()) {
   currentlyAvailableProducts.add(product);
  }
 }
}

Taka sobie klasa opakowująca listę produktów. Na razie poza nazwą nie widać innych niebezpieczeństw. Zobaczmy co wskazują metryki :

  • (Metrics plugin) LCOM - 0
  • (Sonar) LCOM4 - 1

Nadszedł czas przyjrzeć się co to są te LCOM i LCOM4. Pierwsza - LCOM - jest dosyć dyskusyjna za co została podobno dosyć skrytykowana - ale o tym za chwilę. LCOM weźmie każdą parę metod i sprawdzi czy chociaż jedna z danych na których one operują jest wspólna. Na razie mamy dwie metody jedną publiczną jedną prywatna ale obydwie działają na produktach. Wynik zero oznacza, że nie ma żadnych metod rozłącznych działających na różnych danych czyli jest zajebiście.

LCOM4 z drugiej strony sprawdza czy wszystkie metody i dane są połączone strzałkami tak jak na rysunku poniżej (fachowo to się nazywa ilość niezależnych grafów).

Na razie wszystko jest połaczone czyli wartości metryki to 1 czyli jest zajebiście.

Wiele odpowiedzialności

Dodajmy teraz nową odpowiedzialność do klasy : obliczanie cen produktów ( w końcu to product manager)

 
public class ProductManager {

 private List products = newArrayList();


 private Set vipUsers=newHashSet();
 
 private BigDecimal vat=new BigDecimal("0.23");

 
 public List showProducts() {
  List currentlyAvailableProducts = newArrayList();
  for (Product product : products) {
   addProductIfAvailable(currentlyAvailableProducts, product);
  }
  return currentlyAvailableProducts;
 }

 private void addProductIfAvailable(
   List currentlyAvailableProducts, Product product) {
  if (product.isAvailable()) {
   currentlyAvailableProducts.add(product);
  }
 }
 
 public BigDecimal calculateGrossPrice(Product product,User user){
  if(vipUsers.contains(user)){
   return product.getNetPrice();
  }
  
  BigDecimal netPrice = product.getNetPrice();
  return netPrice.add(netPrice.multiply(vat));
 }

}

  • (Metrics plugin) LCOM - 1
  • (Sonar) LCOM4 - 2


Ponieważ metody obliczania ceny brutto oraz wyświetlania produktów korzystają z zupełnie innych zestawów danych, więc na rysunku dostaniemy dwa rozłączne grafy - co zwyczajnie oznacza, że możemy te funkcjonalności spokojnie rozdzielić.

Problemy z LCOM4 i Sonarem

Pierwsza wersja kalsy z LCOM4=2 korzystała tylko z pola vat i wyglądała mniej więcej tak :

public BigDecimal calculateGrossPrice(Product product,User user){
BigDecimal netPrice = product.getNetPrice();
return netPrice.add(netPrice.multiply(vat));
}

Pomimo, że w teorii powinno to wystarczyć aby podbić LCOM4 do dwójki to jednak sonar z premedytacją zlewał tę metodę. Po małych poszukiwaniach okazało się, że problem nie jest trywialny gdyż czasami metody takie jak gettery, toString() czy jakieś dziedziczenia narzucone przez frameworki sztucznie podbijają LCOM4. Programiście Sonara wyłączają samotne metody jednak jak się okazuje trochę tutaj przekombinowali w druga stronę i czasami takie małe niezależne metody nie są brane pod uwagę przy obliczaniu LCOM4. Więcej w ticketach :
* SONARJAVA-65
* SONAR-2941
* SONAR-2934
* dyskusja - " A getter/setter is defined as method which accesses a single field" - właśnie to miało chyba miejsce w powyższym przypadku, nawet jak technicznie nie był to getter ale taka metodka utilsowa.

A wracając do wartości LCOM=1 w pluginie metrics. Generalnie wartosć 1 to z tego co zrozumiałem najgorsza wartość jaką możemy uzyskać gdyż zakres dopuszczalnych wartości to [0,1]. Oznacza to ni mniej ni więcej jak to, że możemy dalej psuć klasę a wartość LCOM będzie cały czas wynosić 1. Chyba stąd ta cała krytyka. LCOM4 będzie się zwiększać razem z kolejnymi nadmiarowymi funkcjonalnościami (minus optymalizacje sonara)

Rozwiązanie

Tutaj wystarczy jakoś rozsądnie podzielić managera na odrębne kalsy. Np. część finansowa może pójść do :
 
public class FinancialCalculator {

 private Set vipUsers = newHashSet();

 private BigDecimal vat = new BigDecimal("0.23");
 
 
 public BigDecimal calculateGrossPrice(Product product,User user){
  if(vipUsers.contains(user)){
   return product.getNetPrice();
  }
  
  BigDecimal netPrice = product.getNetPrice();
  return netPrice.add(netPrice.multiply(vat));
 }
}


No i teraz trzeba wykazać się niesamowitymi pokładami braku inteligencji aby do takiej klasy wrzucić metodę "showProducts" gdzie do "Managera" czy "Helpera" nawet by pasowała bo tam wszystko pasuje.

RFC - Response For Class

Problem z tą metryką jest taki, iż trudno stwierdzić czy dana wartość to już za dużo czy jeszcze nie. Te wskazania traktować należy jako sygnał, że coś tutaj może być nie tak.

Wpłynąć można na nią raczej dosyć prosto - redukując liczbę wywołań innych klas ale także redukując liczbę wywołań siebie przez inne klasy. I już na początek niespodzianka. Poniższa pusta klasa ma RFC=2. Według dokumentacji pierwsze "+1" Bierze się z okazji domyślnego konstruktora. Skąd się bierze +2 - pojęcia nie mam - widocznie poza domyślnym konstruktorem sonar znajduje tam coś jeszcze.

 
public class BlogCreator {
}

Po dorzuceniu węgla :

 

public class BlogCreator {

 private Browser browser;
 private Keyboard keyboard;

 public void writeBlog() { +1
  browser.loginToBlogger();  +1
  Topic postTopic = workOnTopic();
  Post typedPost = keyboard.type(postTopic); +1
  review(typedPost); 
  browser.submit(typedPost); +1
 }

 private Topic workOnTopic() { +1
  return new Topic(); +1
 }

 private void review(Post typedPost) { +1
  //coś tam
 }
}

Mamy RFC=9. Powtórzonych wywołań się nie liczy czyli jakbym użył raz jeszcze browser.loginToBlogger() to RFC byłoby stałe. Podobnie tylko raz liczy się metody wewnątrz klasy czyli workOnTopic() jest zliczane przy deklaracji i można to sobie wywoływać ile razy tam się chce.

Czy RFC=9 jest duże? Kod powyżej jest raczej czytelny i paradoksalnie wrzucenie wszystkiego w jedną wielką metodę mogłoby zmniejszyć jednocześnie RFC jak i czytelność kodu (mniej metod ~ mniejsze RFC). Także osobiście polecałbym traktować tę metrykę jako taki wskaźnik pomocniczy a nie wartość absolutną.

Jak można zmniejszyć RFC zwiększając czytelność kodu? Trzeba pobawić się poziomami abstrakcji i przykryć koncepcje bardzo szczegółowe czymś bardziej ogólnym :

 
public class BlogCreator2 {

 private Medium internetMedium;
 
 public void writeBlog() {
  MentalCreation ideaForBlog=awakeDivineSparkAndTrascendenOverYourHumanLimitation();
  internetMedium.publishForHumanity(ideaForBlog);
 }

 private MentalCreation awakeDivineSparkAndTrascendenOverYourHumanLimitation() {
  return new MentalCreation();
 }
}

W tym przypadku się okazało się, że RFC spadło do 6. Dobrze? Niedobrze? Nie wiadomo. Generalnie wiele zależy tutaj od percepcji tych, którzy będą kod czytać. Generalnie po wpisaniu słowa "abstraction" w Amazonie na pierwszych miejscach wyskakują pozycje o sztuce, malarstwie i psychologii. Chyba jednak pewne rzeczy łatwiej będzie ogarnąć na polu matematyki i programowania funkcyjnego.

Z drugiej strony jak gdzieś w projekcie RFC wyskoczy na poziomie 50 to jednak coś może być mocno zjebanego. ProductManager z tego odcinka ma RFC=17. FinancialCalculator już tylko RFC=9; ProductProvider RFC=10 (zrodla tego nie wklejałem ale to jest reszta, która została w ProductManagerze po ekstrakcji FinancialKalkultora). Czyli każdą z tych klas teoretycznie łatwiej przeczytać ale razem mają RFC większe od RFC samego ProductManagera.

Klasa CyclomaticExample z poprzedniego odcinka w swojej najbardziej zjebanej formie ma RFC=21. Po refaktoringu RFC=11. Czyli w jakimś tam stopniu można założyć, że im RFC mniejsze tym czytelniej

Materiały

niedziela, 2 czerwca 2013

Jak rodzi się złożoność kodu (part łan)

Był rok 2011 - konkretnie lato lub jesień. W tle rozpoczynała się restrukturyzacja firmy a na pierwszym planie trwały batalię o termin oddania kolejnej wersji produktu. Standardowo jak to w takich sytuacjach bywa funkcjonalność na deadline była absolutnym priorytetem a kod to tam sobie posprzątamy później w wolnym czasie. Zaczęły pojawiać się nawet takie dziwne i egzotyczne pomysły w stylu "nie piszcie testów to według obliczeń dostarczycie 30% szybciej" albo " teraz szybko zaimplementujcie a jak będzie sukces to później zrobi się sprint lub dwa na refaktoring". Oczywiście gdy kończy się implementować jeden "urgent ficzer" wtedy okazuje się, że to był tylko taki przedsionek dla kolejnej niezbędnej funkcjonalności.

Cóż, każdy z nas(w zespole) przeżył pracę z klasami liczącymi 10000+ linii kodu i dobrze wiedzieliśmy do czego prowadzi patologia "poprawimy później". Dlatego praktyką zespołu było i jest ciągłe sprzątanie po sobie. Dzięki temu byliśmy w stanie dostarczyć kolejne super urgent ficzery w 2012 i 2013. Jednocześnie pojawił się postęp kulturowy kiedy to użytkownicy zwani biznesowymi zaczęli przejawiać zrozumienie w kierunku konceptu "dostarczamy w miarę regularnie bo pilnujemy wysokiej jakości kodu". To był taki krok pierwszy w kierunku wysokiej higieny kodu - zrozumienie, że jak się nie posprząta gówna to będzie śmierdzieć. Czas teraz na kolejny krok czyli dlaczego tak często trzeba latać do kibla - (kontynuując metaforę - trzeba zrozumieć co jemy i jak to wpływa na żołądek)

Tyle tytułem epicko-kulinarnego wstępu.

Jaka jest właściwie złożoność kodu?

Oczywiście każdy z nas czytał "czysty kod" nawet po kilka razy. Mimo tego co jakiś czas musimy odwiedzić wyniki Sonara aby zobaczyć czy naruszenia kodu znowu nie pikują w okolicach akceptowalnych granic. W naszym projekcie sformułowanie "o kurwa" pojawia się przy około 50 naruszeniach - w innych projektach punkt niemiłego zaskoczenia może znajdować się gdzieś indziej. I chociaż wspomniany "Czysty kod" to doskonała podstawa zawierająca wiele reguł, które każdy programista powinien wbić sobie do głowy (może wtedy zmienne w stylu "temp1" czy "zzz" zniknęłyby z kodu) to jednak aby uzyskać intuicyjne zrozumienie złożoności kodu być może musimy zejść trochę głębiej.

Tutaj dla analogii także przemycę fakt żywieniowy. Podobnie jak ogólne rady "pisz czysty kod, małe metody i czytelne nazwy" wyznaczają ogólny kierunek poszukiwań tak samo rady "jedz zdrowo i dużo się ruszaj" są tylko początkiem. Trzeba zejść głębiej aby zrozumieć jak działają poszczególne składniki. Przykładowo na półce w sklepie stoi sobie taka buteleczka soku z napisem slim za 2,99. Producent mógł tam napisać duże "slim" czy "odchudzanie" ponieważ dodał śladowe ilości L-karnityny, która w niezależnych badaniach naukowych faktycznie ujawniła właściwości odchudzające ALE... po pierwsze L-karnityna pomaga w transporcie tłuszczy w trakcie wysiłku czyli jeśli nie ruszy się dupy to i tak ten soczek w niczym nie pomoże... a co śmieszniejsze aby polepszyć smak i odczucie konsumenta w napoju znajdują się różne cukry i słodziki, które spowodują wyrzut insuliny, która z kolei zablokuje tłuszcz w komórkach... Wpieprzamy w dobrej wierze taką buteleczkę słodzików i później niestety trzeba robić refaktoring organizmu.

Ale wracając do kodu

Badanie złożoności poprzez metryki

Kwestia z metrykami jest nieco skomplikowana bo mogą być one pomocne ale niestety często są wykorzystywane do oceny ludzi co całkowicie je dyskwalifikuje. Jeśli nie użyjemy jej jako targetu czy innego gówna to może pomóc bo zobaczmy. Jeśli np. ktoś mi powie "będzie do przeniesienia 100kg cementu" to mniej więcej czuję o jaką ilość i materiału i pracy tu chodzi. Jeśli zaś ktoś powie "musimy dodać logowanie do kodu o złożoności cyklomatycznej 17" to ilu z was tak naprawdę "CZUJE" z czym ma do czynienia. Dodatkowo - ilu z was wie jak się doprowadza kod do złożoności cyklomatycznej 17? No właśnie. Dlatego dzisiaj zerknijmy na tę metrykę.

Jest sobie kod będący przepisem na wyciągnięcie pieniędzy z bankomatu.

 
public void wyciągnijPieniadzeZBanku(){
  ubierzSie();
  WezKarte();
  IdzDoBankomatu();
  UzyjKarty();
  SchowajKase();
  WracajDoDomu();
 }

Do obliczenia złożoności cyklomatycznej wykorzystamy plugin "metrics" dla eclipse co w praktyce wygląda tak:

Dlaczego wartość tej metryki wynosi 1? Złożoność cyklomatyczna to tak naprawdę odpowiedź na pytanie "W ilu miejscach kod może zmienić swój bieg"

 
public void wyciągnijPieniadzeZBanku(){
  if(padaDeszcz){
   wezParasol();
  }
  ubierzSie();
  WezKarte();
  IdzDoBankomatu();
  UzyjKarty();
  SchowajKase();
  WracajDoDomu();
 }

Możemy wziąć parasol ale nie musimy - są dwie drogi wykonania metody - złożoność cyklomatyczna wzrosła do 2. Czy kod stał się mniej czytelny? Cóż to jest indywidualna ocena każdego czytelnika. Dla mnie nadal jest czytelny. Co do czytelności to tutaj jest jeszcze jedna kwestia. Zerknijmy na dwa kawałki kodu

 
public void wyciągnijPieniadzeZBanku(){
  if(padaDeszcz){
   wezParasol();
  }
  ubierzSie();
  WezKarte();
  IdzDoBankomatu();
  UzyjKarty();
  SchowajKase();
  
  if(portfel.isEmpty()){
   WracajDoDomu();
  }else{
   IdzZKolegamiNaBrowara();
  }
 }
oraz
 
public void wyciągnijPieniadzeZBanku(){
  if(padaDeszcz){
   wezParasol();
  }
  ubierzSie();
  WezKarte();
  IdzDoBankomatu();
  UzyjKarty();
  
  for (int i = 0; i < banknoty.size(); i++) {
   portfel.schowaj(banknoty.get(i));
  }
  
  WracajDoDomu();
 }

Obydwa kawałki kodu maja złożoność cyklomatyczną na poziomie 3 chociaż można się spierać czy oba są czytelne/nieczytelne w tym samym stopniu. Dlatego też nie ma się co spinać, że sama złożoność na danym poziomi dostarcza nam 100% potrzebnych informacji. Druga kwestia. Można by zastosować w kodzie pętlę "ForEach" ale niestety plugin metrics liczy tę złożność cyklometryczną zliczając ilość warunków logicznych dlatego zupełnie ignoruje konstrukcję "foreach" - sonar dla odmiany dał sobie radę.

 
public void wyciągnijPieniadzeZBanku(){
  if(padaDeszcz){
   wezParasol();
  }
  ubierzSie();
  WezKarte();
  IdzDoBankomatu();
  UzyjKarty();
  
  for (int i = 0; i < banknoty.size(); i++) {
   portfel.schowaj(banknoty.get(i));
  }
  
  if(portfel.isEmpty() || nieChleje){
   WracajDoDomu();
  }else{
   IdzZKolegamiNaBrowara();
  }
 }

Tutaj dodaliśmy jeszcze jeden warunek logiczny (niechleje) i Cyclomatic Complexity wynosi już 5. Pomału zaczyna się robić bajzel.Ile mamy teraz puntów decyzyjnych?

  • Możemy brać parasol albo i nie
  • możemy schować pieniądze do portfela albo i nie
  • Jak nie mam kasy w portfelu to idę do domu
  • Ale nawet jak mam kasę i nie chleję to i tak idę do domu
Jeśli chcielibyśmy przetestować wszystkie scenariusze to też ich tam chyba jest już z kilkanaście. No to dorzućmy coś jeszcze :

 
public void wyciągnijPieniadzeZBanku(){
 if(padaDeszcz){
   wezParasol();
  }
  
  ubierzSie();
  
  if(jestZimno){
   ZalozDodatkowoSportowyDres();
  }
  
  WezKarte();
  
  if(golf.jestZatankowany()){
   podjedzDoBanku();
  }else{
   IdzDoBankomatu();
  }
  
  if(pin==null){
   pin=telefon.dzwonDo(ZIOMALE.WIESIEK);
  }
  
  banknoty=UzyjKarty(pin);
  
  for (int i = 0; i < banknoty.size(); i++) {
   portfel.schowaj(banknoty.get(i));
  }
  
  if(portfel.jestPusty() || nieChleje){
   WracajDoDomu();
  }else{
   IdzZKolegamiNaBrowara();
  }

Złożoność cyklomatyczna tego kawałeczka wynosi już 8. Sądzę, że zdolności percepcyjne większości czytelników są na takim poziomie, że każdy dostrzeże w tym kodzie naturę gówna. I na koniec ulubiona praktyka w pewnych kręgach czyli arrow code.

 
public void wyciągnijPieniadzeZBanku(){
 if(padaDeszcz){
   wezParasol();
  }
  
  ubierzSie();
  
  if(jestZimno){
   ZalozDodatkowoSportowyDres();
  }
  
  WezKarte();
  
  if(golf.jestZatankowany()){
   podjedzDoBanku();
  }else{
   IdzDoBankomatu();
  }
  
  if(pin==null){
   pin=telefon.dzwonDo(ZIOMALE.WIESIEK);
  }
  
  banknoty=UzyjKarty(pin);
  
  for (int i = 0; i < banknoty.size(); i++) {
   portfel.schowaj(banknoty.get(i));
  }
  
  if(portfel.jestPusty() || nieChleje){
   WracajDoDomu();
  }else{
   while(telefon.mamSrodki()){
    for (ZIOMALE ziomal: ZIOMALE.values()) {
     if(telefon.dzwonDo(ziomal)!=null){
      if(zabkaJEstOtwarta()){
       IdzZKolegamiNaBrowara();
      }
     }
    }
   }
  }

Plugin Metrics określi wartość złożoności cyklometrycznej na 9 zaś sonar na 12. Jest to dowód na to, że nie ma co się co przywiązywać do konkretnych wartości tej metryki. Generalnie to co pokazuje sonar bardziej do mnie przemawia gdyż ten "arrow code" dosyć tutaj namieszał.

Teraz zostaje nam już tylko to gówno refaktorować. Na szybko może wyjść coś takiego :

 
public void wyciągnijPieniadzeZBanku(){
 przygotujSie(padaDeszcz,jestZimno);
  WezKarte();
  SrodekTransportu srodekTransportu=uzyskajSrodekTransportu();
  srodekTransportu.udajSieDoBankomatu();
  pin=znajdzPin();
  banknoty=UzyjKarty(pin);
  portfel.schowaj(banknoty);
  OpcjaNaWieczor opcjaNaWieczor=zbadajMojeOpcjeNaWieczor(portfel);
  opcjaNaWieczor.zrealizuj();

Złożoność cyklomatyczna tej metody wynosi znowu 1. Mamy również chyba jeden spójny poziom abstrakcji. Niektóre koncepcje jak Telefon czy Ziomal zostały przykryte przez bardziej abstrakcyjne twory jak OpcjaNaWieczor. Gdzie się podziała złożoność? Nie zniknęła ale została równomiernie rozłożona a powstałe konstrukcje obiektowe mogą (lub nie) zwiększy "reużywalność" kodu i zmniejszyć koszt utrzymania.

Podsumowując: Im większa wartość złożoności cyklometrycznej (które lepiej brzmi z angielska Cyclomatic complexity) tym gorzej ale z drugiej strony konkretne wartość nie mówi nam wszystkiego i jest zależna od narzędzia. W kolejnych odcinach przedstawię naturę innych metryk. Mam nadzieję, że ten sposób tłumaczenia był bardziej efektywny niż pisanie wzoru na CC "M = E − N + 2P" i nurkowanie w teorię grafów (której sam do końca nie rozumiem"

Dodatkowe źródła :

  1. http://en.wikipedia.org/wiki/Cyclomatic_complexity
  2. http://www.guru99.com/cyclomatic-complexity.html

niedziela, 26 maja 2013

Od Zera Do Matchera

To jest materiał podstawowy, który przygotowałem do wewnętrznej prezentacji w pracy. Jeśli ktoś jest obcykany w matcherach niech nie traci czasu i poczyta sobie lepiej jakieś dzieła w stylu trzynastej księgi Pana Tadeusza.

Jeśli jednak ta tematyka jest Ci obca to zapraszam do lektury

Domena

Domenę biznesową tworzą dwie klasy - piosenka i techno mikser

 

public class Song {

 private String title;

 private int numberOfBits;
 
 public Song(String title,int numberOfBits) {
  this.title = title;
  this.numberOfBits = numberOfBits;
 }

 public void play(){
  System.out.println("plying "+title);
 }
 
 public String getTitle() {
  return title;
 }
 
 public int getNumberOfBits() {
  return numberOfBits;
 }
}

 

public class TechnoMixer {
 public List makeTechnoMix(Song... originalSongs){
  int technoMultiplier = 2;
  String technoPostfix = "_techno_mix";
  return mixSongs(technoMultiplier, technoPostfix, originalSongs);
 }
 
 public List makeThunderdomeMix(Song... originalSongs){
  int thunderdommeMultiplier = 4;
  String thunderdommePostfix = "_thunderdomme_mix";
  return mixSongs(thunderdommeMultiplier, thunderdommePostfix,
    originalSongs);
 }

 private List mixSongs(int bitsMultiplier,
   String titlePostfix, Song... originalSongs) {
  List mixedSongs=new ArrayList<>();
  for (Song originalSong : originalSongs) {
   int newNumberOfBits=originalSong.getNumberOfBits()*bitsMultiplier;
   String newTitle=originalSong.getTitle()+ titlePostfix;
   mixedSongs.add(new Song(newTitle, newNumberOfBits));
  }
  return mixedSongs;
 }
}

Działanie jest proste - TechnoMixer zwyczajnie zwiększa ilość bitów znacząco podnosząc komfort słuchania piosenki :) No i odpowiednio modyfikuje tytuł utworu oznajmując wszem i wobec, że słuchamy lepszej jej formy

Spróbujmy to przetestować...

Test próba pierwsza

 

@Test
public void shouldMixSongsToTechno() {
 //given
 Song equador=new Song("Equador", 100);
 Song onaTanczyDlaMnie=new Song("Ona Tanczy Dla Mnie", 50);
 Song harlemShake=new Song("Harlem Shake", 150);

 //when
 List mixes = technoMixer.makeTechnoMix(equador,onaTanczyDlaMnie,harlemShake);
 
 //then
 Assert.assertEquals(mixes.get(0).getNumberOfBits(), 200);
 Assert.assertEquals(mixes.get(1).getNumberOfBits(), 100);
 Assert.assertEquals(mixes.get(2).getNumberOfBits(), 300);
 
 Assert.assertEquals(mixes.get(0).getTitle(), "Equador_techno_mix");
 Assert.assertEquals(mixes.get(1).getTitle(), "Ona Tanczy Dla Mnie_techno_mix");
 Assert.assertEquals(mixes.get(2).getTitle(), "Harlem Shake_techno_mix");
}

Testy przechodzi ale wygląda kiepsko - a pamiętajcie, że to tylko prosty przykład. Przy bardziej skomplikowanych obiektach będzie jeszcze więcej syfu.

Test próba druga - zewnętrzna metoda

 

@Test
public void shouldMixSongsToTechno2() {
 //given
 Song equador=new Song("Equador", 100);
 Song onaTanczyDlaMnie=new Song("Ona Tanczy Dla Mnie", 50);
 Song harlemShake=new Song("Harlem Shake", 150);

 //when
 List mixes = technoMixer.makeTechnoMix(equador,onaTanczyDlaMnie,harlemShake);
 
 //then
 assertThatSongsHaveBits(mixes,200,100,300);
 assertThatSongsHaveTitles(mixes,"Equador_techno_mix","Ona Tanczy Dla Mnie_techno_mix","Harlem Shake_techno_mix");
}


private void assertThatSongsHaveBits(List mixes, int... expectedBites) {
 for (int currentSongNumber = 0; currentSongNumber < expectedBites.length; currentSongNumber++) {
  Assert.assertTrue(mixes.get(currentSongNumber).getNumberOfBits()==expectedBites[currentSongNumber]);
 }
}

private void assertThatSongsHaveTitles(List mixes, String... expectedTitles) {
 for (int currentSongNumber = 0; currentSongNumber < expectedTitles.length; currentSongNumber++) {
  Assert.assertEquals(mixes.get(currentSongNumber).getTitle(),expectedTitles[currentSongNumber]);
 }
}

Test wygląda już czytelniej. Metody są bliźniaczo podobne ale nie będziemy ich jeszcze refaktorować. Na razie zastanówmy się nad kilkoma rzeczami :

  • Czy wywołanie może być jeszcze czytelniejsze
  • Czy można gdzieś przenieść metody pomocnicze aby nie zaśmiecały testu
  • Zakłądając, że w bardziej skomplikowanej domenie obiekt Song mógłby wystąpić w większej ilości testów - czy można zrobić jakoś tak aby metod "reużyć"?

Można!

Matcher - wersja pierwsza

 

@Test
public void shouldMixSongsToTechnoWithMatcher() {
 //given
 Song equador=new Song("Equador", 100);
 Song onaTanczyDlaMnie=new Song("Ona Tanczy Dla Mnie", 50);
 Song harlemShake=new Song("Harlem Shake", 150);

 //when
 List mixes = technoMixer.makeTechnoMix(equador,onaTanczyDlaMnie,harlemShake);
 
 //then
 SongMatcher.assertThat(mixes).haveBits(200,100,300).and().titles("Equador_techno_mix","Ona Tanczy Dla Mnie_techno_mix","Harlem Shake_techno_mix");
}

 

public class SongMatcher {

 private List songs;

 public SongMatcher(List songs) {
  this.songs = songs;

 }

 public static SongMatcher assertThat(List songs) {
  return new SongMatcher(songs);
 }

 public SongMatcher haveBits(int... expectedBits) {
  for (int currentSongNumber = 0; currentSongNumber < expectedBits.length; currentSongNumber++) {
   Assert.assertEquals(expectedBits[currentSongNumber],songs.get(currentSongNumber).getNumberOfBits());
  }
  return this;
 }

 public SongMatcher titles(String... expectedTitles) {
  for (int currentSongNumber = 0; currentSongNumber < expectedTitles.length; currentSongNumber++) {
   Assert.assertEquals(expectedTitles[currentSongNumber],songs.get(currentSongNumber).getTitle());
  }
  return this;
 }

 public SongMatcher and() {
  return this;
 }

}

Test wygląda już ok ale w matcherze mamy te jakże podobne do siebie metody. A Gdyby tak obiekt miał 10 własności? Czas na jakiś refaktoring - na tyle na ile znam refleksję to wyjdzie coś takiego....

Matcher - wersja druga

 

public class SongMatcher2 {
 private List songs;

 public SongMatcher2(List songs) {
  this.songs = songs;

 }

 public static SongMatcher2 assertThat(List songs) {
  return new SongMatcher2(songs);
 }

 public SongMatcher2 haveBits(Integer... expectedBits) {
  assertValueOnProperty("NumberOfBits",expectedBits);
  return this;
 }
 
 public SongMatcher2 titles(String... expectedTitles) {
  assertValueOnProperty("Title",expectedTitles);
  return this;
 }


 private void assertValueOnProperty(String fieldName, Object[] expectedBits) {
  Assert.assertNotNull(songs);
  Assert.assertFalse(songs.isEmpty());

  try {
   assertValueOnPropertyUnsafely(fieldName, expectedBits);
  } catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
   throw new RuntimeException(e);
  }
  
 }

 private void assertValueOnPropertyUnsafely(String fieldName,
   Object[] expectedBits) throws NoSuchMethodException,
   IllegalAccessException, InvocationTargetException {
  Method methodToInvoke = songs.get(0).getClass().getDeclaredMethod("get"+fieldName);
  for (int currentNumberOfValue = 0; currentNumberOfValue < expectedBits.length; currentNumberOfValue++) {
   Song currentSong = songs.get(currentNumberOfValue);
   Object invokeResult = methodToInvoke.invoke(currentSong,new Object[0]);
   Assert.assertEquals(expectedBits[currentNumberOfValue], invokeResult);
  }
 }

 

 public SongMatcher2 and() {
  return this;
 }
}

No i uważne oko zauważy, że wykształcił nam się taki matcher w matcherze. Zakładając, że obiektów w domenie będzie więcej to być może znowu jakoś tę logikę z refleksją da się gdzieś tam ładnie udostępnić. Możemy alb o delegować albo dziedziczyć. Niby książki mówią, żeby delegować ale tutaj z powodów nad którymi teraz nie chce mi się rozwodzić będziemy dziedziczyć.

Matcher - wersja trzecia

 

public class BaseMatcher {

 protected List objectsUnderTest;
 
 public BaseMatcher(List objectsUnderTest) {
  this.objectsUnderTest = objectsUnderTest;
 }

 protected void assertValueOnProperty(String fieldName, Object[] expectedValues) {
  Assert.assertNotNull(objectsUnderTest);
  Assert.assertFalse(objectsUnderTest.isEmpty());

  try {
   assertValueOnPropertyUnsafely(fieldName, expectedValues);
  } catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
   throw new RuntimeException(e);
  }
  
 }

 private void assertValueOnPropertyUnsafely(String fieldName,
   Object[] expectedBits) throws NoSuchMethodException,
   IllegalAccessException, InvocationTargetException {
  Method methodToInvoke = objectsUnderTest.get(0).getClass().getDeclaredMethod("get"+fieldName);
  for (int currentNumberOfValue = 0; currentNumberOfValue < expectedBits.length; currentNumberOfValue++) {
   Object currentObject = objectsUnderTest.get(currentNumberOfValue);
   Object invokeResult = methodToInvoke.invoke(currentObject,new Object[0]);
   Assert.assertEquals(expectedBits[currentNumberOfValue], invokeResult);
  }
 }

 

 public BaseMatcher and() {
  return this;
 }
}

 

public class SongMatcher3 extends BaseMatcher{

 public SongMatcher3(List songs) {
  super(songs);
 }

 public static SongMatcher3 assertThat(List songs) {
  return new SongMatcher3(songs);
 }

 public SongMatcher3 haveBits(Integer... expectedBits) {
  assertValueOnProperty("NumberOfBits",expectedBits);
  return this;
 }
 
 public SongMatcher3 titles(String... expectedTitles) {
  assertValueOnProperty("Title",expectedTitles);
  return this;
 }
}

Na razie możemy testować tylko listy ale to na potrzeby przykładu i jeszcze do tego wrócimy. Wygląda całkiem obiecująco ale pojawia się pewien problem otóż kompilator zgłasza błąd w teście :

 

@Test
public void shouldMixSongsToTechnoWithMatcher3() {
 //given
 Song equador=new Song("Equador", 100);
 Song onaTanczyDlaMnie=new Song("Ona Tanczy Dla Mnie", 50);
 Song harlemShake=new Song("Harlem Shake", 150);

 //when
 List mixes = technoMixer.makeTechnoMix(equador,onaTanczyDlaMnie,harlemShake);
 
 //then
 SongMatcher3.assertThat(mixes).haveBits(200,100,300).and();// O TUTAJ JEST BŁĄD .titles("Equador_techno_mix","Ona Tanczy Dla Mnie_techno_mix","Harlem Shake_techno_mix");
}

Dzieje się tak dlatego, że nasz matcher bazowy nic nie wie o metodach matchera domenowego. Ostatni szlif i ostatnia sztuczka własnie przed nami. Musimy jakoś tę wiedzę do nadklasy przekazać.

Matcher - wersja czwarta

 

public class BaseMatcher2 {

 private K self;
 
 protected List objectsUnderTest;
 
 public BaseMatcher2(List objectsUnderTest,Class selfClass) {
  this.objectsUnderTest = objectsUnderTest;
  this.self=selfClass.cast(this);
 }

 protected void assertValueOnProperty(String fieldName, Object[] expectedValues) {
  Assert.assertNotNull(objectsUnderTest);
  Assert.assertFalse(objectsUnderTest.isEmpty());

  try {
   assertValueOnPropertyUnsafely(fieldName, expectedValues);
  } catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
   throw new RuntimeException(e);
  }
  
 }

 private void assertValueOnPropertyUnsafely(String fieldName,
   Object[] expectedBits) throws NoSuchMethodException,
   IllegalAccessException, InvocationTargetException {
  Method methodToInvoke = objectsUnderTest.get(0).getClass().getDeclaredMethod("get"+fieldName);
  for (int currentNumberOfValue = 0; currentNumberOfValue < expectedBits.length; currentNumberOfValue++) {
   Object currentObject = objectsUnderTest.get(currentNumberOfValue);
   Object invokeResult = methodToInvoke.invoke(currentObject,new Object[0]);
   Assert.assertEquals(expectedBits[currentNumberOfValue], invokeResult);
  }
 }

 public K and() {
  return self;
 }
}

 

public class SongMatcher4 extends BaseMatcher2{

 public static SongMatcher4 assertThat(List songs) {
  return new SongMatcher4(songs);
 }

 public SongMatcher4(List objectsUnderTest) {
  super(objectsUnderTest, SongMatcher4.class); 
 }

 public SongMatcher4 haveBits(Integer... expectedBits) {
  assertValueOnProperty("NumberOfBits",expectedBits);
  return this;
 }
 
 public SongMatcher4 titles(String... expectedTitles) {
  assertValueOnProperty("Title",expectedTitles);
  return this;
 }
}

Teraz powinno być ok

Co dalej?

Jak już wcześniej zwróciliśmy uwagę powyższe matchery nadają się jedynie do testowania list. Można by klasę bazową zamienić w fabrykę wyspecjalizowanych matecherów co ma sens bo testowania czy ilość elementów jest większa od zera nie ma sensu dla pojedynczych obiektów. I coś takiego jest już gotowe, nazywa się FEST i gorąco polecam

Co Jeszcze?

Przy okazji pisania artykułu użyłem JUnit 4.11, który ma nowy ciekawy patent : @FixMethodOrder(MethodSorters.NAME_ASCENDING). Od czasu przesiadki na Javę 7 JUnit zaczął wywoływać metody w losowej kolejności co czasami psuje testy integracyjne bo tam np. testując flow transformacji plików dla zaoszczędzenia czasu kolejne testy korzystają ze stanu pozostawionego przez poprzednie fazy. A ta małą adnotacja działa jak do rany przyłóż.

wtorek, 21 maja 2013

GeeCON 2013 - rekonstrukcja zdarzeń

Programowanie Funkcyjne

Ten temat pojawił się trzeciego dnia, więc pamiętam go najlepiej bo jako odpowiedzialny kierowca byłem wtedy trzeźwy i wyspany. Generalnie do czasu tej konferencji programowanie funkcyjne kojarzyło mi się jedynie z przetwarzaniem danych pomiędzy zbiorami gdzieś tam wewnątrz aplikacji (tak jak na matematyce dyskretnej w klasie 1 studiów). Aż tu na jednej prezentacji pojawił się rysunek podobny do poniższego (odtworzone z pamięci) :

Pewnie dla wyjadaczy scali, klołżerów i innych takich ten temat jest oczywisty jak składanie pita37 ale dla mnie to był moment oświecenia, że jako funkcję można przedstawić dowolne "coś" co dostaje na wejściu "cokolwiek" i zwraca "coś innego". I teraz taki Wzorzec Dekorator wydaje się być hakiem na obiekcie gdzie czymś bardziej naturalnym byłoby wywołanie f(g(x)) albo F(f1(x),f2(x),...) - jeśli dobrze zrozumiałem F to funkcja wyższego rzędu a fn funkcje niższego rzędu - czyli tutaj to mogą być np poszczególne walidatory w trakcie jednej wielkiej walidacji.

Jest jeszcze jedna ciekawa a być może nawet ważniejsza korzyść jaką może przynieść programowanie funkcyjne. Generalnie we wszystkich książkach o OOP we wstępie jest napisane, że właśnie ten sposób jest naturalny dla ludzi i dlatego wszystko pójdzie gładko. A to gówno prawda bo ludzie myślą w sposób proceduralny przez co bliżej nam do programowania imperatywnego co ma swoje potwierdzenie w kodzie większości studentów - masa ponakładanych na siebie ifów,forów i innego świństwa zamkniętych w 8000 linijkową metodę.

A jeśli do kogoś to nie przemawia to niech pomyśli sobie co dzieje się w jego głowie gdy idzie pozmywać naczynia po obiedzie:

Czy coś takiego :

Mam do dyspozycji obiekt klasy Zlew, obiekt klasy Ludwik oraz kolekcję obiektów klasy NAczynie oraz obiekt klasy Człowiek (czyli JA)

 
zlew.umieść(naczynia)
ludwik.nalejDo(zlew)
kran.odkręć()
ja.zmyjNaczynia(zlew)


Czy Może naturalniej jednak będzie :

Jest do pozmyawania sterta naczyń
 
JA muszę wrzucić te wszystkie naczynia do zlewu
JA muszę teraz odkręcić wodę 
JA muszę teraz nalać ludwika
JA teraz dla każdego talerza
      muszę go umyć
      jeśli jest już czysty 
          to JA go odłożę na suszarkę
      jeśli nie
          to JA go myję dalej
      W międzyczasie zakręcę wodę
      Dobra zrobię sobie krótka przerwę
      (...)
      
No jeśli ktoś myśli o abstrakcjach i interfejsach w trakcie zmywania naczyń to jakieś dobre zioło namierzył albo po ASPięknych.

Generalnie zarówno programowanie obiektowe jak i funkcyjne nie jest naturalne dla naszego umysłu ALE. Klasy i obiekty to wynalazek greckich filozofów i chyba ktoś ich kalibru ma odpowiednie doświadczenie aby stworzyć poprawny model obiektowy. W przeciwieństwie do humanistycznej obiektówki programowanie funkcyjne opiera się na zrozumieniu ścisłych zasad matematyki. A enkapsulacja i re-używalność jest równie dobra.

?Nowe spojrzenie na dług techniczny

Temat długu technicznego, kompatybilności wstecz, dodawania nowych ficzerów itd pojawił się na kilka prezentacjach pierwszego i drugiego dnia. I w sumie tutaj tez jest mały mindfuck bo do tej pory TechDebt traktowałem jako coś bezpośrednio związanego z jakością kodu i architekturą systemu.

A tutaj pojawia się taka ciekawostka. Jeśli mamy np. takie wymagania :

  • Zrobić funkcjonalność 1
  • Zrobić funkcjonalność 2
  • Zrobić funkcjonalność 3


To one tak naprawdę brzmią

  • Zrobić funkcjonalność 1
  • Zrobić funkcjonalność 2 i nie popsuć funkcjonalności 1
  • Zrobić funkcjonalność 3 i nie popsuć funkcjonalności 1 i też nie popsuć funkcjonalności 2

No to teraz jeśli założymy, że mamy w naszym systemie N (dla uproszczenia) podobnych funkcjonalności, to koszt dodania funkcjonalności N+1 będzie:

  • Tam gdzie jakimś cudem osiągnęliśmy legendarny poziom modularyzacji - Cost~F(n+1)
  • Tam gdzie ficzery są od siebie zależne ale system jest gotowy na rozbudowę - Cost~F(n+1) + M(n) - gdzie M(n) koszt utrzymania przy życiu pozostałej części systemu
  • Tam gdzie jest skomplikowana interakcja (zamierzona lub nie) pomiędzy ficzerami - Cost~F(n+1)+M(n^2)
  • Tam gdzie gówno pływa swobodnie po kodzie, "big ball of mud" i wszystko jest powiązane ze wszystkim w 10000 linijkowych klasach nazwanych "Menażer" - Cost~F(n+1)+M(2^n)

Oczywiście poprzez refkatoringi, metodyki projektowe i inne takie można poprawić charakterystykę funkcji kosztu ALE - można też zmniejszyć pracochłonność zmniejszając N poprzez usunięcie częsci fukncjonalności. Niby szok kulturowy bo funkcjonalność - szczególnie ta działająca - zarabia dla nas pieniądze - no poza momentami kiedy ich nie zarabia!

Kiedyś czytałem w focusie artykuł o ludziach, którzy patologicznie kolekcjonują rzeczy przez co nie mogą się poruszać po swoim mieszkaniu. U mnie w komórce przez 10 lat też leżały jakieś deski bo a nuż kiedyś się przydadzą. Nikt ich nie używał to poszły na przemiał i podobnie trzeba wywalać jakieś dziwne opuszczone ficzery aplikacji, których nikt nie używa. Czyli nowy rodzaj długu technicznego : doskonale działąjąca funkcjonalność, której (praktycznie) nikt nie używa

Inne takie

Inna ciekawa rzecz to : http://code.google.com/p/spock/ w sumie fajna rzecz ale na Groovym . Po PHP trzymałem się z dala od języków dynamicznych ale trzeba się chyba przemów.

czwartek, 9 maja 2013

Jak się nie odkleić od rzeczywistości - lekarstwo na zarządzanie zasobami.

Odczuwam pierwsze objawy choroby, z którą zawsze walczyłem. Przez lata widziałem jak ludzie sprowadzani do roli pionka w projekcie zamieniają się w zombie pozbawione pasji. Wszędobylski palec wskazujący wsparty lśniącym gantem stanowił narzędzie wyroczni określającej byt pokornej jednostki ( w sensie - "ty Zdzichu tera robisz to i masz na to dwie godziny").

Odczuwam pierwsze objawy choroby, z którą zawsze walczyłem. Kiedy jesteś odpowiedzialny tylko za swoją pracę to dostaniesz wpi***** tylko za swoją pracę. Kiedy jesteś odpowiedzialny za pracę wielu to cios może przyjść z każdej strony. Umysł sam zaczyna się bronić szukając wszędzie potwierdzenia, że jesteśmy kryci - "czy to już jest gotowe" lub " na kiedy to będzie skończone" . Kiedy sam sobie piszesz kod doskonale wiesz na jakim etapie jesteś - kiedy masz dostać wpi**** za czyjś kod nagle zaczyna cię bardzo ciekawić na jakim etapie on jest - chyba dlatego ci ludzie zawsze tak bardzo wielbili raporty...

Odczuwam pierwsze objawy choroby, z którą zawsze walczyłem... i będę walczył dalej. Najpierw trzeba zrozumieć objawy, później opracować lekarstwo...

Objawy

Objawy są naturalne i występują u wszystkich znajdujących się w określonej sytuacji - no może prawie wszystkich. Aby to sobie przećwiczyć zerknijmy na obrazek powyżej, który pochodzi z gry kolonizacja. Jeśli ktoś o niej nie słyszał to idea w uproszczeniu wygląda tak - dopływamy do brzegu w 1492 roku, tworzymy miasto a w nim zbieramy surowce i produkujemy towary. Jeśli akurat z Europy wyemigruje specjalista to będziemy zbierać tych zasobów i produkować towarów więcej. Ale z drugiej strony jak już na przykład nazbieramy odpowiednio dużo rudy żelaza to przerzucamy górnika do pasania świń bo tak maksymalizujemy zyski miasta. Brzmi znajomo?

Widziałem to wiele razy w projektach bazujących na ganto-excelu - Dobra tego taska mamy zrobionego to przesuńmy risorsa do łatania layoutów, a później zbierzemy resztę risorsów do aktualizacji dokumentacji

Ba byłem nawet na studiach dla managerów i widziałem na własne oczy Ćwiczenia z MS Projecta polegające na "zrobieniu tak aby paski były zielone" . I ch** kogo obchodziło, że programista nazwany przez instrukcję ćwiczenia "Janem" będzie pracował przez 3 godziny and jednym taskiem, później przez godzinę nad innym i na koniec na godzinę wróci do pierwszego. Grunt, ze gantt będzie zielony.

No i pamiętam jak w jednej takiej firmie zawsze na koniec dnia manager robił obchód po sali i podbijał do każdego "raportuj, raportuj gdzie jesteś na gancie" - not kool , not f*** kool

Umysł dąży do uproszczenia modelu. A jeśli do tego psychika danego człowieka jest na etapie "jak najlepiej zaspokoić moje potrzeby" to mamy gotowy przepis na ganta-style-managera.

Co można z tym zrobić jeśli już sobie uświadomimy problem? Natura, mechanika czy elektronika już znalazły rozwiązanie. Co ma wspólnego układ hormonalny, lodówka i tranzystor z napięciem zasilania Baza-Kolektor?

Ujemne sprzężenie zwrotne!

Zaciśnij dłoń i uderz nią w ścianę - powinno zaboleć co powstrzyma cię przed kontynuowaniem tej czynności (no chyba, że bardzo to lubisz) - właśnie zostałeś poddany działaniu ujemnego sprzężenia zwrotnego. Jak tę koncepcję zastosować w IT? - proste - Jak już coś wymyśliłeś to musi cię to też zaboleć. Można to łatwo osiągnąć będąc cały czas blisko kodu. Ta koncepcja została ładnie opisana w tej książce ---> http://37signals.com/rework - wszystkie ręce na front! - jak już coś obiecałeś to też się przy tym namęczysz. A przy okazji będziesz wiedział co się dzieję w projekcie i nie będziesz ludzi męczył pytaniami (no dobra w praktyce na razie mi to tak w 90% przypadków wychodzi)

Dlatego cały czas intuicyjnie byłem przeciwny koncepcji team lidera, który biega sobie tylko na spotkania, naobiecuje i deleguje. Powiedziałem moim mocodawcom, że chcę być jak Mel Gibson w Patriocie (przełożeni z USA) z bagnetem na pierwszej linii frontu aniżeli jak jakiś generał siedzieć wygodnie w namiocie spokojnie przesuwający figury na mapie

No i akurat w tym tygodniu ktoś wrzucił taki fajny symboliczny rysunek na fejsa :

niedziela, 21 kwietnia 2013

Co nas motywuje i dlaczego korpo-targety nie działają

Lista tzw. motywatorów wewnętrznych :

Wolność

Dążenie do mistrzostwa

Poczucie celu

Mam nadzieję, że każdy kto codziennie kładzie ręce na klawiaturze będzie się identyfikował z tymi trzema wartościami. Nie jest to przypadkowy zbiór ale wynik poszukiwań, które przeprowadził autor książki --> Drive: The Surprising Truth About What Motivates Us. Znajdziecie tam masę przykładów potwierdzających ten wybór jakkolwiek jeśli komuś nie chce się do książki sięgać ( a pewnie nikomu teraz nie chce się tam sięgać) krótkie wsparcie dla tych dogmatów.

  • wolność/autonomia : generalnie my jako ludzie dążymy do sytuacji, w której mamy raczej większe pole manewru, większe możliwości wyboru aniżeli mniejsze - chyba standardowe zachowanie w świecie zwierząt. Ale jest jeszcze ważniejsza rzecz - posiadając autonomię pracujesz na swoje konto (poniekąd) i możesz łatwiej zwiększyć swoją wartość w grupie. Jest jednakże jedna sytuacja, w której z radością zrzekniesz się wolności wyboru - sytuacja w której nie masz umiejętności (w korpo-języku - kompetencji) aby poradzić sobie z daną sytuacją. Na przykład w sytuacji gdy na ulicy zaatakuje mnie dziki lew (a dlaczego nie) z chęcią zrobię dokładnie to co mi powie jakiś specjalista od "przeżycia spotkania z lwem" (tak tutaj właśnie działa zasada autorytetu tak ochoczo wykorzystywana przez twórców reklam z pastami do zębów). W tej sytuacji jestem zależny od pomocy z zewnątrz a nasz umysł nie lubi być zależny - ma naturalną motywację aby nie być zależnym, aby być wolnym - i naturalnie dochodzimy do kolejnego punktu.
  • dążenie do mistrzostwa : no to jak już się nauczę walki z lwami to nie dość, że łatwiej mi przetrwać bo nie jestem zależny od zewnętrznej siły to dodatkowo wśród znajomych jestem znany jako "koleś który zna się na walce z lwami" (a na nasze - "to jest koleś, który zna się na Javaskrypcie") - ego jest dopieszczone a wartość społeczna rośnie. No chyba, że nie za bardzo umiejętność walki z lwami jest do czegoś potrzebna i tutaj pojawia się kolejna potrzeba
  • poczucie celu : i tutaj wskazówka -" maksymalizacja zysków firmy" w kontekście motywacji wewnętrznej nie jest celem jako takim dla większości ludzi, którzy w tej firmie pracują. Korporacje starają sobie wymyślić jakieś wizje i inne takie - skuteczność tych technik pozostawię wam do oceny. Jako ludzie chcemy być częścią czegoś większego ale nie jakimś tam trybikiem ale realną siłą która to coś kształtuje. W jednym z artykułów o "Valve" (taka firma bez managerów, która zarabia miliony na produkcji gier i jest dobrym przykładem zastosowania wspomnianych tu wartości) przeczytałem, ze w holyłudzie przemysłowi udało się zabić wszelką kreatywność w zawodzie grafika i potworzyły się takie specjalizacje jak "specjalista od tworzenia nosów". Czy tworząc nos jednej z postaci we władcy pierścieni identyfikowałbyś się z celem tworzenia tego filmu i czuł przepływającą przez ciebie moc tworzenia?

I teraz pojawiają się korpotargety...

... i ogromne nieporozumienie pod tytułem "pieniądze zabijają wewnętrzną motywację". Coś zupełnie innego zabija tutaj wewnętrzną motywację.

Przykład pierwszy - mierzenie efektywnośc

Powyższy ciąg myślowy może prowadzić do najbardziej patologicznych targetów w IT - nagradzanie/karanie programistów w zależności od tego ile napisali linii kodu. Inne chore pomysły to np. mierzenie ilości klas czy czasu spędzonego przy komputerze. Znam jednego dziadka, który zaproponował następujący sposób mierzenia efektywności programisty : Zmierzyć Velocity zespołu w jednym sprincie, następnie usunięcie "badanego członka" i zmierzenie Velocity teamu bez tej osoby - to jest przerażające. Tam gdzieś są osoby, które naprawdę tak myślą - oni mogą zrobić wiele krzywdy!

A jeśli czyta to ktoś z poza IT, kto nie rozumie braku sensu w przedstawionych pozycjach to już tłumaczę - ocenianie pracy programisty przy pomocy ilość napisanych linii kodu ma tyle samo sensu co ocenianie efektywności sprzedawcy na podstawie tego ile słów wypowie w trakcie rozmowy z klientem.

Podsumujmy :
Wolność MistrzostwoPoczucie celu
Zabita - jesteś zmuszony, po prostu zmuszony pisać więcej kodu niż potrzeba. Zamiast "dobrego kodu" produkujesz "duży kod" Poniekąd zabite - bo zamiast szkolić się w tym co potrzebne szkolisz się w tym co niepotrzebne ale nagradzane Zabite - bo to kompletnie nie ma sensu.

No można by mierzyć ilość dostarczonej funkcjonalności ale :

  1. W waterfailu i tak to nie ma znaczenia bo już jakiś wana-be-manager zaplanował 13 miesięcy wcześniej ile funkcjonalności będzie dostarczone.
  2. A w edzajlu,a w edzajlu problem jest taki, że Hrowa teoria mówi : "oceniajmy indywidualnie" - ej stary zrobisz mi code review? - spadaj realizuję swoje indywidualne cele....

Teraz w kontekście frazy "pieniądze zabijają wewnętrzną motywację". Jeśli siedzę sobie przy klawiaturze i ktoś wylewa na mnie wiadro z gównem i płaci za to powiedzmy 15% moich rocznych zarobków to mimo wszystko czuje się zdemotywowany. Nie czuję się zdemotywowany tym, że ktoś mi płaci ale tym, że siedzę zanurzony w gównie i najważniejsze - nie ma takiej sumy przy której gówno nagle przestaje śmierdzieć. Tak samo nie zacznę czuć nagle wewnętrznej motywacji przy określonej sumie pieniędzy - zacznie się jednak dziać co dla badaczy było niesamowite - płacąc więcej będę jeszcze mniej efektywniejszy - ejjj helołłłł! - ja nie lubię siedzieć w gównie - jak szybciej uzbieram swoją kasę to po prostu przestanę siedzieć w gównie. Tak samo np. jak zapłacicie milion złotych programiście , który siedzi w Informatycznym łajnie to jego efektywność nagle spadnie bo koleś pójdzie do kuchni na kawę i będzie miał to wszystko gdzieś - kasa jest odłożona. No niesamowite.

Przykład drugi - mierzenie jakości

Tutaj może powstać cel - pokrycie kodu minimum 60% ( a w patologicznym wypadku 100%). To jest bardziej chytre i bardziej kontrowersyjne niż poprzedni przykłąd. A to dlatego, że wielu wyznawców niejakiego wujka Bobra na prawdę wierzy, że trzeba mieć pokrycie 100%. Pierwsza kwestia to nie pamiętam kiedy ostatni raz wygenerowany przez eclipse getter nie działał. A druga ważniejsza kwestia to coś co trzeba wyczuć - siedzisz przy klawiaturze i piszesz kod albo najpierw test a później kod - nieważne - ważne jest to, że w pewnym momencie przestajesz myśleć czy wszystkie przypadki testowe masz już ogarnięte zaczynasz myśleć czy to co napisałeś już ci wygeneruje odpowiednie pokrycie. Kolega z jednej firmy kiedyś wspominał, że mają nawet taką stronkę gdzie teamy się lansują ile który ma pokrycia. I jak ich pokrycie sprawia to dzwoni manager z zagranicy i mówi "chłopaki zróbcie coś z tym bo jesteśmy już na 3 miejscu" - no to chłopaki dorzucają kilka testów z pustymi asercjami...

I ponownie to nie pieniądze zabijają tutaj motywacje ale gówno sączące się ze szlauchu

Wolność MistrzostwoPoczucie celu
Zabita - nie piszemy testów dla testowania ale jesteśmy zmuszeni, powtarzam zmuszeni do pisania testów dla pokrycia. Zabite - jedyne czego się uczysz to kreatywne sposoby na zwiększenie pokrycia Zabite


To co jest dobre?

Jeszcze dokłądnie nei wiem ale słyszałem mądrego człowieka, który mówił - "disconnecting from revenue is very dangerous" ( czy coś podobnego - ale na pewno po angielsku)

Ps

A no i żeby nie być gołosłownym powtórzyłem przed chwilą po raz kolejny eksperyment naukowy - zmierzyłem poziom mojej motywacji wewnętrznej - później wyciągnąłem pieniądze z bankomatu - wykonałem kolejny pomiar --> pieniądze same w sobie nie zmniejszyły mojej motywacji wewnętrznej - ba nawet trochę czuję się bardziej takim czystym pasjonatem z krwi i kości. Od tej pory daję prawo każdemu pasjonatowi programowaniu do odczuwania motywacji z powodu zarobków.

Uzupełnieniem do dzisiejszego artykułu może być --> http://pawelwlodarski.blogspot.com/2013/04/czy-za-kreatywna-prace-trzeba-pacic.html