28 czerwca 2009

Z rozdziału 8. o Groovy SDK z "Programming Groovy"

Rozdział 8. "Exploring the GDK" rozpoczyna część drugą zatytułowaną "Using Groovy". Do tej pory autor koncentrował swoją uwagę na składni języka i wszystkim, co było w zasadzie konieczne, aby przykuć uwagę czytelnika i zachęcić go do dalszej lektury (w efekcie poznania Groovy). Należało w końcu wpłynąć na twarde (=statyczne i silnie typowane) myślenie javowe i pokazać urok Groovy. Nie, nie jest i nie będzie to początek moich peanów nt. Groovy i wskazywanie go jako jedynego słusznego języka programowania. Odnoszę jednak wrażenie, że wielu z Was z pewnością poczuło ten dreszczyk emocji poznania nowego języka (Grails raz jeszcze), który oferuje możliwości Javy w połączeniu z elementami języków dynamicznych i funkcyjnych. W końcu warto uczyć się nowych języków, aby nasze myślenie było bardziej elastyczne i posiadało umiejętność dopasowania się do ciągłych zmian w projekcie, w życiu, gdziekolwiek. W końcu i przysłowiowym młotkiem można wiele zdziałać, ale do krojenia chleba znacznie lepiej skorzystać z noża (było też o tym w książce "Pragmatyczny programista - od czeladnika do mistrza", ale Krzysiek o tym nie wspomina - Pragmatyczny programista). Do niedawna sądziłem, że poznawanie nowego języka jest dobre, ale nie było ku temu jakiś wyraźnych powodów, aby pomysł zrealizować. Kiedy jednak się udało i mogę powiedzieć, że choć trochę liznąłem Groovy, widzę, ile radości mi to sprawiło. Największą zaletą Groovy jest możliwość niedalekiego odejścia od JVM. Wciąż z niej korzystam i to daje mi możliwość czerpania z Groovy tylko tyle, ile potrzebuję i w czym czuję się pewniej z jednoczesnym utrzymaniem tych samych narzędzi programistycznych. Owa bariera poznawcza (aka krzywa uczenia) stała się dzięki temu niewielka i wymaga(ła) jedynie poznania kilku konstrukcji językowych. Czyż tego samego nie oczekują od nas szkielety aplikacyjne? Potraktujmy Groovy jako jeden z wielu szkieletów aplikacyjnych i stosujmy właściwie do okoliczności. Nic na siłę.

Początek rozdziału to kolejne obietnice wręcz natychmiastowego zwiększenia produktywności z Groovy, bo nie tylko, że mamy większą dynamikę języka, ale i rozszerzone biblioteki bazowe Javy (Java API). Metody znane z relacji rozdziału o kolekcjach (Z rozdziału 7. o kolekcjach (listach) z "Programming Groovy" oraz Dokończenie rozdziału 7. "Working with Collections" z "Programming Groovy") - each(), collect(), find(), findAll(), any() oraz every() są faktycznie dostępne w każdej klasie javowej. To zdecydowanie zmienia nasze postrzeganie, czego możnaby oczekiwać więcej od java.lang.Object. Niektórych przyprawi to o ból głowy, że i sama Java wymaga wiele nauki, a tu proszę, Groovy jeszcze dodaje swoje trzy grosze. Nie ma lekko. Jeśli pozwolimy sobie na większą elastyczność jest pewnym, że członkowie zespołu projektowego podziękują nam za inspirujące pomysły (wierzę, że poznanie jakiegokolwiek nowego języka to wkroczenia na teren niedostępny innym, którzy nim nie władają - czy to język mówiony, czy programowania).

