- JAVA exPress

advertisement
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 slowa można zapisać z użyciem nastepuj
acych
kroków:
1. inicjacja zmiennych i pobranie zawartości strony z tlumaczeniem
2. dla każdego znalezionego slowa bed
cześci
acego
a tlumaczenia
(a) znajdź polski odpowiednik
(b) znajdź angielski odpowiednik
(c) zapamietaj
tlumaczenie
dla
(d) wypisz tlumaczenie 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 odzwierciedlala 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 calym 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 zawily, 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 slowa.
Zatrzymajmy sie na chwile.
Jeśli przyjrzymy sie jeszcze raz strukturze strony HTML,
to zauważymy, że slowo polskie i angielskie podlega analizie w identyczny sposób.
Slowa te wystepuj
a naprzemiennie — najpierw slowo polskie, później slowo angielskie.
Dlaczegóżby zatem nie uprościć nieco algorytmu? Wydzielmy metode,
która b edzie
We
rsja
w
potrafila znaleźć kolejny wyraz (polski lub angielski). Użyjemy jej do naprzemiennego
znajdowania wyrazów polskiego i angielskiego.
Wyszukiwanie kolejnego slowa oprzemy na istniejacym
już algorytmie z powyższego
kodu źródlowego. Wydzielona metode nazwijmy moveToNextWord, bedzie
zwracać
kolejny znaczacy
wyraz polski lub angielski znajdujacy
sie na stronie HTML lub null,
jeśli wszystkie slowa zostaly odnalezione. Gló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 bledem,
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ódlowy.
wystapi
wtedy,
Wróćmy do przykladu. W metodzie moveToNextWord wyjatek
kiedy pojawia sie problemy podczas pracy ze strumieniem. Jest to sytuacja, co do
której nie przewidujemy bezpośredniej obslugi w naszym przypadku, dlatego zastosujemy czwarta opcje obslugi wyjatku
— stworzymy wlasna klase wyjatku
o nazwie
WebDictionaryException dziedziczac
a z java.lang.RuntimeException. W przypadku gdy blad
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ć obsluge tego
dla
wyjatku.
Przypomne tylko, iż obsluga 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ś obsluga 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 stalo, jednak wyjatek
zostal 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 pelni 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 slowie 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
slowach, 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ż glównym elementem napedowym
algorytmu sa teraz slowa 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 s€lowo 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
s€lowo:
row
ana
Zapis ten upraszcza algorytm, który możemy niemalże natychmiast odczytać z
• stwórz nowy obiekt klasy DictionaryWord,
yge
ne
s€lowo jako polska wersje s€lowa,
• zapisz bieżace
• przejdź do kolejnego s€lowa,
s€lowo jako angielska wersje s€lowa,
• ustaw bieżace
We
rsja
w
• przejdź do kolejnego s€lowa,
• do listy dodaj nowe t€lumaczenie (obiekt klasy DictionaryWord),
• wyświetl t€lumaczenie na ekranie,
licznik t€lumaczeń.
• zwieksz
Kod czyta sie niczym ksiażk
e.
Kod ca€lej 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
wysi€lku 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ż ca€lkiem niez€ly 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
pe€lni doświadczyć zwyciestwa.
71
R
Mistrz Programowania
BNS IT, http://www.bnsit.pl
Download