DWORZEC GŁÓWNY POCZEKALNIA KONDUKTOR BOCZNICA MASZYNOWNIA ROZJAZD Maszynista new Year(2010).happy = Happiness.HIGH; Jak przystało na zakończenie roku, parę słów co nam się udało osiągnąć w zeszłym roku i co planujemy na kolejny. A więc witajcie po raz szósty. Tak, sporo tego się nazbierało. W sumie to prawie 300 stron wiadomości o Javie. I to wszystko dzięki współpracy kilkunastu osób, Waszemu zaangażowaniu i wsparciu sponsorów. A od numeru czwartego zaczęliśmy tłumaczyć Java exPress na język angielski. Sporo pracy, ale satysfakcja większa. Teraz każdy może sięgnąć po Java exPress. Java exPress był obecny na prawie każdej większej imprezie Javowej w Polsce, byliśmy patronem Devoxx i Jazoon, udało nam się zorganizować 3 edycje COOLuarów. Rozpoczęliśmy konkurs Java Geek. A w przyszłym roku? Dalej będziemy publikować Java exPress co 3 miesiące. Także wersja angielska zacznie się ukazywać regularnie. Podobnie z Java Geek i COOLuarami - planujemy w 2010 roku zorganizować 2 edycje. Jedną w Krakowie, a drugą... sam nie wiem gdzie. Może tym razem Wrocław, a może Warszawa? ;) Pojawią się także nowe inicjatywy, jak na przykład szkolenia dla każdego, czy screencasty. O ile starczy sił i ochotników do pomocy, współpracy i sponsoringu ;) Na koniec jak zwykle apel. Jeśli chcesz napisać artykuł, pomóc w tłumaczeniach lub tworzeniu stron www, napisz do nas na [email protected]. A zupełnie na koniec: Wszystkiego dobrego w 2010 roku. Niech Java będzie z Wami ;) Do zobaczenia w 2010 roku, Grzegorz Duda Rozkład jazdy Redaktor: Grzegorz Duda Korekta: Agnieszka Wadowska-Duda Strony www i teksty html: Marek Podsiadły Grafika: Jakub Sosiński new Year(2010).happy = Happiness.HIGH; 2 Na horyzoncie... 3 Log4j - czyli jak skutecznie tworzyć logi w aplikacjach javovych 4 GORM – Grails Object Relational Mapping 10 Behaviour-driven development z easyb 20 Signal Framework dla platformy Java ME 26 Gradle - mocarne narzędzie do budowy projektów 33 Express Killers, cz. V 45 Express Killers, cz. V - odpowiedzi 46 Recenzja: GroovyMag 11/2009 47 Mistrz Programowania: Refaktoryzacja, cz. II 48 2 ROZJAZD Megafon W wersji 9.0 zostało dodane wsparcie dla Androida, OSGi, Google ApEngine czy JavaEE 6. Wiele rzeczy zostało poprawionych. Zresztą zerknijcie sami (http://www.jetbrains.com/idea/whatsnew/index.html). Konferencje w 2010 Rok 2010 zapowiada się bardzo ciekawie od strony konferencyjnej. Dużo i różnorodnie - tak można określić scenę konferencji javowych w Polsce. 3 W lecie przyjdzie nam odwiedzić Javarsovię (http:// javarsovia.pl/). Dokładna data oraz formuła nie są jeszcze znane, ale można liczyć, że koniec czerwca lub początek lipca będzie się działo w Warszawie. To także konferencja organizowana przez społeczność. Tym razem z Warszawa JUG. Na koniec roku zostaje już klasyczne JDD (http://jdd. org.pl/). Jak zwykle w Krakowie, jak zwykle w październiku, jak zwykle doskonale. Jeśli do tego dodamy 2 edycje Nie-konferencji COOLuarów, może Warsjawę, może Java4poeple, kilka Eclipse DemoCampów oraz inncy mniejszych spotkań, to rok 2010 wygląda bardzo ciekawie. Nie wspoPierwszą konfe- minając o cyklicznych spotkaniach licznych rencją w nowym w naszym kraju JUGów. roku będzie 4Developers (htt- Konkurs Java Geek p://2010.4develoJeśli konferencje, to nie pers.org.pl/), która może zabraknąć konkurodbywać się bęsu Java Geek. Wystardzie tym razem w towaliśmy na COOLuPoznaniu, 26 marca. To co ją wyróżnia spoarach, a później JDD z śród innych konferencji, to 4 ścieżki: Java, konkursem na zbieranie .Net, zarządzanie projektami oraz PHP. pieczątek. Kolejne punkty do tego całorocznego konkursu można Kolejną konferenbędzie zdobyć na 4Developers. cją będzie GeeCON, który w dniach Koniec konkursu będzie we wrześniu 2010 13-14 maja zawita roku, a więc jeszcze sporo czasu na zdobydo... Poznania. No właśnie - w tym roku cie wielu punktów. A dla najbardziej akPoznań rządzi konferencyjnie. A GeeCON tywnych przewidziane są ciekawe nagrody nikomu nie trzeba chyba przedstawiać. JeJavowe. Szczególy na http://www.dworld. dyna w Polsce 2 dniowa konferencja, orgapl/page/show/Java_Geek KONDUKTOR Długo na to czekałem i w końcu jest. Wczoraj została wypuszczona 9 wersja znakomitego IDE IntelliJ IDEA. Co tu się długo rozpisywać. Ściągać i używać. Szczególnie, że oprócz płatnej wersji Ultimate, jest także darmowa dostępna wersja Communiy Edition. Porównanie funkcjonalności znajdziecie na stronie http://www.jetbrains.com/ idea/features/index.html. nizowana przez społeczność JUGową. POCZEKALNIA IntelliJ IDEA 9.0 DWORZEC GŁÓWNY Grzegorz Duda BOCZNICA MASZYNOWNIA Na horyzoncie... ROZJAZD MASZYNOWNIA BOCZNICA KONDUKTOR POCZEKALNIA DWORZEC GŁÓWNY Poczekalnia Log4j - czyli jak skutecznie tworzyć logi w aplikacjach javovych Michał Szynkaruk W życiu każdego programisty JAVA prędzej czy później pojawia się etap, w którym dochodzi on do wniosku, że korzystanie z „System.out.print” w przypadku chęci sprawdzenia wartości danych zmiennych, stanu w którym obecnie znajduje się aplikacja itp. nie jest ani efektywne, ani efektowne. Oczywiście można powiedzieć, że korzystanie ze standardowego wyjścia na konsole jest łatwe i z pozoru szybkie. No tak, ale co w przypadku jeśli chcemy, by zamiast na konsole wynik był zapisywany do pliku? Wówczas trzeba wszystkie te elementy zastąpić odpowiednio innym kodem. Świetnie, ale jeśli chcemy aby wyniki były zapisywane już nie tylko do pliku, ale ponownie na konsole? Uff … no wymaga to wiele niepotrzebnej pracy. Czy można sobie ułatwić życie? Odpowiedź na to pytanie jest prosta: Log4j! Jest to wieloplatformowa biblioteka napisana w całości w Javie, której autorem jest Ceki Gülcü, a jej początki sięgają jeszcze ubiegłego wieku. Bez obawy, chodzi o lata 90-te :) Umożliwia ona zapisywanie logów w aplikacjach Javovych. Obecnie wspierana jest przez fundację Apache i wchodzi w skład projektu Jakarta. Wyróżnia się trzy wersje: 1.2 która jest uznawana za tę stabilną, 1.3 która została z pewnych względów porzucona oraz 2.0 która jest, nazwijmy to, eksperymentalna. Co może być zapisywane w logach? A w jaki sposób? To już zależy od naszej wyobraźni. Ale od początku. W log4j wyróżniamy trzy typy obiektów: • Logger’y • Appender’y • Layout’y Loggery posiadają metody, które tworzą logi i ustawiają im odpowiedni priorytet. Miejsca, do których mogą trafić logi są definiowane za pomocą Appender’ów. O tym jaką postać mają mieć komunikaty decydują obiekty typu Layout. Wyróżniamy następujące Logger’y: • NOPLogger • RootCategory • RootLogger Najważniejszy jest RootLogger i to właśnie z niego będę korzystać w dalej omówionych przykładach. Pozostałe możemy pominąć. Zwłaszcza, że RootCategory uznawany jest za przestarzały. Jeśli chodzi o ustawienie priorytetów, to wyróżniamy następujące poziomy: • FATAL: Poważne błędy powodujące przedwczesne zakończenie działania Przede wszystkim szeroko rozumiany przeaplikacji pływ aplikacji. Możemy zapisywać jakie są aktualne wartości poszczególnych zmien• ERROR: Błędy wykonania nych i obiektów, a także wypisywać komunikaty o różnego typu błędach bądź infor- • WARN: Przy użyciu przestarzałych komponentów, w sytuacjach niespomować o zdarzeniach biznesowych majądziewanych nie wpływających na wacych miejsce w trakcie działania aplikacji. dliwe działanie 4 • TRACE: Bardziej szczegółowe info PatternLayout umożliwia określenie szablonu wpisu • ALL/OFF: Wszystkie poziomy / wyłączenie logów SimpleLayout powoduje, że logi są wpisywane w postaci: poziom – wiadomość Przy wyborze danego poziomu musimy wziąć pod uwagę to, że wszystkie logi wy- XMLLayout formuje wpis do formatu pisywane na poziomach niższych od tego xml’owego który wybraliśmy nie będą wyświetlane. Przykładowo, jeśli poziom ustawimy na DateLayout ułatwia nam zarządzanie czaDEBUG (domyślny) to wówczas wszystkie sem umieszczanym we wpisie. logi wypisywane na poziomie TRACE nie Z własnego doświadczenia oraz wszelkich ujrzą światła dziennego. rozmów użytkowników opisywanej biblioJeśli chodzi o różnego rodzaju Appendery, to jest ich dużo zdefiniowanych, choć nikt nam nie zabrania by tworzyć własne w oparciu o klasę AppenderSkeleton. Wymienię najciekawsze z nich : teki wynika, iż najbardziej popularnymi są PatternLayout i HTMLLayout. Jeśli zdecydujemy się na wykorzystanie PatternLayoutu, to wówczas możemy określić w jaki sposób ma wyglądać dany ConsoleAppender wypisuje logi na konso- wpis. Jak to zrobić ? Określamy szablon, w lę, domyślnie działa jak System.out. , którym stosujemy poniższe elementy: ale może także jako System.Err. %% - pojedynczy znak % WriterAppender zapisuje logi do pliku ko- %c - kategoria zdarzenia rzystając z klasy Writer bądź OutputStream. %C - nazwa klasy, z której został wysłany wpis JDBCAppender umożliwia zapis logów do bazy danych. %d - data zdarzenia LF5Appender zapisuje logi do konsoli %m - treść komunikatu opartej na Swingu. %n - separator linii SMTPAppender pozwala wysłać logi na emaila, głównie dotyczące poważnych błę- %p - priorytet zdarzenia dów w działaniu aplikacji. %r - liczba milisekund, które upłynęły od DailyRollingFileAppender rotuje logi do momentu uruchomienia aplikacji określonego rozmiaru, umożliwiając przy %t - nazwa wątku, z którego został wysłatym zapis z podziałem na lata, miesiące, ny wpis dni itd. 5 BOCZNICA KONDUKTOR Mając już wybrany Appender który wie gdzie zapisywać należy ustalić co będzie• DEBUG: Szczegółowe info. dotyczące my zapisywać, a więc czas na Layouty : przepływu w działaniu aplikacji, w celach diagnostycznych HTMLLayout produkuje tabele HTML POCZEKALNIA • INFO: W celu śledzenia wykonania DWORZEC GŁÓWNY Z własnego doświadczenia wynika, iż najbardziej popularnymi są PatternLayout i HTMLLayout. MASZYNOWNIA ROZJAZD Poczekalnia ROZJAZD Poczekalnia DWORZEC GŁÓWNY POCZEKALNIA KONDUKTOR BOCZNICA MASZYNOWNIA BasicConfiguratora używamy jeśli na szybko chcemy skonfigurować tworzenie log’ów. %x - kontekst diagnostyczny powiązany z metody : configure() bezargumentowy i z wątkiem jednym argumentem ( Appender ) oraz resetConfiguration(). %M - nazwa metody, z której został wysłany wpis Konfiguracja za pomocą pliku właściwości jest podobna do konfiguracji przy wy%L - numer linii, z której pochodzi zdarze- korzystaniu pliku XML, lecz składnia jest nie inna. Oto przykładowy kod wywołany ze %F - nazwa pliku, z którego pochodzi zda- statycznej metody main: rzenie %l – lokalizacja, z której został wysłany wpis BasicConfigurator.configure(); Logger logger = Logger. getRootLogger(); logger.debug(„Hello world”); Zaleca się ograniczenie czterech ostatnich elementów ze względu na spowalnianie Wynikiem jest: aplikacji. 2 [main] DEBUG root - Hello world Pozostała nam jeszcze drobna rzecz jaką jest konfiguracja. Możemy tego dokonać Jest to zrozumiałe gdyż korzystamy z domyślnego szablonu o którym nie tak dawna trzy sposoby : no wspomniałem. 1. przy użyciu obiektu typu BasicConfiguraJeśli chcemy zdefiniować własny szablon, tor to możemy to zrobić przykładowo: 2. przy zastosowaniu pliku właściwości 3. przy wykorzystaniu pliku XML BasicConfiguratora używamy jeśli na szybko chcemy skonfigurować tworzenie log’ów. Layout lay1 = new PatternLayout(„[%p] %c - %m - Data wpisu: %d %n”); Appender app1 = new ConsoleAppender(lay1); BasicConfigurator.configure(app1); Logger logger = Logger.getRootLogger(); logger.debug(„Hello world”); Jest to najmniej skomplikowany sposób konfiguracji. Powoduje utworzenie obiektu PatternLayout, którego ConversionPat- Wynik jaki otrzymałem: tern ma wartość: %-4r [%t] %-5p %c %x - %m%n [DEBUG] root - Hello world - Data wpisu: 2009-11-24 15:20:29,049 Klasa BasicConfigurator posiada tylko trzy Dlaczego tak się dzieje ? To proste: dekla- 6 Nie ma problemu, dodajemy na końcu przykładowo: logger.setLevel(Level.TRACE); A jeśli chcemy, aby logi były wypisywane do pliku ? Nie ma problemu, linijkę Appender app1 = new ConsoleAppender(lay1); możemy przykładowo zastąpić w poniższy sposób: Appender app1 = null; try { app1 = new FileAppender( lay1,”C:/log_express.txt”); } catch(IOException ex) { ... } Konfiguracja w obu przypadkach powoduje takie samo działanie. Różnica jest tylko No dobrze, wspomniałem o tym, że można w sposobie zapisu, a decyzja z jakich medokonać konfiguracji także przy użyciu pli- tod będziecie korzystać zależy od was. ku properties oraz w formacie xml. Zasada działania jest podobna: umieszczamy dany Warto wspomnieć, że jeśli nie dokonamy plik o nazwie log4j.properties lub log4j. konfiguracji Log4j pojawi się stosowne xml w dowolnym pakiecie (jeśli będzie to ostrzeżenie, a logi nie będą wpisywane. domyślny pakiet, wówczas nie będziemy musieli bawić się w określanie gdzie system ma szukać pliku konfiguracji, gdyż od- Jak skorzystać z Log4j w Netbeans? Oto przykładowy sposób: będzie się to automatycznie). Przykładowa zawartość pliku właściwości : • pobieramy archiwum ZIP ze strony: http://apache.privatejetscharter. log4j.appender.C=org.apache. net/logging /log4j/1.2.15/apachelog4j.ConsoleAppender log4j-1.2.15.zip log4j.appender.C.layout= org.apache.log4j.PatternLayout log4j.appender.C.layout. 7 • wyodrębniamy plik log4j-1.2.15.jar BOCZNICA KONDUKTOR A co jeśli chcemy zmienić poziom wypisywania logów ? encoding=”UTF-8” ?> <!DOCTYPE log4j:configuration SYSTEM „log4j.dtd”> <log4j:configuration xmlns:log4j= ”http://jakarta.apache.org/ log4j/”> <appender name=”console” class=”org.apache.log4j.ConsoleAppender”> <param name=”Target” value=”System.out”/> <layout class=”org.apache. log4j.PatternLayout”> <param name=”ConversionPattern” value=”[%p] %c - %m - Data wpisu: %d %n”/> </layout> </appender> <root> <priority value =”debug” /> <appender-ref ref=”console” /> </root> </log4j:configuration> POCZEKALNIA ConversionPattern=[%p] %c - %m rujemy szablon o danej postaci oraz apData wpisu: %d %n pender który ma za zadanie wypisać logi log4j.rootLogger=DEBUG, C na konsole. Następnie umieszczamy apPrzykładowa zawartość pliku xml: pender jako argument funkcji configure i to wszystko. <?xml version=”1.0” DWORZEC GŁÓWNY Decyzja z jakich metod będziecie korzystać zależy od was. MASZYNOWNIA ROZJAZD Poczekalnia ROZJAZD Poczekalnia DWORZEC GŁÓWNY POCZEKALNIA KONDUKTOR BOCZNICA MASZYNOWNIA Tworzenie logów w inteligentny sposób znacząco wspomaga tworzenie oprogramowania. • tworzymy nową bibliotekę przez wy- Poza wyraźnymi plusami wspólną wadą bór zakładek Tools → Library może być to, że przy bardzo dużych logach można mieć problemy z dostrzeżeniem • dodajemy bibliotekę do projektu tych najistotniejszych dla nas informacji. • korzystamy z odpowiednich „impor- Na świecie to zjawisko jest określane mianem scrolling-blindness. tów”, np. import org.apache.log4j.*; Log4j versus Java Logging API Przy pisaniu wielu programów z wykorzystaniem Log4j spotkałem się z opiniami w stylu: „Przecież istnieje już coś takiego jak Java Logging API, która jest wspierana przez SUN Microsystems.” Zgadza się. Nie oznacza to jednak, że jeśli coś jest standardem to znaczy, że jest lepsze, wręcz przeciwnie. Java Logging API powstała później niż Log4j i w dużej mierze się na niej opiera. Podsumowanie Możliwości jakie posiada biblioteka Log4j są ogromne. Jest ona łatwa w użyciu i co ważne – na otwartym kodzie. Była optymalizowana pod względem szybkości działania. Nie ma także obaw o to jak będzie funkcjonować w aplikacjach wielowątkowych. Tworzenie logów w inteligentny sposób znacząco wspomaga tworzenie oprogramowania. W następnym numerze umieszczę kilka przykładowych kodów źródłowych pokazujących jak efektownie i efektywnie korzystać z Log4j w różnego typu aplikacjach. Pamiętajcie: nigdy więcej System.out.print :) Choć różnic jest niewiele to wśród pro- O autorze gramistów Javy otarła się opinia, że to co możemy wykonać za pomocą Java Logging API jest także możliwe przy wykorzystaniu Log4j, a nawet więcej. Plusy i minusy obu bibliotek zebrałem w poniższej tabelce. Log4j + + Michał jest studentem IV roku informatyki na Wydziale Informatyki i Zarządzania Politechniki Wrocławskiej. Jest certyfikowanym programistą Javy (SCJP 6.0). Java Logging API Wspierany przez Apache Wspierane przez SUN Dłużej na rynku Nie trzeba dodawać nowych bibliotek Używany przez ogromną społeczność - - Wybierane przez mniejszą liczbę programistów Rzadko aktualizowany 8 ROZJAZD MASZYNOWNIA BOCZNICA KONDUKTOR POCZEKALNIA DWORZEC GŁÓWNY Dworzec główny GORM – Grails Object Relational Mapping Mateusz Mrozewski W tym artykule chciałbym przedstawić podstawowe informacje na temat GORM (Grails Object Relational Mapping) oraz kilka bardziej zaawansowanych zagadnień z nim związanych. Po przeczytaniu artykułu dowiesz się jak działa warstwa persystencji w Grails. Rozpoczynamy GORM, warstwa persystencji w Grails zrealizowana jest jako mapowanie obiektowo-relacyjne. Pod maską GORM znajdziemy Hibernate. Możemy z niego również skorzystać bezpośrednio. Mapowanie OR jest zrealizowane w Grails na tzw. klasach domenowych (Domain class). Klasa domenowa zwana jest w innych kręgach encją. Klasa domenowa utworzona (z linii poleceń lub wyklikana w NetBeans) w najprostszym przypadku jest po prostu pustą klasą, wewnątrz której definiujemy atrybuty, które chcemy przechowywać, np. class Person { String firstname String lastname } Gdy utworzymy taką klasę domenową i wystartujemy nasz projekt (z domyślnymi ustawieniami) w NetBeans, zostanie uruchomiona baza Hypersonic i zostanie utworzona nowa tabelka Person. Będzie posiadała 4 kolumny: firstname, lastname, id i version. Grails domyślnie zajmuje się za nas generowaniem identyfikatorów (w najbardziej odpowiedni dla danej bazy sposób) oraz wersjonowaniem. Podstawowe operacje (CRUD) wykonujemy w następujący sposób: //Utworzenie nowego rekordu CREATE def p1 = new Person( firstname:”Mateusz”, lastname:”Mrozewski”); p1.save() // Odczytanie rekordu READ // tu korzystamy z automatycznie // wygenerowanego id def p2 = Person.get(1) // Aktualizacja UPDATE // modyfikujemy rekord // odczytany wcześniej p2.firstname = „Krzysztof” p2.save(); // Usuwanie DELETE // usuwamy odczytany wcześniej // rekord p2.delete() Jak widać wykonanie podstawowych operacji jest niezwykle łatwe. Asocjacje Z GORM robi się zawsze ciekawiej gdy dochodzi do asocjacji. Jednak jest to również rozwiązane w sposób czysty i prosty. Przykłady różnych relacji poniżej: // // // // // // One-to-one unidirectional jeden do jednego, jednokierunkowa mając referencję do samochodu, możemy pobrać jego silnik, ale nie odwrotnie Class Car { Engine engine } Class Engine {} // One-to-one bidirectional // jeden do jednego, // dwukierunkowa // Mając referencję do samochodu, // możemy pobrać jego silnik, // odwrotnie również Class Car { Engine engine } 10 KONDUKTOR Różnica względem poprzednich przykładów jest taka, że klucz obcy zostanie umieszczony w tabeli engine, a nie w tabeli car. Relację o krotności większej od jeden tworzymy poprzez wykorzystanie właściwo// One-to-one unidirectional - jeści hasMany. Stosując ją po obu stronach den do jednego, jednokierunkowa // mając referencję do samochodu, relacji otrzymamy relację wiele do wielu, a tylko po jednej stronie relację jeden do możemy pobrać jego silnik, // ale nie odwrotnie wielu. Tak jak w przypadku relacji jeden do Class Car { jednego, aby wymusić kaskadowanie nale Engine engine ży zastosować właściwość belongsTo. Na } przykładzie: Class Engine { static belongsTo = Car } // One-to-one bidirectional - jeden do jednego, dwukierunkowa // Mając referencję do samochodu, możemy pobrać jego silnik, // odwrotnie również Class Car { Engine engine } Class Engine { static belongsTo = [car:Car] } // One-to-many unidirectional jeden do wielu, jednokierunkowa class Car { static hasMany = [wheels:Wheel] } class Wheel {} W związku z relacją jeden do wielu należy pamiętać o kilku ważnych sprawach: • Grails do zmapowania tej relacji w bazie wykorzysta dodatkową tabelę złączeniową. Można to zachowanie nadpisać i wykorzystać klucze obce. Jak widać kaskadowanie realizujemy po- • Grails pozwala na wykorzystanie dwóch przez właściwość belongsTo. Warto zwróstrategii pobierania: lazy i eager. Docić uwagę na różnicę w deklaracji tej włamyślnie stosowane jest lazy, co w przyściwości przy relacji jedno i dwukierunkopadku relacji o krotności większej niż wej. jeden może prowadzić do problemów wydajnościowych (każdy kolejny reOd wersji Grails 1.2-M4 mamy jeszcze jedkord należąc do relacji jest pobierany ną możliwość deklarowania relacji jeden oddzielnym zapytaniem w miarę jak 11 ROZJAZD class Car { static hasOne = [engine:Engine] } class Engine { Car car } POCZEKALNIA W przypadku obu relacji powyżej operacje nie są kaskadowane. To oznacza, że jeśli skasujemy samochód, to jego silnik pozostanie (usuniemy z bazy, nie mówię o wypadkach). Podobnie ma się zapis zmian, utworzenie nowego rekordu, aktualizacja. Musimy to zrobić ręcznie dla obu klas. Wygodniej było by mieć kaskadowanie. Przykład z kaskadowaniem operacji wygląda tak: do jednego: DWORZEC GŁÓWNY Class Engine { Car car } MASZYNOWNIA Klasa domenowa zwana jest w innych kręgach encją. BOCZNICA Dworzec główny DWORZEC GŁÓWNY POCZEKALNIA KONDUKTOR BOCZNICA MASZYNOWNIA ROZJAZD Dworzec główny Grails domyślnie stosuje kolekcję Set w przypadku relacji o krotności większej niż jeden. iterujemy po naszej kolekcji). np.: • Domyślnie kaskadowane są operacje class Osoba { String imie INSERT i UPDATE, ale do kaskadowania String nazwisko operacji DELETE musimy zastosować } właściwość belongsTo. class Pracownik extends Osoba { W przypadku relacji wiele do wielu stoString nip sujemy właściwość hasMany po obu stro- } nach relacji: Strategia dziedziczenia określa nam, jak taka struktura zostanie przedstawiona w bazie danych. Dopóki nie wprowadziliśmy dziedziczenia, każda klasa domenowa miała swoją tabelkę. W tym przypadku, jeśli zastosujemy strategię table-per-hierar chy (domyślne) wszystkie klasy z hierarchii będą umieszczone w tej samej tabeli. TaRównież w tym przypadku możemy zasto- belka będzie wyglądała następująco: sować właściwość belongsTo. Dokumentacja wspomina, że scaffolding nie wspiera CREATE TABLE `osoba` ( `id` bigint(20) NOT NULL auto_ jeszcze tego typu relacji. class Book { static hasMany = [authors:Author] } class Author { static hasMany = [books:Book] } increment, `version` bigint(20) NOT NULL, Grails domyślnie stosuje kolekcję Set w `imie` varchar(255) NOT NULL, przypadku relacji o krotności większej niż `nazwisko` varchar(255) NOT NULL, jeden. Domyślne zachowanie możemy `class` varchar(255) NOT NULL, nadpisać stosując taką kolekcję, jaka nam `nip` varchar(255) NULL, PRIsię podoba, z więc dowolną implementa- MARY KEY (`id`) ); cję Set, List lub Map. Strategie dziedziczenia Grails wspiera dwie strategie dziedziczenia: table-per-hierarchy oraz table-persubclass. Taka jest przynajmniej informacja w oficjalnej dokumentacji. Jak wiadomo pod maską Grails znajdziemy Hibernate’a, a ten wspiera również strategię tableper-concrete-class, więc pewnie i w Grails dałoby się to jakoś wykorzystać. Czym tak naprawdę są strategie dziedziczenia? Nazwa brzmi dumnie, ale sprawa nie jest zbyt złożona. Wyobraźmy sobie sytuację, w której mamy klasę domenową dziedziczącą po innej klasie domenowej, Jak widać tabela posiada kolumny „imie” i „nazwisko” zadeklarowane w klasie Osoba oraz pole „nip” z klasy Pracownik. Dodatkowo zawiera automatycznie utworzony przez Grails identyfikator (w moim przypadku auto_increment bo korzystam z MySQLa), kolumnę „version” służącą do wersjonowania rekordów oraz kolumną „class”, która pozwala nam określić czy dany wiersz reprezentuje osobę czy pracownika. Plusem tego rozwiązania jest posiadanie w jednej tabeli wszystkiego co potrzeba (kwestia optymalizacyjna - nie potrzeba żadnych operacji złączenia). Minusem jest to, że nasza aplikacja pozwoli teraz zatrudnić pracownika „na czarno” - 12 Drugą dostępną strategią jest table-persubclass. W tym wypadku każda podklasa będzie posiadała dodatkową tabelkę, która będzie przechowywać tylko dodatkowe pola. Pola odziedziczone będą przechowywane w tabelce klasy nadrzędnej. Jak to zrobić? Wystarczy dodać mały kawałek kodu w klasie nadrzędnej: class Osoba { String imie String nazwisko static mapping = { tablePerHierarchy false } } class Pracownik extends Osoba { String nip } W tym przypadku zostanie utworzona tabela osoba, zawierająca pola „id”, „version”, „imie” i „nazwisko”. Druga tabelka to pracownik, która będzie zawierała pola „id” (pokrywające się z tabelą osoba) oraz pole „nip”. Minusem tego rozwiązania jest potrzeba wykonania złączenia zawsze, gdy będziemy chcieli pobrać rekord pracownika. W zamian za to możemy wymusić „nip” na poziomie bazy danych. Zagnieżdżone klasy domenowe Dokumentacja na ten temat nie jest zbytnio rozpisana, ale w sumie nie wiem, czy potrzeba tu dużo tłumaczenia. Chodzi o to, że możemy zagnieździć klasę domenową w innej klasie domenowej. Z punktu widzenia kodu nadal deklarujemy dwie klasy, oznaczamy jedynie, że pewna właściwość jest klasą zagnieżdżoną, np.: 13 class Engine { String type } Z takiej deklaracji otrzymamy jedną tabelę „car”, która będzie posiadała pola „id”, „version” oraz „engine_type”. Pozwala nam to na ładne rozdzielenie modelu obiektowego przy jednoczesnym uproszczeniu zapytań. Zamiast dwóch zapytań lub jednego ze złączeniem mamy po prostu jedno. Należy pamiętać, że obie klasy należy zadeklarować w pliku Car.groovy. Jeśli przeniesiemy klasę Engine do oddzielnego pliku do zostanie wygenerowana oddzielna tabelka dla tej klasy. Opimistic i Pessimistic Locking Blokowanie to sposób na zabezpieczenie się przez współbieżnymi zmianami. Zasadniczo są dwa podejścia: optymistyczne, które zakłada, że współbieżne zmiany raczej nie nastąpią (sprawdzenie wykonane jest na koniec operacji/transakcji) oraz pesymistyczne, które zakłada, że współbieżne zmiany są na tyle częste, że lepiej wywłaszczyć rekord przed wykonaniem jakichkolwiek zmian. Są to powszechnie stosowane podejścia, nie tylko w Grails (i nie tylko we framework’ach web’owych). Blokowanie optymistyczne wykorzystuje wersję rekordu (robi to za nas Grails, a właściwie to Hibernate). Każda klasa domenowa ma automatycznie wygenerowany atrybut „version”, który przy każdej operacji UPDATE jest inkrementowany. Jeśli przy zapisie rekordu GORM wykryje, że wersja została w między czasie zmieniona przez inną transakcję, nasza transakcja zostanie ROZJAZD MASZYNOWNIA BOCZNICA KONDUKTOR kolumna „nip” musi być null (gdyż może- class Car { Engine engine my chcieć dodać osobę, która nie jest prastatic embedded = [‚engine’] cownikiem i nie ma NIPu). } POCZEKALNIA Każda klasa domenowa ma automatycznie wygenerowany atrybut „version”, który przy każdej operacji UPDATE jest inkrementowany. DWORZEC GŁÓWNY Dworzec główny ROZJAZD Dworzec główny DWORZEC GŁÓWNY POCZEKALNIA KONDUKTOR BOCZNICA MASZYNOWNIA Blokownie pesymistyczne działa natomiast wykorzystując operację „SELECT ... FOR UPDATE”. wycofana i zostanie wyrzucony wyjątek StaleObjectException. Jak to obsłużymy zależy od nas: możemy napisać zmiany lub zrezygnować z zapisu i poprosić użytkownika o ponowne wykonanie zmian na aktualnych danych. (które pozwalają budować zapytania podobnie jak robi to JaQu) albo wykorzystać stary (dobry) HQL. def car = Car.get(1) // w tym miejscu inny wątek może pobrać ten sam rekord i blokowanie nic nam nie da car.lock() // tu blokujemy rekord car.model = „Audi” car.save() // inna wersja rozwiązująca powyższy problem def car = Car.lock(1) car.model = „Audi” car.save() Dla metody list() możemy dodatkowo określić ilość zwróconych rekordów, od którego rekordu rozpocząć, sortowanie łączenie z kierunkiem, a także w jaki sposób pobrać asocjacje (eager/lazy). Metoda get() po prostu pobiera rekord o zadanym id, a getAll() rekordy o zadanych id. Jeśli któryś z nich nie będzie istniał, lista wyników będzie zawierała null. Metody klas domenowych Kawałek kodu chyba najlepiej pokaże, jaBlokownie pesymistyczne działa nato- kie metody możemy wywołać: miast wykorzystując operację „SELECT ... FOR UPDATE”. Wykonanie takiej operacji // Wylistuj wszystkie osoby def persons = Person.list() na bazie danych powoduje zablokowanie // Pobierz osobę o id=3 rekordu dla innych transakcji (na poziomie def person = Person.get(3) bazy danych). Inne transakcje, które spró- // Pobierz osoby o id 3,4,5 bują pobrać ten sam rekord zatrzymają się def persons = Person.getAll(3, 4, i będą czekać na jego odblokowanie. Nale- 5) Pobierz 10 osób z offsetem 100, ży pamiętać, że jest to mechanizm zależ- // sortując po nazwisku malejąco ny od danej bazy danych i tak jak wspo- def persons = Person.list(max:10, mina dokumentacja, dostarczony z Grails offset:100, sort:”lastname”, orHSQLDB tego nie obsługuje. Jak wygląda der:”desc”) to w kodzie: Dynamic finders Dynamic finders to metody tworzone dyBlokować możemy też od razu w wykona- namicznie w środowisku uruchomienioniu zapytań. Blokady zwalniane są w mo- wym na podstawie właściwości klasy: mencie zatwierdzenia lub cofnięcia transclass Person { akcji. Pobieranie danych GORM pozwala nam na wykonywanie zapytań w kilka różnych sposobów. Możemy do nich zaliczyć podstawowe metody operujące na klasach domenowych, dynamic finders (metody typu find*), kryteria String firstname String lastname Date dateOfBirth } def p1 = Person.findByFirstname(„Mateusz”) def p2 = Person.findByLastname(„Mrozewski”) def p3 = Person.findByFirstnameAn- 14 Wypisane metody to tylko kilka możliwych przykładów. Możemy takie metody wywoływać dla każdej właściwości. Tak jak w przykładzie wyszukania osoby p3 możemy połączyć warunki dla dwóch dowolnych właściwości z operatorem AND lub OR (maksimum dwóch - dla większej ilości powinniśmy wykorzystać kryteria lub HQL). Dwa typy metod to findBy*, który zwraca pierwszy rekord wyniku oraz findAllBy*, który zwraca wszystkie pasujące wyniki. A dozwolone operacje to: Ale chwileczkę, skąd te wszystkie metody się wzięły? Przecież nic nie dziedziczyliśmy, nic nie implementowaliśmy. Tutaj z pomocą przychodzi nam dynamiczność Groovy oraz to, jak zostało to wykorzystane w Grails. Konkretnie chodzi tu o wykorzystanie metaklas i ich właściwości methodMis• InList - czy wartość znajduje się na po- sing. W książce The Definitive Guide to danej liście Grails w dodatku poświęconym Groovy możemy znaleźć nawet prosty przykład • LessThan - czy wartość jest mniejsza implementacji dynamic finder. niż podana • LessThanEquals - czy wartość jest Kryteria mniejsza lub równa niż podana Kryteria to sposób na budowanie zło• GreaterThan - czy wartość jest większa żonych zapytań poprzez wykorzystanie składni Groovy. A konkretnie wykorzystaniż podana na jest tu koncepcja builders. • GreaterThanEquals - czy wartość jest Nas interesuje jednak jak to wygląda w kowiększa lub równa niż podana dzie. Oto mały przykład z dokumentacji: • Like - podobne do SQLowego LIKE • ILike - jak wyżej, tylko case insensitive (to nie produkt apple’a) • NotEqual - nie równe (nie ma samego equal, bo to równoważne z np. findByName) • Between - czy wartość jest pomiędzy zadanymi def c = Account.createCriteria() def results = c { like(„holderFirstName”, „Fred%”) and { between(„balance”, 500, 1000) eq(„branch”, „London”) } maxResults(10) order(„holderLastName”, „desc”) } W tym przykładzie zostały utworzone • IsNotNull - czy wartość nie jest null’em kryteria dla klasy domenowej Account. Następnie rozbudowaliśmy je o kolejne • IsNull - czy wartość jest null’em warunki - ten prosty przykład chyba dość dobrze pokazuje jakie są możliwości kry- 15 ROZJAZD MASZYNOWNIA BOCZNICA Ten typ zapytań możemy również wykorzystać dla asocjacji. Do tych zapytań możemy też dodać parametry takie jak w metodzie list(). Prawda, że fajne? Osobiście uważam, że przy zastosowaniu już dwóch parametrów przestaje być to czytelne i dużo łatwiej pisze się i analizuje kryteria, ale do prostych zapytań jest to jak najbardziej ciekawe rozwiązanie. KONDUKTOR dLastnameLike(„Mateusz”, „M%”) def p4 = Person.findByDateOfBirthIsNull() POCZEKALNIA Ale chwileczkę, skąd te wszystkie metody się wzięły? DWORZEC GŁÓWNY Dworzec główny DWORZEC GŁÓWNY POCZEKALNIA KONDUKTOR BOCZNICA MASZYNOWNIA ROZJAZD Dworzec główny W GORM możemy też wykorzystać HQL teriów. Co można w takich kryteriach zrobić: UPDATE) Kilka przykładów: • wykorzystać wszystkie metody z klasy // Znajdź pierwszą osobę // o imieniu Mateusz Hibernate Restrictions • wykorzystać operatory logiczne AND, OR i NOT • pytać o inne klasy domenowe będące w asocjacji z główną klasą • wykorzystać wszystkie metody z klasy Hibernate Projections • wykorzystać wszystkie metody z klasy Hibernate ScrollableResults def person = Person.find( „from Person as p where p.firstname = ?”, [‚Mateusz’]) // Znajdź wszystkie osoby // o imieniu Mateusz // i nazwisku na M def results = Person.findAll( „from Person as p where p.firstname = :firstname and p.lastname like :lastname”, [firstname:’Mateusz’, lastname:’M%’]) • określić ilość pobranych rekordów oraz Niezbędnikiem jest zapoznanie się z rozdziałem dokumentacji Hibernate poświęod którego rekordu zacząć conej HQL. • określić czy asocjacje będą pobierane w trybie EAGER czy LAZY Zdarzenia i znaczniki czasowe Nie wypisuję wszystkich możliwości, gdyż Bardzo ciekawą funkcją w GORM są zdajest tego mnóstwo, a wszystko jest w Ja- rzenia. Zdarzenia to domknięcia wywołyvadoc’ach. Jednak ta krótka lista przynaj- wane w odpowiednim momencie cyklu mniej pokazuje jak szerokie mamy możli- życia instancji klasy domenowej. Możemy wości. GORM to nie tylko CRUD. je użyć np. do zapisania daty ostatniej aktualizacji, utworzenia encji, zapisania do logu śladu po usuwanej encji, itp. WyróżHQL niamy następujące typy zdarzeń: W GORM możemy też wykorzystać HQLa (Hibernate Query Language). Jest to język • beforeInsert - wywoływane zanim zapytań bardzo podobny do SQLa, ale nie obiekt zostanie zapisany do bazy po raz operujemy w nim na tabelkach, tylko na pierwszy encjach (klasach domenowych). Mamy do dyspozycji trzy metody, które możemy wy- • beforeUpdate - wywoływane zanim obiekt zostanie zaktualizowany w bazie wołać na naszej klasie domenowej: • find - zwraca pierwszy wynik pasujący • beforeDelete - wywoływane przed usunięciem rekordu do zapytania • findAll - zwraca wszystkie wyniki pasu- • afterInsert - wywoływane po zapisaniu obiektu do bazy po raz pierwszy jące do zapytania • executeQuery - wykonuje zapytanie, • afterUpdate - wywoływane po zaktualizowaniu obiektu w bazie niekoniecznie zwracające wyniki (np. 16 } Date lastUpdated Date loadTime def beforeInsert = { dateCreated = Date() } def beforeUpdate = { lastUpdated =new Date() } def onLoad = { loadTime = new Date() } Dla zobrazowania możliwości mały przykład: class Person { String firstName static mapping = { table ‚people’ firstName column:’First_Name’ } } Cache Pod maską GORM siedzi Hibernate, nie mogło więc zabraknąć mechanizmu second-level cache z tej biblioteki. Cache możemy włączyć w pliku konfiguracyjnym DataSource.groovy (domyślnie przy wygenerowaniu projektu jest on włączony). Możemy podobnie jak w Hibernate, w pamięci podręcznej (mało zręczne tłumaczenie słowa cache) zapisać encje lub wyniki zapytań. Czy dana klasa domenowa ma być zapisywana do pamięci podręcznej określa się w samej klasie domenowej: W tym przykładzie naszą klasę domenową rozbudowaliśmy o zapisywanie do bazy daty aktualizacji, daty utworzenia oraz ustawianie czasu wczytania rekordu. Jeśli chodzi o datę utworzenia i aktualizacji, to GORM pozwala nam na małe uproszczenie: wystarczy zadeklarować w klasie domenowej właściwość lastUpdated i dateCreated a będą one automatycznie ustawiane na odpowiednie wartości (konwencja nie tylko ponad konfigurację, ale również ponad kodowanie). class Person { Własne mapowanie Kolejną cechą GORMa jest własne mapowanie tabel. Własne mapowanie pozwala nam samodzielnie określić jakie będą nazwy tabel i kolumn. Jest to niezwykle przydatne, gdy musimy dostosować się do konwencji przyjętej w firmie/projekcie lub gdy piszemy kod do istniejącej bazy danych. Więcej na ten temat można poczytać oczywiście w dokumentacji, wydaje mi 17 static mapping = { cache true } } „Keszować” możemy też asocjacje. Możemy ustawić jeden z kilku dostępnych rodzajów pamięci podręcznej. Wpływa to na wydajność naszej pamięci, a to wynika ze strategii obsługi danych oraz dopuszczalnego ryzyka współbieżnych modyfikacji: ROZJAZD MASZYNOWNIA BOCZNICA • onLoad - wywoływane po wczytaniu Wspomnę tylko jeszcze, że możemy rówobiektu z bazy nież wpłynąć na to, jak generowane będą identyfikatory (domyślnie GORM wykoI w przykładzie: rzystuje ten najodpowiedniejszy dla danej bazy danych). Możemy też wpłynąć na inclass Person { deksy, jakie zostaną wygenerowane. Date dateCreated KONDUKTOR • afterDelete - wywoływane po usunię- się że ten dział wymaga najmniej dodatkociu obiektu z bazy wych opisów i dyskusji. POCZEKALNIA Własne mapowanie pozwala nam samodzielnie określić jakie będą nazwy tabel i kolumn. DWORZEC GŁÓWNY Dworzec główny ROZJAZD MASZYNOWNIA BOCZNICA KONDUKTOR POCZEKALNIA DWORZEC GŁÓWNY Dworzec główny Warstwa persystencji jest niezwykle rozbudowana. Jest dość elastyczna i bogata w funkcje. • read-only - nasza aplikacja tylko odczytuje dane i nigdy ich nie modyfikuje. Szybkie w działaniu, gdyż nie potrzeba nam żadnych synchornizacji. danych. Bardzo mi się to spodobało, że dostajemy z automatu zabezpieczenie na poziomie bazy danych również, a nie tylko na poziomie aplikacji. • read-write - nasza aplikacja zapisuje i Do deklaracji ograniczeń w klasie domeodczytuje dane. Dostęp jest synchroni- nowej wykorzystywana jest sekcja conzowany. straints: • nonstrict-read-write - nasza aplikacja i class Task { String name czyta i pisze, ale prawdopodobieństwo String description wystąpienia jednoczesnego zapisu jest znikome. static constraints = { description(maxSize:1000)\ • transactional - przy tej strategi może} my wykorzystać w pełni transakcyjną } pamięć podręczną, taką jak JBoss TreeCache. Podsumowanie Jak widać warstwa persystencji jest niezwykle rozbudowana w Grails. Jest dość Ograniczenia (contraints) elastyczna i bogata w funkcje. Jeśli komuś Grails posiada wbudowany mechanizm GORM nie do końca odpowiada, to zawsze walidacji danych. Pozwala on nam na bar- można skorzystać z Hibernate bezpośreddzo łatwe i deklaratywne określanie, jakie nio, wykorzystać plugin do JPA lub bezwłaściwości mogą przyjąć jakie wartości. pośrednio łączyć się z bazą danych (JDBC Dlaczego jest to ważne z punktu widzenia albo pakiet GroovySQL). Nie mniej jednak GORM? Ponieważ może to wpłynąć na to, myślę, że GORM zadowoli większość użytjak zostanie wygenerowany schemat bazy kowników. Wspólnie z Mateuszem Mrozewskim chcieliśmy Was zaprosić do udziału w SZKOLENIACH DLA KAŻDEGO Forma będzie nieco inna od tej, którą zapewne znacie. Szkolenia będą „wirtualne”. Dzięki temu Wy, bez wychodzenia z domu będziecie mogli podnieść swoje kwalifikacje, a my będziemy mogli zaoferować atrakcyjną cenę :) Czyż to nie jest Win-Win? Rozpoczniemy od Groovy i Grails. Spotkania będą odbywały się co tydzień, w sesjach 90 minutowych. Szczegółowe informacje pojawią się wkrótce na http://dworld.pl, ale już teraz zapraszamy do wstępnej rezerwacji miejsca (link). Rozpoczęcie: rozpoczęcie 1Q 2010 Cena: ok. 50 zł za 90 minut Miejsce: internet ;) Prowadzący: Mateusz Mrozewski, Grzegorz Duda 18 19 ROZJAZD MASZYNOWNIA BOCZNICA KONDUKTOR POCZEKALNIA DWORZEC GŁÓWNY Dworzec główny Notatki o testowaniu: Behaviour-driven development z easyb Bartosz Majsak Groovy to język, który przykuwa coraz @Test większą uwagę entuzjastów technologii public void testConferenceDiscount() { Javowych, a artykuły pojawiające się na Participant p = łamach Java exPress tylko to potwierdzanew Participant(); ją. Jego niewątpliwymi zaletami są prostop.setJavaUserGroupMember(true); RegistrationService ta i przejrzystość składni, które pozwalają registrationService = zdecydowanie zwiększyć produktywność i new RegistrationService(); uwolnić od monotonnych linijek kodu tak PaymentDetails paymentDetails = dobrze znanych nam z Javy. registrationService.register( participant); W niniejszym artykule chciałbym przedassertEquals(80.0, stawić bardzo ciekawe i użyteczne narzępaymentDetails.getPrice()); dzie bazujące na Groovym. Zachwyca ono } swoją prostotą i wszechstronnym zastosowaniem w testach, oraz, co najistotniejsze, pozwala na pisanie testów w myśl koncep- Nie trzeba mieć wielkiego doświadczenia w pisaniu testów, aby stwierdzić, że pocji behaviour-driven development. wyższy kod jest daleki od ideału. Przede wszystkim jest nieprecyzyjny. Nazwa meKilka(dziesiąt) słów wprowadzenia tody tak naprawdę nie niesie ze sobą żadZanim jednak przejdziemy do przedstawie- nej istotnej informacji. W momencie, gdy nia easyb, warto zacząć od krótkiego wpro- w module rejestracji pojawi się jakiś błąd, wadzenia do coraz bardziej popularnej będziemy zmuszeni do wnikliwej analizy techniki behaviour-driven development. kodu takiej metody testowej, aby określić Na jakich założeniach się opiera? Czym wymaganie definiujące warunki przydzieróżni się od test-driven development? Ja- lania zniżki. W powyższym wypadku nie kie narzędzia w Javie pozwalają pisać testy stanowi to wielkiego wyzwania, ale zaw oparciu właśnie o nią? Po przeczytaniu pewne wielu z Was musiało nie raz w swotego artykułu, poza wzbogaceniem słow- jej w karierze analizować bardziej „roznika informatycznych żargonów o kolejny budowane” testy. Co więcej, pisząc testy, modny akronim, z pewnością będziesz zwłaszcza na początku przygody z test-driven development, bardzo często powraca znał odpowiedzi na te pytania. pytanie „co ja tak naprawdę powinienem Do zilustrowania koncepcji BDD posłuży przetestować?”. Nierzadko zdarza się nam nam prosty przykład. Załóżmy, że organi- skupiać bardziej na implementacyjnych zujemy konferencję o technologiach zwią- szczegółach klas, których działanie chcemy zanych z Javą i potrzebujemy stworzyć mo- weryfikować, niż na faktycznych wymagaduł rejestracji. Oczywiście, aby zachęcić niach, które mają spełniać. Między innymi potencjalnych uczestników powinniśmy z takimi właśnie problemami próbuje się zaoferować jakieś zniżki. Dla maksymalne- mierzyć koncepcja behaviour-driven devego uproszczenia naszego przykładu umów- lopment. my się także, że cena wejściówki to 100,00 PLN. A zatem, do dzieła! Zaczynamy (jak Behaviour-driven development, sformułowane po raz pierwszy przez Dana Northa zawsze zresztą, prawda?), od testu: w magazynie „Better Software” z marca 2006, to podejście skupiające się przede 20 @Test public void shouldReceiveTwentyPe rcentOfDiscountIfRecipientIsJUGMe mber () { ... } Sposób, w jaki piszemy testy, najczęściej powinien przebiegać według następującego schematu: definiowanie warunków początkowych wykonanie logiki podlegającej weryfikacji sprawdzenie zgodności otrzymanych rezultatów z oczekiwanymi @Test public void shouldReceiveTwentyPe rcentOfDiscountIfRecipientIsJUGMe mber () { // given Participant p = new Participant(); p.setJavaUserGroupMember(true); // when RegistrationService registrationService = new RegistrationService(); PaymentDetails paymentDetails = registrationService.register( participant); // then assertEquals(80.0, paymentDetails.getPrice()); } Szablon ten bezdyskusyjnie poprawia przejrzystość testu i ułatwia jego zrozumienie. Po raz pierwszy zobaczyłem go na prezentacji Szczepana Fabera podczas konferencji GeeCON. Jest to jednak tylko konwencja, która bez dyscypliny w zespole może zostać bardzo szybko zapomniana. Na szczęście istnieją biblioteki, które wspierają technikę behaviour-driven development i pozwalają nam myśleć o testach jak o realnych wymaganiach stawianych systemowi. Jedną z nich jest właśnie easyb. przekładając to na bardziej opisową kon- Czym jest easyb? strukcję moglibyśmy napisać: Easyb to zrealizowany w Groovym język dedykowany (ang. DSL; domain-specific mając określone warunki początkowe; language), oferujący konstrukcje promogdy wykonamy pewną operację w kom- wane przez BDD. Podstawowymi pojęciami są tu scenariusze (ang. scenario) oraz ponencie testowanym; historie (ang. story), będące zbiorami scewówczas powinniśmy otrzymać oczeki- nariuszy. Spójrzmy ponownie na nasz test i spróbujmy określić wymaganie, które wany rezultat; stawiamy modułowi do rejestracji uczestFinalna wersja naszego testu, podążająca 21 ROZJAZD MASZYNOWNIA BOCZNICA za konwencją BDD, mogłaby zatem wyglądać następująco: KONDUKTOR wszystkim na wymaganiach dotyczących testowanego komponentu aplikacji. Jest tak naprawdę delikatną ewolucją test-driven development. Zasadniczą różnicą jest położenie nacisku na weryfikację konkretnych wymagań stawianych aplikacji. Każdy test (scenariusz) tworzony jest w taki sposób, aby opisywać pewne zachowanie systemu. Jednym z postulatów, które pojawiły się także w przytoczonym artykule, jest nadawanie metodom testowym nazw, które de facto są zdaniami opisującymi nasze oczekiwania. W przypadku naszego testu moglibyśmy przemianować metodę na: POCZEKALNIA Behaviour-driven development, to podejście skupiające się przede wszystkim na wymaganiach dotyczących testowanego komponentu aplikacji. DWORZEC GŁÓWNY Dworzec główny DWORZEC GŁÓWNY POCZEKALNIA KONDUKTOR BOCZNICA MASZYNOWNIA ROZJAZD Dworzec główny easyb to tak naprawdę Groovy ników: paymentDetails = registrationService. register(participant) Użytkownicy mający członkostwo w } Java User Group powinni otrzymać 20% zniżki podczas rejestracji. then “he should receive 20% dimając użytkownika z członkostwem w JUG gdy zarejestruje się on na konferencję; scount”, { paymentDetails.price. shouldBe 80.0 } } wówczas powinien otrzymać 20% zniż- Co ciekawe, test w niezaimplementowej ki; formie jest również wykonywalny. W raW easyb nasz test (scenariusz) wyglądać porcie stworzonym przez easyb figurował będzie jako “w toku” (ang. pending). będzie następująco: scenario “Java User Group members should receive 20% discount”, { given “participant is a JUG member” when “he registers for the conference” then “he should receive 20% discount” } Running user registration story (UserRegistrationStory.groovy) Scenarios run: 1, Failures: 0, Pending: 1, Time elapsed: 0.89 sec 1 behavior ran (including 1 pending behavior) with no failures Kolejną ciekawą cechą easyb są metody weryfikacji otrzymanych danych, przypoPonieważ easyb to tak naprawdę Groovy, minające opis w języku naturalnym. Pomożemy korzystać z dowolnych klas napi- równując standardowe podejście: sanych w języku Java, w tym także je te- assertEquals(80.0, stować. Drugą istotną zaletą tego języka są paymentDetails.getPrice()); domknięcia (ang. closure), dzięki którym nasz kod staje się bardziej zwięzły, a sce- z tym, oferowanym przez easyb nie trzeba nariusz przedstawiony powyżej wykony- chyba nikogo przekonywać, które jest barwalną dokumentacją. Ostateczna postać dziej zrozumiałe: naszego scenariusza mogłaby zatem mieć paymentDetails.price.shouldBe taką formę: scenario „Java User Group members should receive 20% discount”, { given “participant is a JUG member”, { p = new Participant() p.setJavaUserGroupMember(true) } when “he registers for the conference”, { registrationService = new RegistrationService() 80.0 Easyb oferuje całą gamę metod do sprawdzania wyników scenariuszy. I tak do porównania dwóch obiektów mamy następujące wyrażenia: •shouldBe / shouldNotBe •shouldEqual / shouldNotEqual możemy porównywać ich wartości: •shouldBeGreaterThan 22 Byłoby dużą niesprawiedliwością i nadużyciem przemilczenie istnienia takich bibliotek javowych jak Hamcrest czy FESTAssert, które również oferują zbliżoną do języka naturalnego weryfikację wyników testu. 1 scenario executed successfully. Story: Users registration scenario Java User Group members should receive 20% discount given participant is a JUG member when he registers for the conference then he should receive 20% discount Dodatkowe zalety Jak już wspomniałem wcześniej, testy pisane w easyb mają tak naprawdę formę wykonywalnej, choć dość uproszczonej, Scenariusze tworzone w easyb cechuje dokumentacji. Stąd wykonując scenariu- Rys 1. Podsumowanie scenariuszy w postaci strony HTML 23 ROZJAZD MASZYNOWNIA BOCZNICA sze, możemy na ich podstawie generować raporty, które traktować możemy jako zręby dokumentacji systemu. W tym momencie oferowane są nam dwie opcje – prosta jak również weryfikować ich typy: forma tekstowa oraz raport w postaci stro•shouldBeA / shouldNotBeA ny HTML. Poza informacjami zilustrowa•shouldBeAn / shouldNotBeAn nymi rysunkiem 1 i 2, zawierać one mogą nie może także zabraknąć wygodnych me- także szczegółowe informacje o błędach, tod do sprawdzania zawartości kolekcji: które pojawiły się podczas wykonywania scenariuszy. •shouldHave / shouldNotHave KONDUKTOR •shouldBeLessThan •shouldStartWith •shouldEndWith POCZEKALNIA Testy pisane w easyb mają tak naprawdę formę wykonywalnej, choć dość uproszczonej, dokumentacji DWORZEC GŁÓWNY Dworzec główny Oczywiście easyb to nie jedyne narzędzie dla JVM do tworzenia testów w oparciu o behaviour-driven development. ROZJAZD DWORZEC GŁÓWNY POCZEKALNIA KONDUKTOR BOCZNICA MASZYNOWNIA Dworzec główny Rys 2. Szczegóły scenariusza będącego w „toku” prosta i zrozumiała struktura. Pokusić by się można zatem o zaangażowanie do ich tworzenia członków projektu, którzy niekoniecznie mają na co dzień do czynienia z kodem. W dobie narzędzi takich jak FitNesse czy Concordion wydaje się jednak mało prawdopodobnym, aby osoby nietechniczne chętne były do opisywania wymagań w easyb. Od pewnego czasu spotkać jednak można pogłoski o przygotowywaniu aplikacji Easiness, która ma wyjść na przeciw oczekiwaniom takich użytkowników. Apache Ant, Maven bądź z linii komend. Integracja Bez wsparcia dla narzędzi takich jak IDE czy serwer ciągłej integracji nie możemy w pełni wykorzystać zalet, jakie oferują nam solidne testy. W momencie oddawania tego artykułu dla easyb dostępna jest wtyczka do IntelliJ IDEA oraz, od niedawna, do Eclipse. Poza tym możemy uruchomić scenariusze napisane w easyb za pomocą Konkluzja W artykule tym zaprezentowana została ciekawa alternatywa pisania testów, jaką jest behaviour-driven development, a w szczególności easyb. Pozwala ona koncentrować się na wymaganiach, a testy tworzyć w taki sposób, aby zrozumiałe były także dla osób bez doświadczenia informatycznego. Oczywiście możliwości i sposo- Alternatywy Oczywiście easyb to nie jedyne narzędzie dla JVM do tworzenia testów w oparciu o behaviour-driven development. Oto niektóre z nich: Groovy - GSpec Java - JBehave, Cuke4Duke, GivWenZen Scala - Specs 24 Źródła 1. http://dannorth.net/introducing-bdd - wprowadzenie do behaviour-driven development 2. http://easyb.org/ - strona główna projektu easyb 3. http://parleys.com/display/PARLEYS/Home#talk=28573704 Devoxx 4. http://fitnesse.org/ - strona projektu FitNes- se 5. http://code.google.com/p/givwenzen/ projektu GivWenZen 6. http://www.concordion.org/ Concordion - strona projektu 7. http://code.google.com/p/specs/ jektu Specs - strona - strona pro- 8. http://groovy.codehaus.org/Using+GSpec+with+Groovy - wprowadzenie do Spec 9. http://jbehave.org/ - projekt JBehave 10. http://wiki.github.com/aslakhellesoy/cuke4duke strona projektu Cuke4Duke 11. http://code.google.com/p/hamcrest/ - bibliote- 12. http://fest.easytesting.org/assert/wiki - bibliote- ka Hamcrest ka FEST-Assert 25 - – prezentacja o easyb z konferencji BOCZNICA KONDUKTOR jącym zalety easyb, są testy funkcjonalne interfejsu użytkownika. Opis interakcji użytkownika z naszą aplikacją, dzięki scenariuszom, wygląda bardzo naturalnie. W tandemie z wygodną biblioteką emulującą zachowanie użytkownika w przeglądarce tworzy naprawdę ciekawe rozwiązanie, ale to temat na kolejny artykuł. POCZEKALNIA by implementowania scenariuszy w easyb są dużo szersze. W artykule tym przedstawione zostały jedynie najistotniejsze jego cechy i funkcjonalności. Gorąco zachęcam do odwiedzenia stron podanych w sekcji Źródła, ale przede wszystkim do poeksperymentowania. Bardzo dobrym zastosowaniem, szczególnie uwydatnia- DWORZEC GŁÓWNY Bardzo dobrym zastosowaniem, szczególnie uwydatniającym zalety easyb, są testy funkcjonalne interfejsu użytkownika. MASZYNOWNIA ROZJAZD Dworzec główny ROZJAZD MASZYNOWNIA BOCZNICA KONDUKTOR POCZEKALNIA DWORZEC GŁÓWNY Bocznica Signal Framework dla platformy Java ME Marek Wiącek Wprowadzenie Signal Framework to open-source’owa biblioteka IoC, AOP oraz MVC przeznaczona dla Javy ME (J2ME) oraz oparta o Springa. Framework został zaprojektowany w taki sposób, aby przezwyciężyć ograniczenia CLDC, które uniemożliwiają uruchamianie kontenerów IoC opartych o Jave SE na platformie J2ME. Signal Framework używa zwykłych XMLowych plików konfiguracyjnych Springa, pozwalając programistom na wykorzystanie istniejących narzędzi i umiejętności. za tworzenie kontekstu po uruchomieniu aplikacji. Kiedy aplikacja J2ME jest uruchamiana, wykonuje ona wygenerowany kod zamiast przetwarzania plików konfiguracyjnych. Dzięki temu kontekst może zostać utworzony bez parsowania plików XML lub używania zaawansowanej refleksji. Rozmiar wygenerowanego kodu jest bardzo mały, ponieważ biblioteka IoC zawiera jedynie około 10 klas. Wygenerowany kod nie zależy od bibliotek Springa. Mimo tego, że framework został stworzony na potrzeby platformy J2ME, kontener Poniższy diagram przedstawia architektu- IoC może być używany w dowolnych aplikacjach Java, które potrzebują wykorzyrę biblioteki: stać funkcjonalność Springa bez polegania na refleksji lub parsowania plików XML (np. platformy Android oraz GWT). Signal Framework wspiera następujące funkcje kontenera IoC zaimplementowanego w Springu: Pliki konfiguracyjne XML Inspiracją dla nazwy frameworka jest biblioteka Antenna (http://antenna.sourceforge.net/) oraz, oczywiście, Spring Framework. Komponenty (beans) typu singleton Kontener IoC Wsparcie dla refleksji w CLDC jest bardzo ograniczone w porównaniu do Javy SE. API pozwala jedynie na tworzenie obiektów za pomocą domyślnych (bezargumentowych) konstruktorów. Refleksja nie pozwala na przekazywanie argumentów do konstruktorów, wywoływanie metod, dostęp do pól, ani tworzenie dynamicznych proxy. Autowiring Aby przezwyciężyć te ograniczenia framework IoC przetwarza pliki konfiguracyjne kontekstu podczas kompilowania aplikacji oraz generuje kod Javy odpowiedzialny Wstrzykiwanie zależności poprzez argumenty konstruktorów oraz własności (properties) obiektów Inicjalizacja na żądanie (lazy initialization) Przetwarzanie komponentów (bean post-processors) (com.aurorasoftworks.signal.runtime.core. context.IBeanProcessor – odpowiednik interfejsu org.springframework.beans.factory.config.BeanPostProcessor) Lekkie AOP oparte o automatycznie generowane klasy proxy 26 org.springframework.beans. factory.BeanFactoryAware Poniżej przedstawiono przykład pliku konfiguracyjnego Springa oraz odpowiadającą mu wygenerowaną klasę: <beans xmlns=”http://www.springframework.org/schema/beans” xmlns:xsi=”http://www.w3.org/2001/XMLSchema-instance” xsi:schemaLocation=”http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd”> <bean id=”greeter” class=”com.aurorasoftworks.signal.examples.context.core.Greeter”> <constructor-arg ref=”msgSource” /> </bean> <bean id=”msgSource” class=”com.aurorasoftworks.signal.examples.context.core. MessageSource”> <constructor-arg> <map> <entry key=”greeting” value=”Hello!” /> </map> </constructor-arg> </bean> </beans> 27 public class ApplicationContext extends com.aurorasoftworks.signal.runtime.core.context.Context { public ApplicationContext() throws Exception { /* Begin msgSource */ ApplicationContext.this.registerBean(„msgSource”, new com.aurorasoftworks.signal.examples.context.core. MessageSource(new java.util.Hashtable(){{ put(„greeting”, „Hello!”); }})); /* End msgSource */ /* Begin greeter */ ApplicationContext.this.registerBean(„greeter”, new com.aurorasoftworks.signal.examples.context.core. Greeter(((com.aurorasoftworks.signal.examples.context.core. MessageSource) GreeterContext.this.getBean(„msgSource”)))); /* End greeter */ } } BOCZNICA KONDUKTOR Generator kodu jest na ogół uruchamiany jako wtyczka Mavena, która wymaga 2 parametrów: nazwy pliku konfiguracyjnego Springa oraz nazwy klasy Java, która zostanie wygenerowana. Generator obsługuje znacznik <import resource=”…”>, jest com.aurorasoftworks.signal. więc możliwe przetworzenie wielu plików runtime.core.context.IConte- konfiguracyjnych poprzez jedno uruchoxtAware – odpowiednik interfejsu mienie wtyczki. POCZEKALNIA com.aurorasoftworks.signal. runtime.core.context.IInitializingBean - odpowiednik interfejsu org.springframework. beans.factory.InitializingBean DWORZEC GŁÓWNY Signal Framework to open-source’owa biblioteka IoC, AOP oraz MVC przeznaczona dla Javy ME MASZYNOWNIA ROZJAZD Bocznica DWORZEC GŁÓWNY POCZEKALNIA KONDUKTOR BOCZNICA MASZYNOWNIA ROZJAZD Bocznica Implementacja AOP dostarczona przez Signal Framework ma niewielkie wymagania pamięciowe Biblioteka obecnie nie obsługuje narzędzia Ant, ale zadania Anta (tasks) mogą być łatwo zaimplementowane jako lekka nakładka na istniejący generator. Framework AOP Oprócz utrudniania implementacji IoC, ograniczenia API CLDC uniemożliwiają rownież użycie istniejących frameworków AOP na urządzeniach J2ME. API AOP Alliance oraz popularne biblioteki AOP, takie jak AspectJ oraz JBoss AOP, zależą od typów zawartych w pakiecie java.lang. reflect.*, które nie są zaimplementowane w Javie ME. Dodatkowo implementacje AOP często wykorzystują własne implementacje class-loaderów i/lub dynamiczne proxy które nie są obsługiwane przez implementacje CLDC. Implementacja AOP dostarczona przez Signal Framework została zaprojektowana na potrzeby urządzeń J2ME: ma niewielkie wymagania pamięciowe, używa jedynie klas obecnych w API CLDC oraz alokuje podczas działania najmniejszą możliwą ilość obiektów. To podejście pociąga jednak za sobą pewne ograniczenia: framework nie jest tak rozbudowany jak jego odpowiedniki dla aplikacji desktopowych i klasy enterprise oraz obsługuje jedynie przechwytywanie metod wywoływanych na interfejsach. Typ w bibliotece Signal AOP (com.aurorasoftworks.signal.runtime.core.*) context.proxy.ProxyFactory context.proxy.IInvocationHandler context.proxy.IMethodInterceptor context.proxy.IProxy context.proxy.IProxyTarget context.proxy.IProxyClass context.proxy.IMethodHandler Framework AOP przezwycięża ograniczenia Javy ME poprzez generację kodu. Kiedy kontekst aplikacji jest przetwarzany podczas kompilacji, framework identyfikuje komponenty implementujące interfejs com.aurorasoftworks.signal.runtime.core.context.proxy.IProxy, a następnie tworzy dla nich klasy proxy. Instancje klas proxy są stosowane do przechwytywania wywołań metod oraz wywoływania interceptorów. Ta koncepcja jest podobna do mechanizmu dynamicznych proxy wspieranych przez Jave SE. Większość klas zawartych we frameworku AOP ma swoje odpowiedniki w Javie SE, co przedstawiono w Tabeli 1. Obiekty proxy są na ogół tworzone przez klasę ProxyFactory. Najbardziej powszech- ną metodą tworzenia instancji proxy jest przekazanie listy interceptorów do metody IProxyFactory#createProxy(IProxyTarget target, IMethodInterceptor [] interceptors). Zwracany obiekt implementuje te same interfejsy, co przekazany argument i może być bezpiecznie rzutowany na te interfejsy. Przechwytywanie metod zostało przedstawione na Diagramie 1. Z uwagi na swoją prostotę biblioteka posiada pewne ograniczenia. Obiekty proxy stworzone przez bibliotekę wielokrotnie Odpowiednik w Javie SE java.lang.reflect.Proxy java.lang.reflect.InvocationHandler org.aopalliance.intercept. MethodInterceptor wartość zwracana przez java.lang.reflect. Proxy.newInstance dowolny obiekt java.lang.Class java.lang.reflect.Method Tabela 1. Odpowiedniki klas Signal AOP w Java SE. 28 Diagram 1. Przechwytywanie metod wykorzystują instancje tablic przeznaczone do przekazywania argumentów do interceptorów aby uniknąć tworzenia tymczasowych obiektów. To jednak oznacza, że kod klas proxy musi być synchronizowany (odpowiada za to generator kodu). W wielowątkowych aplikacjach może to prowadzić do problemów z wydajnością. Kod proxy jest synchronizowany na instancji proxy, co oznacza że wiele instancji proxy tego dla samego obiektu może być używanych współbieżnie bez blokowania żadnych wątków. sekcjach. Framework wspiera zarówno API MIDP jak i LWUIT oraz w razie potrzeby może być łatwo rozszerzony o wsparcie dla innych technologii prezentacji. Framework nie nakłada żadnych ograniczeń na model domenowy, ani widoki, pod warunkiem że używany jest LWUIT lub MIDP. Zamiast tego, biblioteka jest zaprojektowana tak, aby ułatwić tworzenie kontrolerów. Najważniejsze funkcje zaimplementowane w warstwie kontrolera to inicjalizacja kontrolerów (oraz powiązanych widoków, jeśli istnieją) na żądanie Kiedy typy podstawowe są przekazywane (lazy initialization) oraz deklaratywne redo lub zwracane przez metodę proxy mu- guły nawigacji zdefiniowane w pliku konfiszą one zostać opakowane za pomocą ty- guracyjnym IoC. pów obiektowych takich jak java.lang. Integer. Jest to jedyny scenariusz, który Kontroler to zwykły komponent (bean) wymaga tworzenia tymczasowych obiek- zdefiniowany w kontekście IoC aplikacji. tów. W pozostałych przypadkach biblio- Wszystkie kontrolery muszą implementoteka AOP nie potrzebuje tworzyć żadnych wać interfejs com.aurorasoftworks.sitymczasowych obiektów i może być bez- gnal.runtime.ui.mvc.ICotroller lub piecznie używana niezależnie od jakości interfejsy dziedziczące po nim. Kontroler zarządza częścią interfejsu aplikacji, którą garbage collectora. może być pojedynczy widok lub skomplikowany kreator (wizard) składający sie z Framework MVC wielu kroków. Framework MVC jest oparty o funkcjonalność IoC oraz AOP opisaną w poprzednich Najbardziej powszechym rodzajem kon- 29 DWORZEC GŁÓWNY POCZEKALNIA KONDUKTOR BOCZNICA Framework MVC wspiera zarówno API MIDP jak i LWUIT MASZYNOWNIA ROZJAZD Bocznica DWORZEC GŁÓWNY POCZEKALNIA KONDUKTOR BOCZNICA MASZYNOWNIA ROZJAZD Bocznica Najważniejszym komponentem w aplikacji MVC jest dyspozytor (dispatcher). trolera jest kontroler widoku. Kontrolery ne na żądanie. W takim wypadku kontekst widoku dla API MIDP oraz LWUIT powinny aplikacji musi być odpowiednio skonfiguimplementować odpowiednio interfejsy rowany: com.aurorasoftworks.signal.runtime.ui.mvc.midp.IViewController oraz com.aurorasoftworks.signal. runtime.ui.mvc.lwuit.IViewController. Framework automatycznie przekazuje komendy (javax.microedition. lcdui.Command i com.sun.lwuit.Command) do kontrolera widoku, który je wy- generował. Wiele kontrolerów może być powiązanych z widokiem. Drugim typem kontrolera jest kontroler przepływu (flow controller), który zarządza reużytkowalnym procesem, na ogół kreatorem składającym się z wielu widoków. Po zakończeniu działania przepływ powraca do miejsca, w którym został rozpoczęty, podobnie jak wywołanie metody. Kontrolery przepływu powinny implementować interfejs com.aurorasoftworks. signal.runtime.ui.mvc.IFlowController. Przepływy można ze sobą łą- czyć: jeden przepływ może startować inne przepływy, w tym nowe instancje tej samej klasy kontrolera przepływu. Zależności między kontrolerami są definiowane jako zależności między komponentami, dzięki czemu kontrolery mogą „generować” zdarzenia wywołując metody interfejsów. Takie rozwiązanie skutkuje luźnym powiązaniem kontrolerów, deklaratynymi definicjami reguł nawigacji i silnym typowaniem. Wywołania metod interfejsów są przechwytywane przez framework aby w przezroczysty sposób zrealizować niezbędne przetwarzanie takie jak wyświetlanie właściwego widoku, rejestrowanie obserwatorów dla komend oraz inicjaowanie i kończenie przepływu. W większości przypadków pożądane jest aby kontrolery i widoki były inicjalizowa- <beans xmlns= ”http://www.springframework.org/ schema/beans” xmlns:xsi= ”http://www.w3.org/2001/XMLSchema-instance” xsi:schemaLocation=”http://www. springframework.org/schema/beans http://www.springframework.org/ schema/beans/spring-beans.xsd” default-lazy-init=”true”> <!-- ... --> </beans> Najważniejszym komponentem w aplikacji MVC jest dyspozytor (dispatcher). Dyspozytor to obiekt dostarczany przez bibliotekę, który przechwytuje wywołania metod i realizuje niezbędne przetwarzanie, takie jak wyświetlanie odpowiedniego widoku lub rejestracja obserwatora komend: <bean id=”dispatcher” class= ”com.aurorasoftworks.signal. runtime.ui.mvc.midp.Dispatcher” /> Istnieją dwie implementacje koncepcji dyspozytora: com.aurorasoftworks. signal.runtime.ui.mvc.midp.Dispatcher oraz com.aurorasoftworks. signal.runtime.ui.mvc.lwuit.Dispatcher, używane odpowiednio w ap- likacjiach MIDP oraz LWUIT. Po definicji dyspozytora należy umieścić definicje kontrolerów oraz widoków, co pokazano poniżej. Zależności między kontrolerami są definiowane jako zależności między komponentami IoC. <bean id=”accountListViewCtl” class=”com.aurorasoftworks.signal.examples.ui.mvc.midp.AccountListViewController”> 30 W powyższym przykładzie kontroler nazwany accountListViewCtl obsługuje dwa rodzaje zdarzeń: newAccountEvent oraz editAccountEvent, które są powiązane z dwoma innymi kontrolerami jako własności komponentów. Te zdarzenia to zwykłe referencje do interfejsów zdefiniowanych poniżej. Kontrolery „generują” zdarzenia poprzez wywoływanie metod interfejsów, które są z kolei przechwytywane przez dyspozytora. public interface INewAccountEvent { void onNewAccount(); } Dystrybucja kodu źródłowego frameworka zawiera przykładową aplikację zaimplepublic interface IEditAccountE- mentowaną w technologiach MIDP oraz vent LWUIT, która demonstruje prawidłowe { użycie biblioteki MVC. void onEditAccount( } IAccount account); Diagram 2 przedstawia sekwencję wywo- Diagram 2. Sekwencja wywołań obsługi akcji 31 BOCZNICA Komenda wybrana przez użytkownika jest wysyłana do dyspozytora (CommandListener.commandAction), który z kolei przekazuje ją do aktywnego kontrolera (ICommandHandler.handleCommand ). Instancja AccountListViewController reaguje na komendę generując zdarzenie INewAccountEvent.onNewAccount, które jest przechwytywane przez dyspozytora. Dyspozytor deaktywuje instancję AccountListViewController, zmienia bieżący widok na ten, który jest powiązany z instancją NewAccountViewController oraz aktywuje go. Przejście z jednego kontrolera na drugi jest prawidłowo odzwierciedlone w stanie aplikacji: NewAccountViewController staje się aktywnym kontrolerem, a jego powiązany widok jest wyświetlony na ekranie. KONDUKTOR łań niezbędną do obsłużenia akcji użytkownika w scenariuszu opisanym powyżej. POCZEKALNIA <constructor-arg ref=”accountListView” /> <constructor-arg ref=”accountService” /> <property name=”newAccountEvent” ref=”newAccountCtl” /> <property name=”editAccountEvent” ref=”editAccountCtl” /> </bean> DWORZEC GŁÓWNY Dystrybucja kodu źródłowego frameworka zawiera przykładową aplikację MASZYNOWNIA ROZJAZD Bocznica DWORZEC GŁÓWNY POCZEKALNIA KONDUKTOR BOCZNICA MASZYNOWNIA ROZJAZD Bocznica Biblioteka została zaprojektowana aby pogodzić dwa sprzeczne wymagania Podsumowanie Biblioteka została zaprojektowana aby pogodzić dwa sprzeczne wymagania: wykorzystać jak największą część funkcjonalności Springa oraz ułatwić dodanie wsparcia dla innych kontenerów IoC w przyszłości, jeśli będzie to potrzebne. Funkcjonalność opisana w tym artykule została zrealizowana dość małym kosztem: około 10 000 linii kodu nie licząc przykładowych aplikacji. wymaga w pewnych sytuacjach aby klasy aplikacji implementowały interfejsy biblioteki. Z tego samego powodu szablony klas (generics) nie są stosowane w API frameworka. Po kilku wersjach beta framework stał się dość dojrzały i zawiera wszystkie znaczące funkcje, które były planowane. Wersja produkcyjna powinna być dostępna najpóźniej w styczniu 2010. Dodatkowe informacje można znaleźć w Pewne uproszczenia musiały zostać zaaknastępującej lokalizacji: http://www.auroceptowane z uwagi na ograniczenia platrasoftworks.com/products/signalframeformy Java ME; przede wszystkim framework. work nie wspiera annotacji i zamiast tego 32 „Jakiego narzędzia używasz do budowy projektu i dlaczego?” W świecie Javy, na tak zadane pytanie można spodziewać się jednej z dwóch odpowiedzi: Używam Mavena. Świetne pluginy pozwalają mi w trzech liniach uzyskać efekt, na który potrzebowałbym kilkadziesiąt linii XMLa, gdybym używał Anta. Czasami przeszkadza mi brak elastyczności Mavena, ale radzę sobię wówczas używając plugina AntRun. • ciężko jest zaimplementować jakikolwiek algorytm w skrypcie budującym; trudno wyrazić nawet tak podstawowe konstrukcję jak if czy for, • ciężko jest wykonać nawet pozornie proste taski, których wykonywanie nie zostało przewidziane przez developerów Anta/Mavena, i które nie są dostępne w postaci gotowych tasków lub pluginów, • potrzebna jest dogłębna znajomość tych narzędzi, by zrozumieć złożony skrypt budujący, • podejście „build by convention” nie jest wspierane (Ant), albo skutecznie utrudnia konfigurację (Maven), • wsparcie dla budowy projektów wielomodułowych jest niewystarczające, • skrypty budujące są niepotrzebnie duże i nieczytelne z uwagi na narzut samego języka (XML), Użytkownik Mavena Używam Anta i dzięki jego elastyczności mogę zrobić wszystko - co tylko zechcę. Maven zdobył popularność dzięki zarządzaniu dependencjami, ale ja mam Apache Ivy, który potrafi o wiele więcej. Użytkownik Anta Nie mam zamiaru powtarzać tu argumentów, którymi zwolennicy Anta i Mavena wymieniali się wielokrotnie w niezliczonych dyskusjach na czatach, forach i blogach. Po pierwsze dlatego, że nie pojawiły się żadne nowe argumenty na rzecz któregokolwiek z tych dwóch rozwiazań. Po drugie dlatego (i to jest główny powód), że na scenie narzędzi do budowy projektów pojawił się nowy zawodnik - Gradle. I jest to gracz tak silny, że ma wszelkie szanse by usunąć w cień zarówno Anta jak i Mavena, a dyskusję o wyższości jednego nad drugim przesunąć do historii komputerowych „flame wars”. Aby móc w pełni skorzystać z lektury tego artykułu, zachęcam do instalacji Gradle. Instrukcja instalacji znajduje się na stronie projektu. Dla Twojej wygody do artykułu dołączone są kody źródłowe. Zarówno artykuł jak i kod źródłowy jest zgodny z wersją 0.8 Gradle (najnowszą w momencie pisania artykułu). Nim zajmiemy się Gradlem, przyjrzyjmy Celem tego artykułu jest przedstawienie się najbardziej znanym (czyt. uciążliwym) projektu Gradle i sprawienie, byś zapra- 33 KONDUKTOR słabościom Anta i Mavena: POCZEKALNIA Wstęp DWORZEC GŁÓWNY Tomasz Kaczanowski BOCZNICA MASZYNOWNIA Gradle - mocarne narzędzie do budowy projektów ROZJAZD Maszynownia ROZJAZD MASZYNOWNIA BOCZNICA KONDUKTOR POCZEKALNIA DWORZEC GŁÓWNY Maszynownia Jakiego narzędzia używasz do budowy projektu i dlaczego? gnął poznać go lepiej. Nie zamierzam nia skryptów budujących w DSLach oparkonkurować z wyczerpującą dokumenta- tych o inne niż Groovy języki JVM - np. cją ani z dołączonymi do dystrybucji Gra- Scala, JRuby). dle przykładami. Pierwszy skrypt budujący Gradle - krótkie wprowadzenie Obiecałem zaprezentować Gradle poW artykule przedstawię Gradle prezen- przez przykłady jego użycia, zajmijmy się tując kilka z życia wziętych przykładów więc napisaniem kawałka kodu. Zaczniejego zastosowania. Jednak nim to nastą- my od bardzo prostej aplikacji webowej. pi, pozwolę sobie pokrótce scharaktery- Następnie rozszerzę ten przykład, co zować ten projekt, tak byś wiedział czego pozwoli mi zaprezentować różne cechy możesz oczekiwać. Gradle - przede wszystkim dynamicznę naturę jego skryptów oraz wsparcie dla Gradle jest bardzo elastycznym narzę- projektów wielomodułowych. dziem do budowy projektów, w szczególności projektów Javy. Umożliwia pisanie Najmniejsza na świecie aplikacja skryptów budujących w opartym na Gro- webowa ;) ovym DSLu, co sprawia, że wyrażenie w nich algorytmów staje się banalnie pro- Layout naszego projektu prezentuje się ste. Gradle pozwala na tworzenie zależ- następująco: ności między zadaniami (taskami) i opie- myWebApp ra się na paradygmacie ”convention over `- src configuration” (z tym że konfiguracja jest `- main |- java zawsze możliwa i łatwa do przeprowa| `- org dzenia). Gradle wykracza poza możliwo| `- gradle ści innych narzędzi, jednocześnie stosuje | `- sample | `- Greeter.java się do przyjętych przez społeczność Javy `- webapp niepisanych standardów, obniżając w ten `- index.jsp sposób koszt związany ze zmianą narzęJak widać projekt jest prosty aż do bólu. dzia budującego. Ma tylko jedną klasę (Greeter.java), Gradle jest nadal w trakcie developmen- która wykorzystuje metody z bibliotu, osiągnął już jednak stabilność, pozwa- teki Commons Lang ze zbioru Apache lającą na jego produkcyjne użycie. Lista Commons, oraz jedną stronę JSP - inzaimplementowanych funkcjonalności dex.jsp - która używa tejże klasy. jest już obecnie imponująca (na uwagę zasługuje m.in. zaawansowane wsparcie build.gradle dla projektów wielomodułowych), a do Czas dodać skrypt budujący - nadamy realizacji czeka jeszcze wiele wartościomu domyślną nazwę build.gradle. wych pomysłów (m.in. możliwość pisa- 34 35 • skrypty Gradle mogą być bardzo zwięzłe, • nie ma XMLa, jest DSL, • Gradle potrafi „rozmawiać” z repozytoriami Maven. • Gradle działa zgodnie z paradygmatem “convention over configuration”, co pozwala zminimalizować wielkość pliku budującego, pod warunkiem że używane są domyślne wartości (takie jak layout projektu, nazwy plików wynikowych itp.). BOCZNICA KONDUKTOR POCZEKALNIA Skrypt ten powinien wykonać następują- Tak, to wszystko prawda, ale zapewniam ce zadania: że to dopiero początek. Póki co, przyjrzyjmy się dokładniej temu, co (i jak) robi • załadować wymagane dependenten skrypt. cje, Informacje o procesie budowania • skompliować klasy Javy, Używając Gradle możesz z łatwością dowiedzieć się wielu użytecznych in• zbudować plik WAR. formacji na temat procesu budowania A więc proszę - przed nami pierwszy projektu. Wykonaj komendę gradle skrypt budujący napisany z użyciem Gra- --tasks aby poznać dostępne zadania dle. (te napisane przez Ciebie i dostarczousePlugin ‚war’ ne przez zadeklarowane w skrypcie version = 0.1 budującym pluginy). Wykonaj gradle --properties żeby poznać wszystkie repositories { mavenCentral() propertiesy i gradle --dependen} cies by dowiedzieć się o wszystkich zależnościach projektu. Ale przede dependencies { compile wszystkim wykonaj komendę gradle „commons-lang:commons-lang:2.4” -? by poznać mnóstwo innych przy} datnych komend. Umieść ten plik w głównym folderze projektu myWebApp main i uruchom pisząc DSL i „convention over configuraw linii komend gradle war. Wynikowy tion” plik myWebApp-0.1.war znajdziesz w folPrawdopodobnie zauważyłeś, że zaprederze build/libs. Zdeployuj go w dozentowany plik build.gradle jest barwolnym kontenerze webowym (np. Tomdzo zwięzły. Jest to możliwe dzięki dwóm catcie) i rozkoszuj się aplikacją działającą cechom Gradle: pod adresem http://YourHostName/ myWebApp-0.1. • Gradle używa DSL, który jest o wiele bardziej zwięzły niż XML (łaPodejrzewam, że w powyższym skrypcie twiej też go czytać i zrozumieć), Twoją uwagę przykuło kilka spraw: DWORZEC GŁÓWNY Gradle jest bardzo elastycznym narzędziem do budowy projektów, w szczególności projektów Javy. MASZYNOWNIA ROZJAZD Maszynownia ROZJAZD Maszynownia DWORZEC GŁÓWNY POCZEKALNIA KONDUKTOR BOCZNICA MASZYNOWNIA Gradle stosuje rozpowszechnioną przez Mavena strukturę projektu Powiedz to tak po prostu Chciałbyś użyć w skrypcie plugina War ? Chciałbyś ustawić wersję na 1.0 ? Po prostu to powiedz: usePlugin ‚war’ version = 0.1 Chciałbyś wypisać komunikat jeżeli zajdzie pewien warunek ? Po prostu to powiedz: if (someCondition) { println “some message” } Jeżeli zastanowisz się nad tymi dwoma przykładami, dojdziesz do wniosku, że nie ma w nich nic niezwykłego. „Tak to powinno wyglądać”. Masz rację. A jednak, wyrażenie tych jakże prostych rzeczy jest bardzo trudne w Antcie i Mavenie. Gradle, używając mieszanki DSL i Groovyego, daje możliwość bezpośredniego wyrażania tego, co pragniesz powiedzieć. Przykładowo, Gradle stosuje rozpowszechnioną przez Mavena strukturę projektu, i domyślnie zakłada, że pliki źródłowe znajdują się w folderach jak poniżej.[1] nazwę tworzoną według wzorca nazwaProjektu-wersja.war (z kolei nazwa projektu jest identyczna z nazwą folderu, w którym projekt się znajduje, o ile nie wyspecyfikowano inaczej). Inną domyślną wartością jest użycie kompilatora Javy w wersji 1.5. Konfiguracja jest możliwa i łatwa do wykonania Jeżeli zajdzie taka potrzeba, wówczas wszystkie powyższe domyślne ustawienia można z łatwością zmodyfikować. Przykładowo, załóżmy że chcemy, by wszelkie artefakty (pliki JAR i WAR) trafiały do katalogu build/artifacts zamiast do domyślnego build/libs. Wystarczy ustawić wartość zmiennej libsDirName na „artifacts”: libsDirName = „artifacts” Aby zmienić nazwę generowanego pliku JAR czy WAR należy z kolei ustawić wartość zmiennej archivesBaseName: archivesBaseName = the-default-name” „i-dont-like- Ponieważ prezentowana aplikacja myInna konwencja sprawia, że stworzony WebApp wykorzystuje domyślne ustaprzez Gradle plik WAR domyślnie nosi wienia, nie ma więc potrzeby modyfi`-- src |-| | | | `-- main |-- groovy |-- java |-- resources `-- webapp test |-- groovy |-- java `-- resources // // // // kod źródłowy pisany w Groovym kod źródłowy pisany w Javie zasoby aplikacji kod źródłowy aplikacji webowej // testy napisane w Groovym // testy napisane w Javie // zasoby testowe 36 Gradle pluginami stoi. • • • • dostępnych staje się prawie 20 nowych tasków (np. clean, compileJava, test, jar), o kopiuje skomplikowane klasy do WEB-INF/classes, o umieszcza wszelkie zależności runtime w katalogu WEB-INF/lib, o tworzy plik archiwum WAR w katalogu build/libs Obecnie Gradle oferuje 9 pluginów (Java, Groovy, War, OSGi, Eclipse, Jetty, Maven, Project-reports, Code-quality), a jeżeli nie znajdziesz wśród nich potrzebnej Ci funkcjonalności, zawsze możesz napisać swój własny (jest to bardzo proste, acz do konfiguracji projektu dołącza- wykracza poza ramy tego artykułu - zerkny jest tzw. „convention object”, nij proszę do dokumentacji Gradle). co skutkuje ustawieniem wielu Repozytoria i zarządzanie domyślnych wartości, np.: zależnościami javadoc.destinationDir= file(‚build/docs/javadoc’) W zaprezentowanym skrypcie użyty został plugin War, co ma następujące konsekwencje: 37 kopiuje pliki z src/main/ webapp do głównego katalogu archiwum, stworzone zostają zależności miedzy taskami, które wymuszają rozsądną kolejność ich wykonywania (np. task test wykonywany jest po taskach compileJava i compileJavaTests tasks), archivesBaseName = projectName • o KONDUKTOR Zgodnie z aktualną modą, Gradle pluginami stoi. :) Każdy plugin może rozszerzyć zasób funkcji oferowanych przez Gradle na trzy sposoby. Dla przykładu, użycie w skrypcie budującym pluginu Java niesie za sobą następujące konsekwencje: dodany zostaje task war, który robi to, czego należy oczekiwać po tasku war: POCZEKALNIA W pluginach siła • zaimportowany zostaje plugin Java (plugin War jest od niego zależny), a wraz z nim wszystkie jego zadania i ustawienia konfiguracyjne, Mam dla Ciebie dobrą wiadomość - jak już przepiszesz swoje skrypty budujące na Gradle, będziesz mógł nadal używać tych samych repozytoriów artefaktów, których używasz obecnie. Gradle opiera się na Apache Ivy, i potrafi „dogadać się” z każdym możliwym do wyobrażenia typem repozytorium - od „standardowych” repozytoriów Mavena, poprzez staroświeckie foldery libs używane tradycyjnie w buildach Anta, aż po niemal dowolne repozytorium o zdefiniowanej DWORZEC GŁÓWNY kowania wartości tych zmiennych, skutkiem czego plik budujący jest bardzo krótki. BOCZNICA MASZYNOWNIA ROZJAZD Maszynownia DWORZEC GŁÓWNY POCZEKALNIA KONDUKTOR BOCZNICA MASZYNOWNIA ROZJAZD Maszynownia Osiągnięcie rzeczy prostych jest w Gradle nie tylko łatwe, ale i daje się elegancko wyrazić. przez programistę strukturze. dostępny w Antcie. Dodaj poniższy kawałek kodu do pliku build.gradle i wykoJak widać na przykładzie pierwszego naj polecenie gradle clean checksum. skryptu, osiągnięcie rzeczy prostych jest w Gradle nie tylko łatwe, ale i daje się Ant i Gradle elegancko wyrazić. Aby Gradle przeszuGradle zapewnia świetną integrację kiwał centralne repozytorium Mavena z Antem. Możesz zaimportować Anw poszukiwaniu wymaganych artefaktowy plik build.xml bezpośrednio tów, wystarczy dodać takie oto linijki do do skryptu budującego Gradle. Dzięskryptu budujacego: ki temu wszelkie targety z pliku Anta, repositories { staną się dostępne jako taski Gradle. mavenCentral() Inny typ integracji zapewnia dostacza} Deklaracje dependencji są o wiele bar- na przez Groovyego klasa AntBuilder. dziej zwięzłe, niż te, którymi posługujesz Obiekt ant jest dostępny w każdym się w Mavenie 2[2] lub Ivy. Zamieszczony skrypcie budującym Gradle, dzięki czeponiżej kawałek kodu dodaje JAR biblio- mu możesz używać całego bogactwa teki Commons Lang do classpatha kom- tasków Anta - począwszy od prostych takich jak javac, copy, jar, aż po barplikacji: dziej złożone - np. selenese (do urudependencies { chamiania testów Selenium) czy findcompile „commons-lang:commons-lang:2.4” bugs(do generowania raportów ze } statycznej analizy kodu narzędziem Findbugs). Pisanie własnej logiki Jak dotąd polegaliśmy wyłącznie na To wszystko czyni migrację z Anta do funkcjonalnościach oferowanych przez Gradle możliwie bezbolesną... a gdy Gradle. Najwyższy czas, by dorzucić nie- już jej dokonasz, to nie spodziewam co własnej wymyślnej funkcjonalności. się byś kiedykolwiek zechciał do Anta W ten sposób poznamy też klika nowych powrócić. :) możliwości Gradle. task checksum ( Liczenie sum kontrolnych pliku (z użyciem Anta) dependsOn: assemble) << { def files = file(libsDir). listFiles() files.each { File file -> ant.checksum(file: file, property: file.name) println „$file.name Checksum: ${ant.properties[file.name]}” } } Napiszemy teraz taska, który policzy sumy kontrolne plików z katalogu build/ libs (domyślny katalog, do którego trafiają stworzone pliki JAR i WAR). Ponieważ wierzymy mocno w reużycie kodu, wykorzystamy w tym celu task checksum W tych kilku linijkach kodu dzieje się cał- 38 39 ROZJAZD MASZYNOWNIA BOCZNICA KONDUKTOR kiem sporo - widać użycie skryptów Gro- dwie fazy i wykonanie pewnych operacji ovyego, definiowanie zależności miedzy na grafie tasków, nim nastąpi ostateczne taskami i użycie tasków Anta. wykonanie tasków. W tym prostym przykładzie, graf tasków jest analizowany i na Po pierwsze, tworzony jest nowy task podstawie tej analizy podejmowana jest o nazwie checksum, który zależy od ta- decyzja o nazwie wynikowego pliku: ska assemble dostarczanego przez plutask release(dependsOn: assemble) gin Java (ten task odpowiedzialny jest za << { println ’We release now’ tworzenie plików JAR i WAR). Następna } linijka, napisana w Groovym, sprawia że wszystkie pliki z katalogu libsDir (war- build.taskGraph.whenReady { taskGraph -> tosc tego property jest domyślnie ustaif(taskGraph. wiana przez plugin Java i wskazuje na hasTask(’:release’)) { katalogbuild/libs) trafiają do listy. W version = ’1.0’ } else { końcu na każdym z plików z listy wywoversion = ’1.0-SNAPSHOT’ ływany jest Antowy task checksum, a } wynik wypisywany jest na standardowe } wyjście. Wykonaj skrypt budujący dwukrotnie pierwszy raz wywołując komendę graSnapshoty i wersje stabilne dle clean assemble a następnie graW świecie Javy przyjęło się, że nazwy ar- dle clean release. Sprawdź w katatefaktów odzwierciedlają fakt, czy arte- logu build/libs jakie nazwy otrzymały fakty te są stabilne (tzw. releasy) czy też wynikowe pliki. nie (snapshoty). Snapshoty zwyczajowo otrzymują suffix „-SNAPSHOT”, co spra- Tworzenie raportu z testów wia że łatwo odróżnić je od wersji stabilWyobraź sobie, że wykorzystujesz Gradle nych. Zaimplementujmy teraz w naszym do umieszczenia bundli z testami integraskrypcie budującym ten schemat nazewcyjnymi (napisanymi z użyciem TestNG) niczy artefaktów. w środowisku OSGi, w którym działają Poniższy fragment pliku build.gra- moduły testowanej aplikacji. Modułów dle pokazuje bardzo interesującą cechę tych jest ponad 10, i ulegają one częstym Gradla. Otóż wywołanie skryptu budu- zmianom (jako że wszystkie są jeszcze jącego podzielone jest na dwie fazy. W we wczesnym stadium developmentu). pierwszej konstruowany jest acykliczny Chciałbyś wygenerować raport z testów, graf zależności miedzy taskami (DAG, który zawierałby: ang. dependency acyclic graph). W dru• dane na temat środowiska (wersja giej fazie następuje właściwe wywołanie Javy, system operacyjny itp.), tasków. Poniższy prosty przykład pokazuje, że jest możliwe „wejście” pomiedzy te • lista bundli zdeployowanych w POCZEKALNIA Gradle zapewnia świetną integrację z Antem. DWORZEC GŁÓWNY Maszynownia ROZJAZD Maszynownia MASZYNOWNIA Dużo lepszym rozwiązaniem jest stworzenie własnego taska i wywołanie go z odpowiednimi parametrami z poziomu skryptu budującego. DWORZEC GŁÓWNY POCZEKALNIA KONDUKTOR BOCZNICA środowisku OSGi (wraz z numerem wersji każdego z bundli), • wyniki testów. Możliwe jest upchnięcie całej logiki tworzenia raportu bezpośrednio do skryptu budującego, ale wówczas ten urósłby nieprzyzwoicie i stałby się nieczytelny. Dużo lepszym rozwiązaniem jest stworzenie własnego taska i wywołanie go z odpowiednimi parametrami z poziomu skryptu budującego. Plik build.gradle wyglądałby w zarysie tak: import org.gradle.sample.report. ReportTask ... dependencies { compile ’commons-lang:commons-lang:2.4’ testRuntime ’org.easymock:easymock:2.5.2’ // much more dependencies here } ... task report(type: ReportTask, dependsOn: itest) { reportDir = ’build/report’ testNgResultsDir = ’build/itest/test-result’ serverLogDir= ’build/itest/server-log’ jars = configurations.testRuntime } „java.version”, „java.vm.version”, „java.runtime.version”, „user.language”, „user.name”].each { ext.append(it + „:\t${System.getProperty(it)}\n”) } text.append(„gradle -v”. execute().text) jars.each { text.append(„$it.name\n”) } ... ant.zip( destfile: zipReportFile, basedir: tmpDir) } ... } Te dwa kawałki kodu demonstrują kilka godnych uwagi rzeczy. Po pierwsze, wszelkie typowe operacje na plikach (kopiowanie, tworzenie archiwum ZIP, usuwanie itd.) są bardzo łatwe do wykonania, gdy można użyć skryptów Groovyego (w tym klasy AntBuilder). Po drugie, stworzenie własnego, reużywalnego taska, a następnie skonfigurowanie go i wywołanie, jest bardzo proste. W tym przypadku task ReportTask przyjmuje cztery parametry, których dostarcza mu skrypt budujący (taki podział pozwala na wyrzucenie prawdziwej logiki bizneA tak wygladałaby klasa tasku (z pomisowej poza skrypt). Po trzecie w końcu, nięciem nieistotnych szczegółów): przykład ten pokazuje, że dependencje w public class ReportTask extends Gradle można przetwarzać w najróżniejDefaultTask { szy sposób - w powyższym przykładzie def String reportDir wykorzystujemy ten fakt do banalnego def String testNgResultsDir zadania wypisania ich do pliku raportu. def String serverLogDir def FileCollection jars @TaskAction def createReport() { def text = new StringBuilder() [„os.name”, „os.version”, Budowa projektów wielomodułowych Nasza aplikacja webowa potrzebuje kolejnego interfejsu użytkownika - napisze- 40 core - projekt zawierający logikę biznesową. Napisany w Javie, zależny od biblioteki Commons Lang. Oba projekty interfejsu użytkownika używają jego klas. Jak widać cały projekt wielomodułowy posiada tylko jeden skrypt budujący build.gradle. Można zastanawiać się, czy dobrym pomysłem jest zawarcie całej logiki budowania w jednym miejscu. Jeżeli uznamy, że nam to nie odpowiada, • swing - odpowiedzialny za interGradle spełi naszą zachciankę i pozwoli fejs użytkownika dla aplikacji de- na stworzenie oddzielnych skryptów busktopowej. Napisany w Groovym. dujących dla każdego podkatalogu.[3] • web - tworzy webowy interfejs Jeden czy wiele skryptów użytkownika. Napisany w JSP. budujących ? Wynikowy WAR musi zawierać wszystkie wymagane w runtimie Najważniejsze jest to, że można uzybiblioteki - nie tylko JAR z projek- skać ten sam efekt stosując oba potu core, ale również jego depen- dejścia. Dla przykładu, jeżeli chcemy dencje (tj. bibliotekę Commons dodać do projektucore dependencję w postaci biblioteki Commons Lang, to Lang). możemy albo dodać ten kawałek kodu Layout projektów wielomodułowych do pliku build.gradle w głównym katalogu: Gradle oferuje dużą elastyczność, jeżeli project (‘:core’) { chodzi o layout projektów wielomodułodependencies { compile “commons-lang:commonswych. Na potrzeby tego artykułu, zastolang:2.4” suję layout hierarchiczny (na schemacie } pominąłem zawartość katalogów src): } • . |-|-|-| `-- 41 build.gradle settings.gradle core `-- src ui |-- swing | `-- src `-- web `-- src albo dodać tę linijkę do pliku build. gradle w podkatalogu core: dependencies { compile “commons-lang:commonslang:2.4” } Jak widzisz, jest to jedynie kwestia smaku. BOCZNICA KONDUKTOR W głównym katalogu znajdują się dwa podkatalogi - na projekt core, i drugi nazwany ui. W podkatalogu ui znajdują się wszystkie projekty związane z budową interfejsu użytkownika - obecnie są to dwa projektu swing i web. POCZEKALNIA my go w Swingu. Jest to dobry moment, by podzielić projekt na trzy części - przy okazji prezentując fragment tego, co Gradle ma nam do zaoferowania w kontekście budowy projektów wielomodułowych: DWORZEC GŁÓWNY Gradle oferuje dużą elastyczność, jeżeli chodzi o layout projektów wielomodułowych. MASZYNOWNIA ROZJAZD Maszynownia ROZJAZD Maszynownia DWORZEC GŁÓWNY POCZEKALNIA KONDUKTOR BOCZNICA MASZYNOWNIA Zanim przyjrzymy się skryptowi budującemu, zastanówmy się jakie zadania powinien on wykonywać. settings.gradle Po piersze przyjrzyjmy się plikowi, którego dotąd jeszcze nie napotkaliśmy: settings.gradle. W nim właśnie znajdują się informacje o layoucie projektu[4]. Jak widać poniżej, wskazuje on Gradleowi w jakich podkatalogach znajdują się poszczególne moduły (podprojekty): include “ui:web” “core”, “ui:swing”, build.gradle flatDir(name: ‘fileRepo’, dirs: “./repo”) } uploadArchives { repositories { add project.repositories.fileRepo } } } project (‘:core’) { dependencies { compile “commons-lang:commonslang:2.4” } } Zanim przyjrzymy się skryptowi budującemu, zastanówmy się jakie zadania powinien on wykonywać. Wydaje się, że są project (‘:ui’) { one następujace: subprojects { • • projekt core - kompilacja klas Javy i budowa archiwum JAR, } } dependencies { compile project (‘:core’) } projekt web - kompilacja klas Javy i plików JSP, i budowa archiwum project (‘:ui:web’) { usePlugin ‘war’ WAR, } • projekt swing - kompilacja klas project (‘:ui:swing’) { Groovyego i budowa archiwum usePlugin ‘groovy’ dependencies { JAR, groovy “org.codehaus.groovy:groovy-all:1.6.5” } } umieszczenie wszystkich stworzonych artefaktów (plików JAR i WAR) do wspólnego zasobu (podNawet jeżeli pierwszy raz w życiu wikatalogu repo w główym katalogu dzisz plik build.gradle opisujący buprojektu) dowę projektu wielomodułowego, nie Plik budujący build.gradle przedsta- powinieneś mieć problemów z jego zrozumieniem. Po pierwsze, korzystając z wiony jest poniżej. property subprojects, skrypt ustawia subprojects { usePlugin ‘java’ pewne własności wspólne dla wszystkich group = ‘org.gradle.sample’ podprojektów (core, swing i web). W ten version = ‘1.0’ sposób wszystkie podprojekty będą: • repositories { mavenCentral() 42 • miały własność group ustawioną na org.gradle.sample a wersję na 1.0, • szukały dependencji w centralnym repozytorium Mavena i w lokalnym katalogu repo, Jeżeli chcemy wybudować wyłącznie jeden z interfejsów użytkownika, możemy użyć tasku buildNeeded[5]. Na przykład, wykonanie polecenia gradle buildNeeded w podkatalogu ui/swing, poskutkuje zbudowaniem wyłącznie projektów core i swing oraz stworzeniem dwóch artefaktów: core-0.1.jar i swing0.1.jar. składowały stworzone artefakty w Wniosek z tego wszystkiego taki, że Grakatalogu repo. dle umożliwia wykonywanie częściowych Po ustawieniach wspólnych dla wszyst- buildów, a nam nie pozostaje nic innego kich podprojektów, każdy z podpro- jak z tej możliwości ochoczo korzystać (w jektów konfigurowany jest osobno. końcu funkcjonalność ta pozwala znacząProjekt core otrzymuje zależność od co skrócić czas budowy projektu). biblioteki Commons Lang. Oba projekty interfejsu użytkownika są zależne Podsumowanie od projektu core. Następnie do projektu webdodany jest plugin War. W końcu, ...ah, czyżby kolejny projekt-zbawca ? Koprojekt swing zostaje skonfigurowany lejne narzędzie, które obiecuje rozwiązajako projekt Groovyego (co sprowadza nie wszelkich problemów związanych z się do dodania do niego pluginu Groovy budowaniem projektów ? Nie całkiem. i dopisania zależności od biblioteki Gro- Gradle nie obiecuje aż tak wiele. Za to próbuje ovyego). uczynić niemożliwe możliwym, możliwe łatwym, a łatwe eleganckim Budowa projektu - pełna i częściowa Moshé Feldenkrais W przypadku projektu wielomodułowego pojawia się pytanie “ale co właściwie chciałbyś zbudować?”. A więc, chciałbyś I nawet już teraz, przed wydaniem wersji zbudować oba projekty interfejsu użyt- 1.0, Gradle oferuje mnóstwo interesujących i przydatnych możliwości. W tym kownika, czy może tylko jeden z nich? artykule zaprezentowałem kilka z nich, Aby zbudować wszystkie projekty, wyko- i nie ukrywam, że jest to jedynie wierznaj komendę gradle build z poziomu chołek (wciąż rosnącej) góry lodowej. :) głównego katalogu projektu. Zauważysz, że w procesie budowania powstaną Chciałem zachęcić cię do zainwestotrzy artefakty: core-0.1.jar, swing- wania odrobiny czasu by lepiej poznać Gradle. Początkowo jego elastyczność i 0.1.jar i web-0.1.war. brak sztywnych reguł może sprawić, że • 43 ROZJAZD MASZYNOWNIA BOCZNICA używały pluginu Java, KONDUKTOR • POCZEKALNIA Gradle umożliwia wykonywanie częściowych buildów, a nam nie pozostaje nic innego jak z tej możliwości ochoczo korzystać DWORZEC GŁÓWNY Maszynownia ROZJAZD Maszynownia DWORZEC GŁÓWNY POCZEKALNIA KONDUKTOR BOCZNICA MASZYNOWNIA Przekonasz się że w tym projekcie tkwi potęga, o jakiej nawet nie marzyłeś. będziesz czuł się nieco nieswojo (szczególnie jeżeli jesteś doświadczonym użytkownikiem Mavena). W moim przypadku, nawet po kilku miesiącach pracy z Gradle, mam poczucie, że wciąż nie dokonałem jeszcze całkowitego “przeskoku myślowego”. Wciąż też zadziwia mnie łatwość i elastyczność, z jaką Gradle pozwala mi tworzyć skrypty budujące projekt. Musisz dać sobie nieco czasu, a przekonasz się że w tym projekcie tkwi potęga, o jakiej nawet nie marzyłeś. Koszt przejścia (z Anta lub Mavena) jest minimalizowany przez to, że Gradle wykorzystuje wiele rozwiązań, które są Ci dobrze znane z innych projektów (np. standardowy layout projektu, taski Anta, repozytoria Mavena itd.). Na stronie Gradle znajdziesz bardzo rozbudowany podręcznik użytkownika, a w kodzie źródłowym mnóstwo przykładów, które pomogą Ci rozpocząć pracę z Gra- dle (zajrzyj też do CookBook po różnorakie pomocne wskazówki). Możesz też liczyć na wsparcie członków społeczności (dostępna jest lista mailingowa i wiki), a w przypadku gdybyś potrzebował profesjonalnych szkoleń, zapoznaj się z ofertą na stronie Gradle Inc. Linki • http://gradle.org – strona projektu Gradle • http://maven.apache.org/plugins/maven-antrun-plugin/ - Maven AntRun Plugin • http://ant.apache.org - Ant • http://maven.apache.org - Maven • http://groovy.codehaus.org/Using+Ant+from+Groovy - użycie Anta ze skryptów Groovy Dla ścisłości dodam, że tak naprawdę to pluginy Java i War narzucają taki układ projektu jako domyślny. [1] Maven 3 pozwala na użycie równie zwięzłej składni, ale jak zobaczymy nieco później, w przypadku dependencji, Gradle oferuje o wiele więcej. [2] W załączonym do artykułu kodzie źródłowym znajdziesz obie wersje - sam oceń, która wydaje Ci się bardziej odpowiednia. [3] Plik settings.gradle może też służyć innym celom. [4] Inne taski o zbliżonym działaniu opisane są w podręczniku użytkownika Gradle. [5] 44 ROZJAZD Rozjazd public class ClassType { // definicja typu obj public static void main(String[] args) { Object pattern = ClassType. class; for (int i = 0; i < obj. length(); i++) { if (pattern.equals(obj)) obj += 5; else obj += -5; System.err.println(obj); } } } public class AccessingToOverridenAttribute { public int a; public void add() { a = 10; } public static void main(String[] args) { AccessingToOverridenAttribute f = new Bar(); f.add(); System.err.println(++f.a); System.err.println(++((Bar) f).a); } } Jeśli zadanie jest zbyt trudne, skopiuj klasę class Bar extends AccessingToOverdo swojego IDE. Pomoże Ci to znaleźć roz- ridenAttribute wiązanie. { public int a = 2; public void add() { a = 5; } } 45 KONDUKTOR Przykłd drugi W drugiej części (już bez pomocy IDE!) przypomnijmy sobie, czym grozi brak enkapsulacji oraz przesłanianie zmiennych. Co będzie wynikiem uruchomienia poniższej metody main()? POCZEKALNIA Przykład pierwszy W dzisiejszym cyklu zaczniemy od krótkiego kodu dla spostrzegawczych. Pytanie brzmi: zakładając, że poniższa klasa się kompiluje, a jej uruchomienie nie spowoduje rzucenie wyjątkiem, jakiej klasy jest referencja obj? DWORZEC GŁÓWNY Damian Szczepanik BOCZNICA MASZYNOWNIA Express Killers, cz. V ROZJAZD MASZYNOWNIA BOCZNICA KONDUKTOR POCZEKALNIA DWORZEC GŁÓWNY Rozjazd Express Killers, cz. V - odpowiedzi Damian Szczepanik Przykład pierwszy: Kluczem do zagadki jest metoda length() i operacje dodawania. Po pierwsze obj nie może być tablicą, gdyż w pętli użyto funkcji length(), a nie length. Oznacza to, że obj jest referencją do klasy, która implementuje metodę length(). Żaden wrapper typów prymitywnych jak Integer czy Double nie implementuje tej metody. Ma ją natomiast String. Czy jest możliwe, by była to inna klasa, którą użytkownik sam zaimplementował? Metoda length() nie stanowiłaby problemu, gdyż można ją zaimplementować. Natomiast operator dodawania jest dozwolony tylko dla typów prymitywnych, wraperów oraz wspomnianej klasy String. Java nie udostępnia mechanizmów przeciążania operatorów, jak to ma miejsce np w języku C++. Klasa String posiada modyfikator final, zatem nie można jej także rozszerzyć. Wobec powyższego jedyną możliwością, by powyższy kod się kompilował jest, by obiekt obj był referencją do klasy String. I nie był wartością null, gdyż to spowoduje wygenerowanie wyjątku podczas uruchomienia. Przykład drugi: Po pierwsze metoda add() zostanie wywołana zależnie od typu obiektu f, a nie deklaracji tej zmiennej. Zatem sterowanie zostanie przekazane do klasy Bar. Oznacza to, że to wartość atrybutu a klasy bazowej nie ulegnie zmianie. Po drugie użycie wartości f.a spowoduje wydrukowanie atrybutu wskazanego typem zmiennej niezależnie od tego, na co wskaże referencja. Zostanie to określone na etapie kompilacji i nie ma znaczenia, że klasa pochodna deklaruje atrybut o tej samej zmiennej. Nie warto zatem deklarować zmiennej o takiej samej nazwie, jak w klasie nadrzędnej i nie należy pozostawiać dostępu do tej zmiennej w sposób, który umożliwi użycie konstrukcji, jak we wspomnianym przykładzie. Po trzecie wydrukowanie zmiennej poprzez wywołanie ((Bar) f).a zadziała wg powyższej zasady: kompilator na etapie kompilacji wskaże atrybut klasy pochodnej, gdyż jawnie tego oczekiwano poprzez rzutowanie. Wnioski: atrybuty powinny być niewidoczne poza klasą i dostępne poprzez metody get i set, co skutecznie minimalizuje opisane powyżej problemy oraz ułatwia debugowanie. 46 ROZJAZD Więcej węgla Istnieją sytuacje, w których Maven przydaje się przy pracy Zakresy to jedna z konstrukcji z aplikacją Grails. Okazuje się, ułatwiająca programowanie że nic nie stoi na przeszkodzie, w Groovy, nieobecna w Jaaby przy użyciu grails-mavenvie. Przydają się one choćby plugin dodać go do nowego w pętlach for i w instrukcjach lub istniejącego projektu i zaswitch. W obszernym artykule rządzać nim przy pomocy mamożna przeczytać o ich implementacji, rodzajach i charakterystykach. venowskich ekwiwalentów poleceń Grails. Nie brakuje też przykładowych zastosowań. Plugin Corner – Bean-Fields Plugin Strony GSP czasami robią się mało czyGroovy around the Globe – Austra- telne. Powyższa wtyczka pozwala skrólia & New Zealand cić ilość kodu potrzebną do wyświetlenia choćby pola formularza. Znaczniki HTML, Autor: Steve Dalton miejsce wstawienia etykiety oraz samego Debiutujący cykl artykułów ma za zadanie pola zawarte są w szablonie, a my w naprzybliżać nam środowiska Groovy i Grails szym widoku w ich miejsce umieszczamy z całego świata, zaczynając od Australii i jedną linijkę. Poza istniejącymi szablonami Nowej Zelandii. Znajdziemy tu informacje mamy możliwość tworzenia własnych. o konferencjach i grupach użytkowników na kontynencie, poznamy też nazwiska i osiągnięcia ludzi z tamtych okolic zajmują- O autorze Student Informatyki na Wydziale Fizyki cych się Groovy i Grails. Uniwersytetu im. Adama Mickiewicza w Poznaniu. Enterprise Development with Groovy and Grails Kod promocyjny Autor: Jason Warner Dla czytelników JAVA exPress przygotowaJason Warner przedstawia historię nie- liśmy specjalny kod promocyjny „poland1”, wielkiej firmy migrującej z ColdFusion na dzięki któremu możecie pobrać jeden doGroovy i Grails. W żadnym miejscu nie wolny numer GroovyMag. Zapraszamy do ukrywając wad Grails pisze o powodach, zapoznania się z tym czasopismem. wyzwaniach i korzyściach wynikających z takiego kroku. 47 KONDUKTOR Grails and Maven – Differing Opinions, Same Goal Autor: Michael Wall POCZEKALNIA Groovy Under the Hood – Groovy Collective Types - Ranges Autor: Kirsten Schwark DWORZEC GŁÓWNY Krzysztof Konwisarz BOCZNICA MASZYNOWNIA Recenzja: GroovyMag 11/2009 ROZJAZD MASZYNOWNIA BOCZNICA KONDUKTOR POCZEKALNIA DWORZEC GŁÓWNY Konduktor Mistrz Programowania: Refaktoryzacja, cz. II Mariusz Sieraczkiewicz Dzisiaj kolejny odcinek książkiniespodzianki o refaktoryzacji. Mariusz Sieraczkiewicz zgodził się opublikować w odcinkach na łamach JAVA exPress swoją książkę “Jak całkowicie odmienić sposób programowania używając refaktoryzacji“. Jest to pierwsza książka z serii Mistrz Programowania i dotyczy... no tak - refaktoryzacji. Pierwsza część książki jest dostępna za darmo na stronie http://www.mistrzprogra- mowania.pl/. Tam także możesz zakupić pełną wersję, bez konieczności czekania 3 miesięcy na kolejną część w JAVA exPress. No i będziesz miał całość w jednym pdf-ie. W każdym razie zapraszam nawet jeśli możesz czekać. Wspomagajmy samych siebie. Może jutro Ty będziesz chciał coś sprzedać... A książka Mariusza jest warta swej ceny ;) Grzegorz Duda 48 ROZDZIAL 3. JAK UŻYWAĆ REFAKTORYZACJI DO TWORZENIA W PELNI OBIEKTOWYCH APLIKACJI Refaktoryzacja: Zmiana algorytmu na pisany ludzkim je zy- JAV Ae xP res s kiem Algorytm poszukiwania slowa można zapisać z użyciem nastepuj acych kroków: 1. inicjacja zmiennych i pobranie zawartości strony z tlumaczeniem 2. dla każdego znalezionego slowa bed cześci acego a tlumaczenia (a) znajdź polski odpowiednik (b) znajdź angielski odpowiednik (c) zapamietaj tlumaczenie dla (d) wypisz tlumaczenie na ekranie Powyższy zapis algorytmu odpowiada ludzkiemu rozumowaniu, zaś obecna wersja kodu jest zapisem programistycznego myślenia. Spróbujmy zatem zrefaktoryzować row ana kod, aby jego struktura odzwierciedlala przebieg opisany powyżej. Pierwszy krok, który zrobimy w tym kierunku, to wydzielenie metody odpowiedzialnej za cześć inicjacja zmiennych i pobranie zawartości strony z tlumaczeniem. Wynikiem tej metody bedzie strumień BufferedReader reprezentujacy analizowana strone HTML. Strumień jest obiektem potrzebnym w calym algorytmie, zatem bedzie polem w klasie SearchWord yge ne Service. Kod po refaktoryzacji wyglada nastepuj aco: package pl . bnsit . webdictionary ; java . io . BufferedReader ; java . io . IOException ; java . io . I n p u t S t r e a m R e a d e r ; java . net . M a l f o r m e d U R L E x c e p t i o n ; java . net . URL ; java . util . ArrayList ; java . util . Date ; java . util . List ; java . util . regex . Matcher ; java . util . regex . Pattern ; We rsja w import import import import import import import import import import public class S e a r c h W o r d S e r v i c e { private String command = null ; private BufferedReader bufferedReader = null ; public S e a r c h W o r d S e r v i c e ( String command ) { 60 R Mistrz Programowania BNS IT, http://www.bnsit.pl REFAKTORYZACJA: ZMIANA ALGORYTMU NA PISANY LUDZKIM JEZYKIEM this . command = command ; s } String polishWord = null ; String englishWord = null ; int counter = 1; JAV Ae xP res public List < DictionaryWord > search () { List < DictionaryWord > result = new ArrayList < DictionaryWord >() ; try { bufferedReader = p r e p a r e B u f f e r e d R e a d e r () ; boolean polish = true ; String line = bufferedReader . readLine () ; // .. dalej kod bez zmian dla return result ; } row ana private BufferedReader p r e p a r e B u f f e r e d R e a d e r () throws IOException , MalformedURLException { String [] commandParts = command . split ( " " ) ; String wordToFind = commandParts [1]; String urlString = " http :// www . dict . pl / dict ? word = " + wordToFind + " & words =& lang = PL " ; } yge ne return new BufferedReader ( new I n p u t S t r e a m R e a d e r ( new URL ( urlString ) . openStream () ) ) ; } We rsja w private boolean hasNextLine ( String line ) { return ( line != null ) ; } W ten sposób zlożona metoda search uprościla sie nieco poprzez wydzielenie metody skladowej prepareBufferedReader. Metoda ta zwraca jako wynik stworzony strumień do odczytu zawartości strony, jednocześnie strumień ten jest polem klasy. Może zatem paść propozycja, aby metoda ta nie zwracala żadnego wyniku, tylko ustawiala to pole. Jest to opcja poprawna, natomiast ja osobiście preferuje zwracanie wyniku, gdyż dzieki temu uzyskujemy jawna informacje o efekcie dzialania metody. Przyjrzyjmy sie wersji z niejawnym ustawieniem pola bufferedReader. 61 R Mistrz Programowania BNS IT, http://www.bnsit.pl ROZDZIAL 3. JAK UŻYWAĆ REFAKTORYZACJI DO TWORZENIA W PELNI OBIEKTOWYCH APLIKACJI JAV Ae xP res s // ... public List < DictionaryWord > search () { List < DictionaryWord > result = new ArrayList < DictionaryWord >() ; String polishWord = null ; String englishWord = null ; int counter = 1; try { init () ; // ... } dla private void init () throws IOException , MalformedURLException { String [] commandParts = command . split ( " " ) ; String wordToFind = commandParts [1]; String urlString = " http :// www . dict . pl / dict ? word = " + wordToFind + " & words =& lang = PL " ; row ana bufferedReader = new BufferedReader ( new I n p u t S t r e a m R e a d e r ( new URL ( urlString ) . openStream () ) ) ; } Wywolanie init() nie daje żadnych informacji o tym, że w ciele tej metody nastepuje zmiana stanu, co wyraźnie widać w poprzednim przykladzie bufferedReader yge ne = prepareBufferedReader(). Jest do doslowny zapis celu dzialania metody, którym jest przygotowanie obiektu typu BufferedReader, a ten z kolei zostanie zapamietany jako pole w klasie. pujaco: We rsja w Zajmiemy sie glówna cześci a algorytmu, która teraz przedstawia sie obecnie naste // ... boolean polish = true ; String line = bufferedReader . readLine () ; while ( hasNextLine ( line ) ) { Pattern pat = Pattern . compile ( " .* < a href =\" dict \\? words ?=(.*) & lang .* " ) ; Matcher matcher = pat . matcher ( line ) ; if ( matcher . find () ) { String foundWord = matcher . group ( matcher . groupCount () ) ; 62 R Mistrz Programowania BNS IT, http://www.bnsit.pl REFAKTORYZACJA: ZMIANA ALGORYTMU NA PISANY LUDZKIM JEZYKIEM JAV Ae xP res s if ( polish ) { System . out . print ( counter + " ) " + foundWord + " = > " ) ; polishWord = new String ( foundWord . getBytes () , " UTF8 " ) ; polish = false ; } else { System . out . println ( foundWord ) ; polish = true ; englishWord = new String ( foundWord . getBytes () , " UTF8 " ) ; result . add ( new DictionaryWord ( polishWord , englishWord , new Date () ) ) ; counter ++; } dla } row ana line = bufferedReader . readLine () ; } } catch ( M a l f o r m e d U R L E x c e p t i o n ex ) { // ... Taki zapis jest bardzo zawily, jest sporo zmiennych tymczasowych: polish — do przechowywania informacji o bieżacej wersji jezykowej, polishWord, englishWord — yge ne zmienne przechowujace tymczasowo wartości wersji polskiej i angielskiej slowa. Zatrzymajmy sie na chwile. Jeśli przyjrzymy sie jeszcze raz strukturze strony HTML, to zauważymy, że slowo polskie i angielskie podlega analizie w identyczny sposób. Slowa te wystepuj a naprzemiennie — najpierw slowo polskie, później slowo angielskie. Dlaczegóżby zatem nie uprościć nieco algorytmu? Wydzielmy metode, która b edzie We rsja w potrafila znaleźć kolejny wyraz (polski lub angielski). Użyjemy jej do naprzemiennego znajdowania wyrazów polskiego i angielskiego. Wyszukiwanie kolejnego slowa oprzemy na istniejacym już algorytmie z powyższego kodu źródlowego. Wydzielona metode nazwijmy moveToNextWord, bedzie zwracać kolejny znaczacy wyraz polski lub angielski znajdujacy sie na stronie HTML lub null, jeśli wszystkie slowa zostaly odnalezione. Glównym celem tej metody jest stworzenie mechanizmu do odnajdywania kolejnych znalezionych wyrazów na stronie HTML. private String moveToNextWord () { try { String line = bufferedReader . readLine () ; 63 R Mistrz Programowania BNS IT, http://www.bnsit.pl ROZDZIAL 3. JAK UŻYWAĆ REFAKTORYZACJI DO TWORZENIA W PELNI OBIEKTOWYCH APLIKACJI while ( hasNextLine ( line ) ) { JAV Ae xP res s Pattern pattern = Pattern . compile ( " .* < a href =\" dict \\? words ?=(.*) & lang .* " ) ; Matcher matcher = pattern . matcher ( line ) ; if ( matcher . find () ) { String foundWord = matcher . group ( matcher . groupCount () ) ; return new String ( foundWord . getBytes () , " UTF8 " ) ; } else { line = bufferedReader . readLine () ; } dla } } catch ( IOException e ) { // TODO obsluzyc blad } return null ; row ana } Refaktoryzacja: Wprowadzenie klarownej obslugi wyjatków // ... yge ne Na chwile chcialbym sie zatrzymać nad obsluga wyjatków. Do tej pory nie poświecali śmy temu tematowi zbyt wiele czasu. Odruchowo stosowaliśmy schemat: We rsja w } catch ( IOException ex ) { ex . p rint Sta ckTr ace () ; } // ... Dlawienie wyjatków W przypadku wystapienia sytuacji wyjatkowej, program wprawdzie wypisze informacje o stosie wywolań w momencie wystapienia wyjatku, jednak nie jest w żaden sposób przygotowany na jego obsluge. to nieco lepsza odmiana techniki Powyższe rozwiazanie 64 R Mistrz Programowania BNS IT, http://www.bnsit.pl REFAKTORYZACJA: WPROWADZENIE KLAROWNEJ OBSLUGI WYJATK ÓW zwanej dlawieniem wyjatków, która w 99% przypadków jest niedopuszczalna. Wyglada s ona mniej wiecej tak: JAV Ae xP res // ... } catch ( IOException ex ) { // nic nie robie } // ... Takie posuniecie spowoduje, że w momencie wystapienia bledu nic sie nie sta nie z aplikacja, a nie jest to pożadany efekt. Blad niezauważony. Na sytuacje b edzie wyjatkow a w jakiś sposób należy zareagować, być może przedstawić jakiś komunikat dla użytkownikowi, być może dokonać próby wznowienia operacji. Coś należy zrobić. Ważne Jeśli wystapi nie należy udawać, że nic sie nie stalo. Należy za wyjatek, reagować na sytuacje wyjatkow a. Inaczej w systemie zginie ważna infor row ana macja, która jest trudna do odtworzenia w przypadku analizy lub naprawy systemu. Jest kilka możliwości reakcji. 1. Zareagować natychmiast w bloku catch. zrób to. yge ne Jeśli tylko jesteś w stanie sensownie zareagować w miejscu wystapienia wyjatku, We rsja w 2. Przerzucić wyjatek do metody wywolujacej (throws w sygnaturze). Jeśli nie jesteś w stanie obslużyć wyjatku lub z pewnego powodu nie chcesz tego zrobić, możesz przerzucić wyjatek do metody wywolujacej dana metode; opcja ta może mieć sens przy stosowaniu refaktoryzacji Wydzielenie metody. 3. Opakować wyjatek lub wygenerować wlasny wyjatek kontrolowany (ang. chec ked) dziedziczacy po java.lang.Exception. Jeśli wyjatek powinien mieć wplyw na inna cześć systemu (np. na interfejs użyt kownika) i ta cześć systemu powinna być przygotowana na jego obsluge, rzuć wlasny wyjatek lub opakuj nim wyjatek źródlowy. 4. Opakować wyjatek lub wygenerować wlasny wyjatek niekontrolowany (ang. un checked) dziedziczacy po java.lang.RuntimeException. 65 R Mistrz Programowania BNS IT, http://www.bnsit.pl ROZDZIAL 3. JAK UŻYWAĆ REFAKTORYZACJI DO TWORZENIA W PELNI OBIEKTOWYCH APLIKACJI Jeśli wyjatek jest bledem, na który system w sposób bezpośredni nie bedzie re agować lub nie jest w stanie reagować, rzuć wyjatek niekontrolowany lub opakuj JAV Ae xP res s nim wyjatek źródlowy. wystapi wtedy, Wróćmy do przykladu. W metodzie moveToNextWord wyjatek kiedy pojawia sie problemy podczas pracy ze strumieniem. Jest to sytuacja, co do której nie przewidujemy bezpośredniej obslugi w naszym przypadku, dlatego zastosujemy czwarta opcje obslugi wyjatku — stworzymy wlasna klase wyjatku o nazwie WebDictionaryException dziedziczac a z java.lang.RuntimeException. W przypadku gdy blad program zostanie przerwany. Jeśli chcemy reagować na te sytu wystapi, acje po stronie interfejsu użytkownia, w metodzie WebDictionary.main, WebDictionary.processMenu lub WebDictionary.searchWord możemy dodać obsluge tego dla wyjatku. Przypomne tylko, iż obsluga wyjatków dziedziczacych po RuntimeExcep tion nie jest wymuszana przez kompilator — nie ma konieczności ich deklaracji w throws. row ana Klasa wyjatku bedzie wygladać nastepuj aco: package pl . bnsit . webdictionary ; public class W e b D i c t i o n a r y E x c e p t i o n extends Ru n ti m eE x ce p ti o n { public W e b D i c t i o n a r y E x c e p t i o n () { } yge ne public W e b D i c t i o n a r y E x c e p t i o n ( String arg0 ) { super ( arg0 ) ; } We rsja w public W e b D i c t i o n a r y E x c e p t i o n ( Throwable arg0 ) { super ( arg0 ) ; } public W e b D i c t i o n a r y E x c e p t i o n ( String arg0 , Throwable arg1 ) { super ( arg0 , arg1 ) ; } } Zaś obsluga wyjatku wyglada nastepuj aco: } catch ( IOException e ) { throw new W e b D i c t i o n a r y E x c e p t i o n ( e ) ; } 66 R Mistrz Programowania BNS IT, http://www.bnsit.pl REFAKTORYZACJA: ZMIANA NAZWY METODY Niby nic wielkiego sie nie stalo, jednak wyjatek zostal odpowiednio przygotowany — jeśli zdarzy sie w systemie coś niepożadanego, na pewno pojawi sie o tym odpowiednia JAV Ae xP res s informacja. Refaktoryzacja: Zmiana nazwy metody Wróćmy do naszej metody moveToNextWord. Teraz możemy ja w pelni wykorzystać, aby uprościć algorytm. public List < DictionaryWord > search () { List < DictionaryWord > result = new ArrayList < DictionaryWord >() ; dla try { bufferedReader = p r e p a r e B u f f e r e d R e a d e r () ; String currentWord = moveToNextWord () ; int counter = 1; row ana while ( hasNextWord ( currentWord ) ) { // ... private boolean hasNextWord ( String word ) { return ( word != null ) ; } yge ne W ten sposób ustawiamy sie na pierwszym slowie znalezionym wśród wierszy strony HTML. Zauważmy, że dzieki takiemu zabiegowi, przenosimy nasz algorytm na inny poziom abstrakcji. Dzieki wydzieleniu metody moveToNextWord nie musimy już funkcjonować na niskim poziomie pojedynczych wierszy HTML, a poruszamy sie po kolejnych while. ja w slowach, które gdzieś na tej stronie sie znajduja. Jest to ogromna zmiana jakościowa, która znaczaco upraszcza konstrukcje algorytmu. Spójrzmy jeszcze na argument petli We rs while ( hasNextWord ( currentWord ) ) { Ponieważ glównym elementem napedowym algorytmu sa teraz slowa a nie wiersze, toteż dokonaliśmy bardzo prostej, ale jakże poteżnej w skutkach refaktoryzacji Zmiana nazwy metody. Poprzednia nazwa (hasNextLine) po wprowadzonych zmianach nie odzwierciedla już odpowiedzialności metody, dlatego wprowadziliśmy nowa nazwe hasNextWord. 67 R Mistrz Programowania BNS IT, http://www.bnsit.pl ROZDZIAL 3. JAK UŻYWAĆ REFAKTORYZACJI DO TWORZENIA W PELNI OBIEKTOWYCH APLIKACJI JAV Ae xP res ostatecznie dodawany do wynikowej listy i wypisywany na konsoli. s Dalsza cześć algorytmu jest bardzo prosta — w petli wyszukujemy polskie i an gielskie slowo umieszczajac je w obiekcie klasy DictionaryWord, który to obiekt jest // ... while ( hasNextWord ( currentWord ) ) { DictionaryWord dictionaryWord = new DictionaryWord () ; dictionaryWord . setPolishWord ( currentWord ) ; currentWord = moveToNextWord () ; dictionaryWord . setEnglishWord ( currentWord ) ; currentWord = moveToNextWord () ; result . add ( dictionaryWord ) ; dla System . out . println ( counter + " ) " + dictionaryWord . getPolishWord () + " = > " + dictionaryWord . getEnglishWord () ) ; counter = counter + 1; } // ... powyższego kodu. Dopóki jest dostepne slowo: row ana Zapis ten upraszcza algorytm, który możemy niemalże natychmiast odczytać z • stwórz nowy obiekt klasy DictionaryWord, yge ne slowo jako polska wersje slowa, • zapisz bieżace • przejdź do kolejnego slowa, slowo jako angielska wersje slowa, • ustaw bieżace We rsja w • przejdź do kolejnego slowa, • do listy dodaj nowe tlumaczenie (obiekt klasy DictionaryWord), • wyświetl tlumaczenie na ekranie, licznik tlumaczeń. • zwieksz Kod czyta sie niczym ksiażk e. Kod calej zrefaktoryzowanej klasy SearchWordService znajduje sie poniżej. Porównaj ten kod z kodem poczatkowym. Nie jest on ko dem krótszym, ale jest kodem zdecydowanie prostszym. Bez wiekszego wysilku można go przeanalizować i wywnioskować, o co chodzi. 68 R Mistrz Programowania BNS IT, http://www.bnsit.pl REFAKTORYZACJA: ZMIANA NAZWY METODY java . io . BufferedReader ; java . io . IOException ; java . io . I n p u t S t r e a m R e a d e r ; java . net . M a l f o r m e d U R L E x c e p t i o n ; java . net . URL ; java . util . ArrayList ; java . util . List ; java . util . regex . Matcher ; java . util . regex . Pattern ; public class S e a r c h W o r d S e r v i c e { JAV Ae xP res import import import import import import import import import s package pl . bnsit . webdictionary ; private String command = null ; private BufferedReader bufferedReader = null ; dla public S e a r c h W o r d S e r v i c e ( String command ) { this . command = command ; } row ana public List < DictionaryWord > search () { List < DictionaryWord > result = new ArrayList < DictionaryWord >() ; yge ne try { bufferedReader = p r e p a r e B u f f e r e d R e a d e r () ; String currentWord = moveToNextWord () ; int counter = 1; ja w while ( hasNextWord ( currentWord ) ) { DictionaryWord dictionaryWord = new DictionaryWord () ; dictionaryWord . setPolishWord ( currentWord ) ; currentWord = moveToNextWord () ; dictionaryWord . setEnglishWord ( currentWord ) ; currentWord = moveToNextWord () ; We rs result . add ( dictionaryWord ) ; System . out . println ( counter + " ) " + dictionaryWord . getPolishWord () + " = > " + dictionaryWord . getEnglishWord () ) ; counter = counter + 1; } } catch ( M a l f o r m e d U R L E x c e p t i o n ex ) { ex . p rin tSta ckTr ace () ; } catch ( IOException ex ) { ex . p rin tSta ckTr ace () ; 69 R Mistrz Programowania BNS IT, http://www.bnsit.pl return result ; } JAV Ae xP res } finally { try { if ( bufferedReader != null ) { bufferedReader . close () ; } } catch ( IOException ex ) { ex . p rin tSta ckTr ace () ; } } s ROZDZIAL 3. JAK UŻYWAĆ REFAKTORYZACJI DO TWORZENIA W PELNI OBIEKTOWYCH APLIKACJI while ( hasNextWord ( line ) ) { dla private String moveToNextWord () { try { String line = bufferedReader . readLine () ; Pattern pattern = Pattern . compile ( " .* < a href =\" dict \\? words ?=(.*) & lang .* " ) ; row ana Matcher matcher = pattern . matcher ( line ) ; if ( matcher . find () ) { String foundWord = matcher . group ( matcher . groupCount () ) ; return new String ( foundWord . getBytes () , " UTF8 " ) ; } else { line = bufferedReader . readLine () ; } We rsja w return null ; } yge ne } } catch ( IOException e ) { throw new W e b D i c t i o n a r y E x c e p t i o n ( e ) ; } private BufferedReader p r e p a r e B u f f e r e d R e a d e r () throws IOException , MalformedURLException { String [] commandParts = command . split ( " " ) ; String wordToFind = commandParts [1]; String urlString = " http :// www . dict . pl / dict ? word = " + wordToFind + " & words =& lang = PL " ; return new BufferedReader ( new I n p u t S t r e a m R e a d e r ( new URL ( urlString ) . openStream () ) ) ; } 70 R Mistrz Programowania BNS IT, http://www.bnsit.pl REFAKTORYZACJA: ZMIANA NAZWY METODY s private boolean hasNextWord ( String word ) { return ( word != null ) ; } JAV Ae xP res } Kod, który stworzyliśmy jest dużo czytelniejszy. Jest już calkiem niezly i do zaakceptowania. W tym momencie warto zrobić sobie chwile przerwy, aby celebrować osiagni ecie. Ważne We rs ja w yge ne row ana dla Każde zwyciestwo należy celebrować. Tylko wtedy jesteśmy w stanie w pelni doświadczyć zwyciestwa. 71 R Mistrz Programowania BNS IT, http://www.bnsit.pl