Tuesday, July 22, 2008

Jakieś to takie skomplikowane...

Ostatnio miałem trochę do czynienia z narzędziem PowerDesigner. Ogólnie rzecz ujmując jest to narzędzie do modelowania...
Próbując określić odpowiedzialność tego narzędzia doszedłem do wniosku, że jest to narzędzie, które wychodząc od procesu biznesowego, pozwala w zrozumiały dla wszystkich (od dołu aż do góry korporacyjnej hierarchii) sposób opisać to, co dzieje się w biznesie, wspomóc analizę tegoż oraz, jeśli zajdzie taka potrzeba, doprowadzić do zaprojektowania stosownych narzędzi informatycznych, począwszy od wstępnych wymagań, na szkielecie systemu z wykorzystaniem konkretnych technologii skończywszy, uff!

Po pierwsze: zgubiłem się w strukturze


Rzeczą, która od razu rzuciła mi się w oczy, to ogrom możliwości tego narzędzia. Użytkownik może stworzyć różnego rodzaju modele, diagramy i zależności między nimi. Jako (zaznaczam: początkującemu) użytkownikowi brakowało mi procesu, który wskazywałby kierunek prac. PowerDesigner nie wspiera żadnej metodyki, więc w zasadzie nie wiadomo co należy robić. I mimo, że można wszystko, to i tak nie wiadomo co...

Po drugie: "Na przekór czasom i ludziom wbrew..."


Projektanci narzędzia z pewnością mieli pomysł na jego używanie. Przynajmniej ja gorąco w to wierzę. Aczkolwiek odniosłem wrażenie, że Sybase próbuje forsować jakieś własne podejście do modelowania, za nic sobie mając przyzwyczajenia analityków. Owszem, PowerDesigner wspiera co tylko może wspierać, ale odpowiedzialności poszczególnych składowych nachodzą na siebie. Tego właśnie mi brakowało! - dokładnie zdefiniowanych odpowiedzialności poszczególnych modelów.

Skoro 95% tego, co można zrobić w PowerDesigner, da się zrealizować za pomocą UML, to po co mi narzędzie za $7000?

Trzeba uczciwie przyznać, że po głębszym przyjrzeniu się narzędzie szokuje możliwościami, lecz nie wychodzenie na przeciw przyzwyczajeniom użytkowników sprawia, że próbują oni używać programu na swój własnny sposób, niezgodny z pomysłem projektantów. Oczywiście powoduje to frustrację, a interfejs użytkownika czyni nieergonomicznym do granic możliwości.

Wiele narzędzi przekonało mnie, że próby generowania kodu "z automatu" kończą się źle. Nie inaczej i w tym przypadku. Totalna kaszana, choć wierzę, że intencje były dobre.

PowerDesigner sprawia wrażenia programu rozwijanego przez grupę fantastycznych, ale kompletnie oderwanych od rzeczywistości, programistów.

Po trzecie: Narzędzie do wszystkiego


Mówią, że "jeśli coś jest do wszystkiego, to jest do niczego". Odkryłem, że to bzdura i daleko idące uogólnienie. Takie, na przykład, koło, dźwignia albo
klin. Używane są wszędzie i do wszystkiego, ale nikt im nie zarzuca, że są nieprzydatne.

Otóż i esencja mojego odkrycia: jeśli coś jest wystarczająco proste, może być "do wszystkiego" albo inaczej złożoność narzędzia/koncepcji/rozwiązania jest odwrotnie proporcjonalna do zakresu jego stosowalności.

Gdyby zatem Sybase, zamiast gigantycznego all-in-one, wypuścił zestaw narzędzi o określonej specjalizacji, które dodatkowo świetnie ze sobą współpracują, to miałby u mnie lepsze noty:)
Dobrym przykładem jest pakiet MS Office. Pomiając indywidualne upodobania, mamy klarowną sytuację: Word - piszemy, Excel - liczmy, PowerPoint - prezentujemy, Outlook - organizujemy, Binder - i to jest genialne, spinamy wszystko razem ale tak, że narzędzie nie zatracają własnej indywidualności. Dla użytkownika jest w miarę jasne co ma zrobić, aby uzyskać określony efekt.

