14 czerwca 2009

Rozdział 4. "Dynamic Typing" z "Programming Groovy" i moje poniedziałkowe wystąpienie o Groovy i Grails we Wrocławiu

Wracam do relacji z lektury "Programming Groovy: Dynamic Productivity for the Java Developer" Venkata Subramaniama. Rozdział 4. "Dynamic Typing" rozpoczyna się rozważaniami dotyczącymi natury języków programowania - podział na języki statyczne vs dynamiczne i silna vs słaba typizacja. Nigdy nie byłem mocny w tych podziałach, a dopiero niedawno udało mi się zrozumieć podział między języki imperatywne vs funkcyjne. Wydawałoby się, że wcześniejsze programowanie w języku z każdej kategorii powinno dać mi jasny obraz, ale może dlatego, że owe programy były od czapy, takie nieżyciowe, nie dane mi było zrozumieć tej różnicy wcześniej. Stąd ten wstęp, który mogłoby wydawać się lekko nie na temat, jest dla mnie jak najbardziej na temat. Groovy jest językiem dynamicznym z silną typizacją. Nawet jeśli nie podajemy typu zmiennej, to dzięki inferencji typów (ang. duck typing) wiadomo, z czym mamy do czynienia. Ważne, aby pamiętać, że to co wiadomo może się w trakcie działania aplikacji zmienić (!) Coś na przekór "nie wszystko złoto co się świeci". Jeśli założymy, że złoto to jest coś co się świeci, podczas uruchomienia aplikacji Groovy i piryt może być złotem - wystarczy, że w międzyczasie nadamy mu ów blask, dynamicznie.

Pierwszą zauważalną zaletą dynamicznego typowania jest mniejsza liczba wystukanych klawiszy, aby napisać kompletną aplikację - zero konieczności określania typów podczas definicji zmiennej. Pojawia się pojęcie "Design by Capability", który jest jakby uzupełnieniem "Design by Contract". Tym razem nie interfejs wyznacza kontrakt (jak ma to miejsce w Javie), ale samo istnienie metody. W końcu tylko dlatego implementujemy interfejs, aby ostatecznie posiadać gwarancję, że rozmawiamy z bytem, który posiada określone zachowanie. W Groovy pozbywamy się tego z tzw. "kaczym typowaniem" (ang. duck typing). Tak na prawdę, dla nas, programistów, nie ma znaczenia, czy realizujemy kontrakt przez interfejs czy zestaw publicznych metod - dostosowujemy się do mechanizmów języka, a w Javie się po prostu inaczej nie da (dobra, dobra - da się, bo w końcu mamy Groovy, który jest właśnie aplikacją javową...ekhm...językiem na podwalinach Javy).

Trochę praktyczniej. W Groovy mamy, więc tak:
 def kwacz(byt) {
byt.kwacz()
}

def idz(byt) {
byt.idz()
}

class Kaczka {
void kwacz() {
println "Kwa, kwa, kwa"
}

void idz() {
println "Idę sobie, kwa, kwa, kwa"
}
}

class Czlowiek {
void idz() {
println "Idę sobie pogwizdując"
}
}

idz new Kaczka()
idz new Czlowiek()
I działa! W Javie konieczne byłoby określenie wspólnego interfejsu. W Groovy już nie. Jeśli tylko dany byt udostępnia oczekiwaną metodę wchodzi do gry. Jeśli nie, dopiero próba wykonania metody spowoduje zgłoszenie wyjątku groovy.lang.MissingMethodException.
 groovy> def kwacz(byt) {
groovy> byt.kwacz()
groovy> }
groovy> class Czlowiek {
groovy> }
groovy> kwacz new Czlowiek()

Exception thrown: No signature of method: Czlowiek.kwacz() is applicable for argument types: () values: []

groovy.lang.MissingMethodException: No signature of method: Czlowiek.kwacz() is applicable for argument types: () values: []
at ConsoleScript0.kwacz(ConsoleScript0:2)
at ConsoleScript0$kwacz.callCurrent(Unknown Source)
at ConsoleScript0.run(ConsoleScript0:6)
To musi pociągać za sobą konsekwencje - większą dyscyplinę programowania. Na scenę wchodzą testy. Właśnie nimi rekompensujemy brak silnego typowania znanego z Javy, gdzie kompilator bierze na siebie kontrolę poprawności programu. W Groovy musimy obłożyć naszą aplikację testami, bo bez nich jesteśmy na polu minowym. Aplikacja może działać latami i nic się nie sypnie, aż do tego dnia, kiedy ktoś wywoła nieistniejącą metodę. Bum! Doszukanie się błędu może być nieprzyjemne (uwaga, eufemizm :)).

