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) {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.
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()
groovy> def kwacz(byt) {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 :)).
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)
Sprawdzenie, czy dany obiekt udostępnia daną metodę realizujemy poprzez metaClass i jego metodę respondsTo(), np.
def kwacz(byt) {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.
if (byt.metaClass.respondsTo(byt, 'kwacz')) {
byt.kwacz()
}
}
class Czlowiek {}
kwacz new Czlowiek()
Groovy nie posiada typów prostych, jak int czy byte. Wszystko jest obiektem!
groovy> def x = 1Dzię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").
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
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 :)
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.
OdpowiedzUsuń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 :)
Temat kaczek skojarzył mi się z książką Head First Design Patterns, gdzie próbowano adaptować indyka na kaczkę :)
OdpowiedzUsuń