A teraz objadę sobie UMLa


Ilu diagramów UML najczęściej używasz? Ja 2 - klas i sekwencji. A ilu elementów z tych diagramów najczęściej korzystasz? Ja z co najwyżej 10. Duch Pareto nie śpi, co? UML, z początku fajny, ale w miarę rozrastania się i usztywniania zrobił się beee. To musi pęknąć, już pęka. Powstają mutacje w stylu Robustness Diagrams, które wybierają z UML tylko to, co jest naprawdę niezbędne do określonego celu.

Osobiście traktuję UML jako zbiór sugestii odnośnie modelowania Używam piktogramów, ale konkretne zasady traktuje raczej luźno. Jestem zdania, że jeśli zespół jednoznacznie rozumie notację, to jest to ok. Nawet jeśli jest to notacja nieformalna.

Więc czego bym tak naprawdę chciał?


Myślę, że jeśli chodzi o projekty (nie tylko IT) będziemy świadkami następujących przemian:
centralizacja na rzecz decentralizacji i współpracy
szansą na ogarnięcie coraz to bardziej złożonych projektów jest podzielenie ich na mniejsze kawałki; brzmi trywialnie...coraz trudniej centralnie sterować ogromnymi przedsięwzięciami programistycznymi, zatem należy zrezygnować (pozornego) kontrolowania sytuacji i bardziej polegać na współpracy niewielkich, wyspecjalizowanych zespołów programistycznych; zamiast na dokładne modele, należy postawić na zaufanie i na kompetentnych ludzi
ustrukturyzowanie na rzecz procesów, elementów składowych oraz płaskich relacji
w miarę rozrostu projektu, utrzymywanie odpowiedniej struktury staje się coraz bardziej pracochłonne; po pewnym czasie nadchodzi moment, w którym dbanie, aby wszystko odbywało się wg wytycznych jest kosztowniejsze niż dodawanie wartości biznesowej do produktu; strukturę mogą zastąpić procesy - łatwiejsze w modyfikacji i bardziej elastyczne oraz elementy składowe wraz z płaskimi relacjami pomiędzy nimi;

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, July 18, 2008

Antywzorce w pracy programistów: Wymyślanie koła na nowo

Zastanawiam się czy wymyślanie koła na nowo ma sens. Ktoś napisał, że jeśli ma być ono bardziej okrągłe niż dotychczasowe to tak. Trudno odmówić sensu temu stwierdzeniu. Ja rozumiem je jako udoskonalanie istniejących rzeczy. Proces udoskonalania jest oczywiście dobry i potrzebny. Jednak podczas programowania „wymyślanie koła na nowo” nabiera czasem nowego, złowieszczego charakteru.

Większość problemów, z którymi się spotkałeś została już rozwiązana. Większość bibliotek, które opracowujesz po nocach została już napisana. Większość algorytmów, które wymyślasz w przypływie twórczego geniuszu już została opublikowana. Dlaczego więc nie korzystasz z tego bogactwa wiedzy?Napisałem „większość”, uwzględniając fakt, że „są na niebie i ziemi rzeczy, o których nie śniło się filozofom".

Jeśli Twoje, z pasją tworzone, autorskie rozwiązania mają charakter edukacyjny albo zwyczajnie robisz to dla własnej satysfakcji – tym lepiej dla Ciebie. Nauczysz się czegoś ciekawego i będziesz dobrze się bawić. Lecz jeśli pracujesz i zależy Ci na wydajności – korzystaj z dorobku innych. Pamiętaj, że ludzie po prostu kochają dzielić się swoją wiedzą i doświadczeniem. Gdyby było inaczej, nie mielibyśmy w internecie żadnej grupy dyskusyjnej.

Kłopot w tym, że większość uczelni kształcących programistów zniechęca do korzystania zewnętrznych bibliotek. No, bo po co używać JGAP w programie skoro można napisać algorytm genetyczny samemu? W ciągu pięciu lat takiej metodyki nauczania studenci nabierają dziwnych nawyków nieoptymalnej pracy, a potem przyjmując studentów do pracy musimy ich tego oduczać...ech...