Sprawdzenie, czy dany obiekt udostępnia daną metodę realizujemy poprzez metaClass i jego metodę respondsTo(), np.
 def kwacz(byt) {
if (byt.metaClass.respondsTo(byt, 'kwacz')) {
byt.kwacz()
}
}
class Czlowiek {}
kwacz new Czlowiek()
Zazwyczaj definiowanie metod w Groovy wiąże się z wykorzystaniem słowa kluczowego def, które określa, że zwracany typ to Object (jak wyżej z metodą kwacz). Jeśli musimy określić zwracany typ innym niż domyślny Object, np. konwencja testów jednostkowych w JUnit (metoda musi być publiczna i zwracać void), to metodę definiujemy identycznie z wymogami w Javie.

Groovy nie posiada typów prostych, jak int czy byte. Wszystko jest obiektem!
 groovy> def x = 1
groovy> println x.class.name
groovy> def y = 1.1
groovy> println y.class.name
groovy> int z = 1
groovy> println z.class.name

java.lang.Integer
java.math.BigDecimal
java.lang.Integer
Dzięki tej cesze, pisanie DSLi (język dziedzinowy, ang. Domain Specific Language) w Groovy jest banalnie proste. Jako przykład autor przedstawia tworzenie jednego, który pozwala na konstrukcje podobne do "5.days.ago.at 4:30" (ale o tym dopiero w rozdziale 18. "Creating DSLs in Groovy").

Na zakończenie rozdziału pojawia się ciekawy przykład z użyciem typów generycznych ala Java Killers Pawła Szulca. Jak to ujął autor (str. 78): "Multimethods fix a problem in Java". Polimorfizm w Javie opiera się na wykonaniu metody z typu obiektu, na który wskazuje referencja, a nie typowi referencji. Wszystko jest pięknie, jeśli metoda w typie potomnym odpowiada deklaracji klasy bazowej/interfejsu. Jeśli jednak poza nią zadeklarujemy bardziej odpowiadającą przekazywanemu typowi jako parametr wejściowy, to i tak nastąpi rzutowanie na typ parametru wejściowego w klasie bazowej/interfejsu. Polimorfizm niepełny? W Groovy obiekt będzie odpytany, czy wspiera metodę o danej sygnaturze. Po konkretne przykłady zapraszam do książki.

Ostatnia sekcja 4.8 "Dynamic: To Be or Not to Be?" to kilka wskazówek odnośnie deklarowania typu obiektu. Jeśli jesteśmy zmuszeni przez dane rozwiązanie, jak JUnit czy odwzorowanie obiektowo-relacyjne (ORM), gdzie typ wskazuje na typ kolumny lub też definiujemy API dla rozwiązań javowych, podajemy typ. W przeciwnym przypadku def wystarczy.

Jeśli temat Cię interesuje, jesteś w poniedziałek 15. czerwca we Wrocławiu i masz chwilę między 18:00 a 20:00 zapraszam na moją prezentację Groovy i Grails "Trochę więcej niż tylko wprowadzenie do Groovy i Grails" w ramach spotkań Wrocław JUG. Postaram się nie zanudzać uczestników teorią, a wypełnić czas praktyką. Zachęcam do aktywnego udziału! Nagrodą będzie wejściówka na JAVArsovię 2009 :)

2 komentarze:

  1. Hmm... Właśnie dlatego, że testy polegają często na sprawdzaniu czy dany obiekt jest danego typu i czy ma dane metody, Twitter przeszedł na Scalę ;) Jeśli musimy w testach sprawdzać coś, co może sprawdząć kompilator, to czy nie lepiej dowiedzieć się o błędzie w czasie kompilacji? W Scali też mamy duck typing (no prawie), i mamy jednocześnie statyczne typowanie :) Trik polega na tym, że ten duck typing to tak nie do końca duck typing, tzn. musimy wcześniej jak dany typ skonwertować na drugi.

    Chyba trochę zamieszałem, więc może krótki przykład. Załóżmy, że mamy dwie klasy, Perkoz i Kaczka. Kaczka ma metodę nurkuj(), i mamy funkcję przyjmującą jako parametr obiekt typu Kaczka:
    def dajNura(k:Kaczka) {
    k.nurkuj();
    }

    I teraz zadziała nam taki kod:

    var p = new Perkoz();
    dajNura(p);

    Żeby zadziałał, musimy tylko wcześniej określić, w jaki sposób skonwertować Perkoza na Kaczkę. I już mamy duck typing ze statycznym typowaniem :)

    OdpowiedzUsuń
  2. Temat kaczek skojarzył mi się z książką Head First Design Patterns, gdzie próbowano adaptować indyka na kaczkę :)

    OdpowiedzUsuń