Nowymi metodami, których autor wcześniej nie przedstawiał, są dump() oraz inspect().
 groovy:000> class Osoba {
groovy:001> String imie, nazwisko
groovy:002> }
===> true
groovy:000> jacek = new Osoba(imie: 'Jacek', nazwisko: 'Laskowski')
===> Osoba@1382926
groovy:000> jacek.dump()
===> <Osoba@1382926 imie=Jacek nazwisko=Laskowski>
groovy:000> jacek.inspect()
===> Osoba@1382926
Za pomocą dump() możemy spojrzeć na strukturę i dane egzemplarza lub też samego typu. Wszystko, co może być przydatne w trakcie śledzenia działania aplikacji, odnotowania w logach (dziennikach zdarzeń) czy po prostu nauki. Metoda inspect() pozwala nam poznać, co jest potrzebne do utworzenia obiektu danego typu. Jak na razie nie zachwyciła mnie (pewnie jeszcze nie zrozumiałem sensu jej istnienia).

Miłośnicy JavaScript czy VBScript będą uradowani dowiadując się, że znana im metoda with() jest również dostępna w Groovy. Służy ona do określenia kontekstu wykonania metod w ramach przekazanego do niej domknięcia (przypomina mi to maćkowe dywagacje w Ograniczony kontekst). Zamiast serii metod na danym egzemplarzu, co w Javie wymagałoby podawania go każdorazowo, w Groovy można krócej. Jej aliasem jest identity() i wywołanie obu zwraca ten sam rezultat. Przykład od razu wyjaśni o co chodzi.
 groovy:000> lista = [1,2]
===> [1, 2]
groovy:000> lista.add(3)
===> true
groovy:000> lista.add(4)
===> true
groovy:000> lista
===> [1, 2, 3, 4]
groovy:000> println lista.size()
4
===> null
groovy:000> println lista.contains(2)
true
===> null
groovy:000> // a teraz prosciej
===> true
groovy:000> nowaLista = [1,2]
===> [1, 2]
groovy:000> nowaLista.with {
groovy:001> add(3)
groovy:002> add(4)
groovy:003>
groovy:003> println size()
groovy:004> println contains(2)
groovy:005> }
4
true
===> null
groovy:000> nowaLista
===> [1, 2, 3, 4]
Zamiast każdorazowego określania egzemplarza, który ma być odbiorcą wywołania serii metod, wystarczy with() z domknięciem. W ramach tego domknięcia wszystkie metody będą kierowane do tego samego obiektu - obiektu, na którym wywołaliśmy metodę with() (lub identity()). Działanie with() polega na właściwym ustawieniu delegate domknięcia.

Metoda sleep() jest udoskonaleniem znanej z Java API metody Thread.sleep(). Ignoruje ona przerwania (wywołanie Thread.interrupt()) przez zadany czas. Parametrem wejściowym sleep() jest domknięcie, które obsługuje przerwanie i które otrzymuje na wejściu InterruptedException. Jeśli domknięcie zwróci false wykonanie sleep() będzie trwało do zadanego czasu, jakby przerwanie nie nastąpiło.
 groovy:000> def prezentacjaSleep(flag) {
groovy:001> thread = Thread.start {
groovy:002> println "Watek rozpoczal sie"
groovy:003> poczatek = System.nanoTime()
groovy:004> new Object().sleep(3000) {
groovy:005> println "Obsluga przerwania z wyjatkiem: " + it
groovy:006> flag
groovy:007> }
groovy:008> koniec = System.nanoTime()
groovy:009> println "Koniec pracy watku - patrz na czasy wykonania - ${(koniec - poczatek)/10**9} sekund"
groovy:010> }
groovy:011> thread.interrupt()
groovy:012> thread.join()
groovy:013> }
===> true
groovy:000> prezentacjaSleep(true)
Watek rozpoczal sie
Obsluga przerwania z wyjatkiem: java.lang.InterruptedException: sleep interrupted
Koniec pracy watku - patrz na czasy wykonania - 0.001169143 sekund
===> null
groovy:000> prezentacjaSleep(false)
Watek rozpoczal sie
Obsluga przerwania z wyjatkiem: java.lang.InterruptedException: sleep interrupted
Koniec pracy watku - patrz na czasy wykonania - 3.00024193 sekund
===> null
Zwróć uwagę na czasy wykonania wątków (oraz nową metodę w klasie Thread start()).