Friday, July 11, 2008

Antywzorce w pracy programistów: The error is out there



Coraz częściej dochodzę do wniosku, że sposób pracy programistów nie został jeszcze całkowicie zbadany. Lawina, którą zapoczątkowali Gang of Four zbiera coraz większe żniwo. Zaczęliśmy od wzorców projektowych w tworzeniu kodu, dalej już popłynęło wzorce w projektowaniu stron www, wzorce J2EE (w innych technologiach pewnie też), wzorce integracyjne, wzorce, wzorce, wzorce...

Nie sposób zauważyć, że choć odkrywanie wzorców projektowych przebiega bardzo ekspansywnie, to jednak ogranicza się on tylko do jednej płaszczyzny: umiejętności technicznych. Nietrudno odgadnąć przyczynę tego stanu rzeczy, wszak pracujemy w konkretnej technologii, za pomocą konkretnych narzędzi i biegłość w praktyce jest gwarantem naszej atrakcyjności jako profesjonalisty w zawodzie.

Pracując z programistami w trakcie szkoleń, jako zewnętrzny konsultant, czy też jako członek zespołu, zauważam, że doskonalenie naszych umiejętności i poszukiwanie wzorców powinno odbywać się w co najmniej dwóch płaszczyznach. Pierwsza – wspomniane wcześniej umiejętności techniczne – rozwija się bardzo ekspansywnie. Druga – umiejętności nietechniczne (mniejsza teraz o nazwę) – trzeba przyznać, że trochę kuleje. Nie tak dawno pisałem o tego rodzaju umiejętnościach w artykule Metaprogramy w tworzeniu oprogramowania. Od tamtej chwili rosła we mnie chęć poszukiwania dobrych praktyk programistycznych na nietechnicznym poziomie. Owa chęć nabiera kształtu w niniejszym artykule. Postanowiłem sobie tropić wzorce w pracy programistów. Ponieważ łatwiej mi najpierw zdefiniować antywzorzec, to od nich właśnie zacznę. Będę wyróżniał postawy i schematy działania, które w moim odczuciu negatywnie wpływają na pracę programisty i jeśli to możliwe będę poszukiwał remedium.

Kilka postów wcześniej pisałem o postawie roboczo nazwanej Job Security, którą z cała pewnością można można nazwać antywzorcem. Teraz czas na drugi, który pozwoliłem sobie nazwać: The error is out there.

Niełatwo zapanować na tym schematem postępowania. Gdy poszukujemy przyczyny wadliwego działania kodu, niemal zawsze przyjmowane jest milczące założenie, że przyczyną błędu jest wadliwie działający framework, lub błąd w bibliotece, lub błąd w systemie, lub błąd współpracownika, lub działanie sił wyższych. Po kilku godzinach poszukiwań okazuje się jednak, że przyczyną zamieszania była literówka. Oczywiście wszystkie z uprzednio wymienionych błędów mogą się zdarzyć, lecz najczęściej wina leży po naszej stronie. Z jakiś powodów programiści odczuwają opór przed zaakceptowaniem faktu, że to oni mogą być przyczyną swoich niepowodzeń. A spróbuj takiemu programiście powiedzieć, że to on jest przyczyną błędu! Zdarzyło mi się parę razy narobić sobie w ten sposób wrogów, więc teraz jestem ostrożny w tego typu komentarzach.
Jak zatem sobie radzić w tego typu sytuacjach? Pytanie trzeba rozbić na dwie części. Po pierwsze, jak radzić sobie jeśli komuś pomagam oraz jak radzić sobie jeśli problem dotyczy mnie samego.

W pierwszym przypadku jest dużo prościej. Tak to już jest, że szybciej dostrzegamy czyjeś błędy niż nasze własne. Jeśli chcę komuś pomóc to unikam mówienia wprost o jego błędach – to zwyczajnie nie działa. Sprawdzają się za to niedyrektywne metafory, np. „Mój kolega też miał podobny problem i...”. Buduję krótką historyjkę o „moim koledze”, w której przekazuję wskazówki odnośnie możliwych rozwiązań. Metoda sprawdza się w 90% przypadków. Pozostałe 10% albo wymaga bardziej wyrafinowanych metod albo trafiliśmy na nieuleczalny przypadek.