Za pomocą operatora tablicowego [] (obsługiwany przez metodę getAt() lub putAt() w zależności od trybu - odczyt/zapis) możemy dynamicznie dostać się do właściwości obiektu. Zamiast statycznego (w sensie podania go w trakcie pisania tej konstrukcji, a nie kwalifikatora static) wywołania egzemplarz.wlasciwosc, można również dynamicznie - egzemplarz['wlasciwosc']. Zaletą takiego wywołania jest możliwość odczytu/zapisu właściwości nie znając jej nazwy podczas pisania aplikacji, np. po określeniu jej przez użytkownika.
 groovy:000> jacek.dump()
===> <Osoba@1382926 imie=Jacek nazwisko=Laskowski>
groovy:000> jacek.imie
===> Jacek
groovy:000> jacek.nazwisko
===> Laskowski
groovy:000> dostepneWlasciwosci = ['imie', 'nazwisko']
===> [imie, nazwisko]
groovy:000> dostepneWlasciwosci.each { nazwa ->
groovy:001> println "${nazwa} = ${jacek[nazwa]}"
groovy:002> }
imie = Jacek
nazwisko = Laskowski
===> [imie, nazwisko]
groovy:000> jacek[dostepneWlasciwosci[0]] = 'JACEK'
===> JACEK
groovy:000> jacek.dump()
===> <Osoba@1382926 imie=JACEK nazwisko=Laskowski>
Wystarczy teraz użyć kolejnej metody getProperties() (albo prościej properties), aby dostać się do atrybutów danego typu i wykonać na nich pewne czynności.
 groovy:000> jacek.properties.each { prop -> println "$prop" }
class=class Osoba
imie=JACEK
nazwisko=Laskowski
metaClass=org.codehaus.groovy.runtime.HandleMetaClass@cf3539[groovy.lang.MetaClassImpl@cf3539[class Osoba]]
===> {class=class Osoba, imie=JACEK, nazwisko=Laskowski,
metaClass=org.codehaus.groovy.runtime.HandleMetaClass@cf3539[groovy.lang.MetaClassImpl@cf3539[class Osoba]]}
Oczywiście w samej Javie też to jest możliwe z wykorzystaniem Reflection API, ale prostota takiego podejścia jest nie do przecenienia. Do tego należy dodać, że wykonanie metod dynamicznie jest równie proste z invokeMethod() (autor dosyć sarkastycznie wprowadza w temat i szkoda byłoby zepsuć Wam poznanie tego osobiście - zapraszam na stronę 139. książki).
 groovy:000> jacek.metaClass.pewnaMetodaZParametrami { String p1, int p2 -> println "Podano p1=$p1 i p2=$p2" }
===> null
groovy:000> jacek.invokeMethod("pewnaMetodaZParametrami", ["Pierwszy", 2] as Object[])
Podano p1=Pierwszy i p2=2
===> null
Wszystkie wymienione metody są dostępne dla java.lang.Object, a tym samym i dla wszystkich typów.

W klasie Process dodano trzy atrybuty - in, out oraz err. Dają one dostęp do, odpowiednio, strumieni wejściowych, wyjściowych i błędów. Mamy również atrybut text, który zwraca całe wyjście z procesu. Odczyt standardowego strumienia błędów to err.text.
 groovy:000> proces = "uname -a".execute()
===> java.lang.ProcessImpl@f9104a
groovy:000> proces.in.text
===> CYGWIN_NT-5.1 work 1.5.25(0.156/4/2) 2008-06-12 19:34 i686 Cygwin
Możemy to samo otrzymać z proces.text. Pamiętajmy, że po wykonaniu polecenia proces kończy się i strumień zostaje zamknięty, więc ponowny odczyt kończy się zgłoszeniem wyjątku.
 groovy:000> proces = "uname -a".execute()
===> java.lang.ProcessImpl@486cdd
groovy:000> proces.text
===> CYGWIN_NT-5.1 work 1.5.25(0.156/4/2) 2008-06-12 19:34 i686 Cygwin

groovy:000> proces.text
ERROR java.io.IOException: Stream closed
at groovysh_evaluate.run (groovysh_evaluate:2)
...
Komunikacja z procesem możliwa jest z operatorem lewego przesunięcia <<, np.
 groovy:000> p = "wc".execute()
===> java.lang.ProcessImpl@956254
groovy:000> p.out.withWriter {
groovy:001> it << "wc zlicza liczbe liter, slow i zdan\n"
groovy:002> it << "wiec powinnismy dostac 2 zdania i in.\n"
groovy:003> }
===> java.io.OutputStreamWriter@4d93e3
groovy:000> println p.text
2 14 74

===> null
Wykonanie poleceń z parametrami to wykonanie execute() na liście napisów, gdzie pierwszy z nich jest poleceniem, a reszta parametrami.
 groovy:000> command = ['c:/apps/groovy/bin/groovy.bat', '--version']
===> [c:/apps/groovy/bin/groovy.bat, --version]
groovy:000> command.execute().text
===> Groovy Version: 1.6.3 JVM: 1.6.0_14
Dobre, co?

W Thread widzieliśmy już dodatek w postaci metody start(), która akceptuje domknięcie - zbiór metod uruchamianych w osobnym wątku. Jeśli chcemy stworzyć wątek-demona korzystamy z Thread.startDeamon() z domknięciem na wejściu. Tutaj autor przeszedł samego siebie z wyjaśnieniem różnicy między wątkiem normalnym a demonem (patrz przypis na stronie 142):

"A deamon thread quits if there are no active nondeamon threads currently running - kind of like employees who work only when the boss is around"

A Tyś demonem? :)

W pakiecie java.io również kilka rozszerzeń Groovy. W klasie File mamy eachFile() oraz eachDir() z ich wariacjami, które akceptują domknięcia do nawigacji w systemie plików. Odczyt zawartości pliku, dowolnego obiektu typu Reader czy InputStream to po prostu odczyt atrybutu text.
 groovy:000> f = File.createTempFile("temp", ".txt")
===> c:\temp\temp1698516868251566991.txt
groovy:000> f << "jakis tekst\n"
===> c:\temp\temp1698516868251566991.txt
groovy:000> f << 'i jeszcze inny\n'
===> c:\temp\temp1698516868251566991.txt
groovy:000> f << 'Jeszcze kolejna linia\n'
===> c:\temp\temp1698516868251566991.txt
groovy:000> f.text
===> jakis tekst
i jeszcze inny
Jeszcze kolejna linia

groovy:000> f.eachLine { linia -> println "$linia" }
jakis tekst
i jeszcze inny
Jeszcze kolejna linia
===> null
groovy:000> println f.filterLine { it =~ /olejna/ }
Jeszcze kolejna linia

===> null
Automatyczna obsługa otwarcia i zamknięcia pliku możliwa jest metodą withStream(), której parametrem wejściowym jest domknięcie wykonywane z parametrem - obiektem InputStream. Podobnie jest z zapisem z withWriter().

Kolekcje List, Set, SortedMap oraz SortedSet otrzymały metody asImmutable() do ich zmiany na obiekt niezmienny oraz asSynchronized() do utworzenia obiektu "wielowątkowego" (ang. thread-safe).
 groovy:000> l = ['ala', 'ma', 'kota']
===> [ala, ma, kota]
groovy:000> l.class.name
===> java.util.ArrayList
groovy:000> l << 'nowy elment'
===> [ala, ma, kota, nowy elment]
groovy:000> L = l.asImmutable()
===> [ala, ma, kota, nowy elment]
groovy:000> L << "nie mozna dodawac nowych elementow!"
ERROR java.lang.UnsupportedOperationException: null
at groovysh_evaluate.run (groovysh_evaluate:2)
...
groovy:000> l << 'A do l wciaz mozna'
===> [ala, ma, kota, nowy elment, A do l wciaz mozna]
Więcej o tych i innych zmianach na stronie Groovy JDK API Specification.