Sprawy stają się bardziej skomplikowane, gdy chodzi o nas samych. Kluczem do sukcesu w tej materii jest rozwijanie umiejętności introspekcji. Wyjściowym ćwiczeniem może być tu przyjęcie założenia, że „przyczyna wadliwego działania aplikacji leży po mojej stronie”. Zdaję sobie sprawę, że reguła nie sprawdza się w 100% przypadków, ale w większości tak. Przyjęcie tego wstępnego założenia znacząco oszczędza czas. Dopiero po upewnieniu się, za pomocą możliwie obiektywnych kryteriów (warto poprosić kogoś o przeanalizowanie problemu) można przystąpić do oskarżania bibliotek i frameworków o spowodowanie kłopotów.

Friday, July 4, 2008

Iluzja O/RM

Gdy po raz pierwszy dałem się uwieść wspaniałemu konceptowi O/RM, Hibernate był w wersji 2 z kawałkiem, a o JPA nawet nie słyszałem.
Idea jest naprawdę wspaniała: raz mapujesz pomiędzy tabelami a obiektami, ba (!) nawet framework stworzy schemat bazy danych za ciebie, a potem już tylko pracujesz z obiektami - programista obiektowy swoją drogą, bazodanowiec swoją. Prawdziwy raj!

We wspomnianej wersji Hibernate'a pałętało się sporo plików xml, ale wspomagając się xdocletem można było jakoś przeżyć. Weszły anotacje i miało być jeszcze prościej, wspanialej - no, słowem obiektowa orgia.

Moim zdaniem wcale nie jest tak kolorowo. Po pierwsze dlatego, że nie możliwe jest zmapowanie O/R raz na zawsze. Baza danych wciąż o sobie przypomina. A to wyskoczy jakiś constraint i trzeba pogrzebać w bazie w poszukiwaniu przyczyn (a potem okazuje się, że to jakiś błąd mapowania:]), a to model obiektowy ulega refaktoringowi albo rozbudowie i znów należy wracać do bazy danych - wciąż wracamy do bazy danych stojąc okrakiem pomiędzy obiektami i relacjami.

Kolejną kwestią jest to, że cała idea O/RM skłania do zaniedbywania bazy danych (myślimy - nie jest moją rzeczą zajmować się bazą danych, jestem programistą obiektowym...). W związku z czym baza jest projektowana niechlujne lub (o zgrozo!) pozwala się frameworkom na zarządzanie schematem. Prowadzi to tak wielkiej kaszany w bazie danych, jak wielka kaszana tylko może być: każdy obiekt ma swoją tabele, istnieje wiele tabel zawierających po kilka wierszy ("a, bo mnie tak wyszło z mapowania..."), tabele z jedną kolumną są na porządku dziennym.

Do czego to prowadzi? Ano, bywa, że O/RM mają problem z wydajnością i czasem korzystnie jest wspomóc się jakimiś procedurami składowanymi i widokami. W takim momencie okazuje się, że praca ze schematem zarządzanym przez O/R M to gorzej niż czyściec dla duszy programisty.

W pewnym momencie rozwoju J(2)EE było paru ludzi (żeby nie wymieniać nazwisk), którzy pchnęli dalszy rozwój technologii w określonym kierunku. A społeczność za tym poszła, bo wizja była fajna. Aktualnie mam na myśli idę O/RM i całą resztę kryjąca się za wzorcem Domain Store. A przecież istnieją rozwiązania, które społeczności Javy zostały zignorowane, a które świetnie się sprawdzają w innych technologiach. Mówię rozwiązaniach, które warstwę danych organizują w okół bazy danych, a nie w okół modelu obiektowego, np: Oracle BC4J albo implementacja Active Record w Ruby on Rails

O faworyzowaniu pewnych rozwiązań świadczy choćby fakt, że do tej pory nie doczekaliśmy się popularnej i w pełni funkcjonalnej implementacji Active Record dla Javy. Myślę, że mogło by to wnieść wiele dobrego do aplikacji, które rozwijamy.