Sviluppo rapido di applicazioni con Groovy & Grails

III parte: Alle radici del dinamismodi

Continuiamo la nostra scoperta di Groovy indagando in profondità quei meccanismi che permettono a Groovy di fare cose che in Java non riusciamo a fare. E tutto ciò... rimanendo sulla cara vecchia JVM.

Nel precedente articolo abbiamo esaminato alcune delle caratteristiche che rendono interessante Groovy, specialmente in relazione a quanto fornito a livello di codice. Ora cerchiamo invece di indagare un po' più in profondità nei meccanismi che permettono a Groovy di fare cose che in Java non riusciamo a fare, sia pure continuando a stare sulla JVM.

Object Convenience Methods

Una delle "chicche" più interessanti di Groovy è la possibilità di accedere a determinati metodi di uso comune in maniera decisamente più pratica.

In particolare, dovendo scrivere sullo standard output, in Java siamo soliti ricorrere al "caro vecchio" System.out.println(...), mentre in Groovy possiamo ricorrere ad una più sintetica

println "Ciao a tutti!"

che tra l'altro risulta ulteriormente potenziata dalla gestione che Groovy ha delle stringhe:

public class TestSimpleGDK extends TestCase {
    def pianeta = "Terra"
    def message = "Veniamo in pace!"
    void testPrinting() {
        println "Ciao $pianeta!, $message"
    }
}

L'operatore '$' all'interno di una stringa permette di referenziare gli attributi della nostra classe e di includerli all'interno della stringa, analogamente a quanto accade in altri linguaggi di scripting. Questo meccanismo ci permette di evitare la concatenazione di stringhe con il segno + (cosa che avremmo dovuto fare comunque, ricorrendo a StringBuilder) rendendo il nostro codice decisamente più leggibile.
L'accessibilità del metodo println (e del fratello print) senza passare dall'odiata System.out, non è stata ottenuta introducendo fantomatiche "funzioni globali" ma molto più semplicemente aggiungendo dei convenience methods alla classe Object; dato che in Groovy, come in Java, tutte le classi ereditano da Object, questi metodi risultano invocabili praticamente ovunque.

Il GDK

Il compito di arricchire la dotazione di serie del JDK con nuove funzionalità è assolto dal GDK, ossia (non ci vuole molto, in effetti, a sciogliere l'acronimo) il Groovy Development Kit. Un set di librerie, classi e funzionalità che si colloca al di sopra del JDK e che ne aumenta le potenzialità in un contesto Groovy.

Figura 1 - Il GDK rispetto alle componenti chiave della piattaforma Java.

Il GDK di fatto è il contenitore di tutte le features caratteristiche di Groovy, ma anche di quelle che vengono aggiunte alle classi Java e che le rendono più maneggevoli e friendly, ma solo dentro Groovy. Un po' come guardare il mondo con occhiali rosa (o dopo un paio di birre...).

Groovy Object

Nei precedenti articoli abbiamo affermato che gli oggetti Groovy sono oggetti Java a tutti gli effetti. A dire il vero, sono qualcosa in più: espongono un comportamento dinamico e gestiscono le chiamate ai metodi in maniera diversa da quanto avviene in Java.
Ogni oggetto definito in Groovy, oltre ad ereditare da java.lang.Object implementa implicitamente anche l'interfaccia GroovyObject, definita nel GDK.

Figura 2 - Tutte le classi Groovy discendono da java.lang.Object ed implementano l'interfaccia GroovyObject. La domanda è... come?

I metodi getProperty() e setProperty() permettono di manipolare l'oggetto, modificandone il valore delle property on-the-fly.

public void testPropertyInjection() {
    def contact = new Contact(nome:"Mario", cognome:"Rossi")
    contact.setProperty("nome","Gian Mario")
    assert contact.nome == "Gian Mario"
}

Con Contact definita come:

public class Contact {
    String nome
    String cognome
}

In pratica, una "Reflection più accessibile". Notiamo anche come sia stato possibile costruire la classe Contact con un costruttore flessibile (ma decisamente leggibile), che non abbiamo esplicitamente definito, e come l'operatore '.' permetta di accedere alle property dell'oggetto senza invocare esplicitamente il corrispondente getter.
Ma il vero cuore della flessibilità di Groovy, è il Meta Object Protocol: un sistema di componenti che permettono la manipolazione delle classi a run-time, con un livello di flessibilità e semplicità d'uso che va decisamente oltre quanto offerto dalla Reflection di Java.

MetaClass

La MetaClass è la chiave della dinamicità di Groovy. In Java ogni oggetto è implicitamente associato a una istanza di Class, che ne descrive le metainformazioni: di fatto descrive e rende accessibile la struttura statica della classe.
In Groovy, ogni oggetto è associato anche a una MetaClass che intercetta e smista le chiamate agli oggetti del metodo. Per gli oggetti Groovy, è sufficiente accedere al metodo getMetaClass() per avere un riferimento alla MetaClass associata all'oggetto.

Figura 3 - Ogni oggetto Groovy mantiene una reference alla corrispondente MetaClass.

Per gli oggetti Java, il percorso è lievemente diverso: l'associazione con la MetaClass è gestita dalla classe MetaClassRegistry, che mette in relazione gli oggetti Java con la loro MetaClass nel mondo Groovy, tramite una Map.

Figura 4 - L'associazione tra una classe Java e la corrispondente MetaClass è gestita tramite una Map dalla classe MetaClassRegistry.

Qual è il ruolo della MetaClass? Fondamentalmente si tratta di un punto di indirezione sul percorso di invocazione del metodo. Prima di invocare il metodo definito sulla nostra classe, la classe Invoker di Groovy interroga la corrispondente MetaClass. La MetaClass ha la possibilità di decidere come comportarsi con ogni invocazione.

Figura 5 - Le chiamate ai metodi di MyClass provenienti dall'Invoker sono vagliate prima dalla corrispondente MetaClass, che verifica la presenza di un metodo che faccia matching nel proprio contesto.

Alcuni elementi fondamentali:

  • La MetaClass è modificabile, per cui possiamo alterare il comportamento di una classe senza modificarla, semplicemente agendo sulla MetaClass.
  • Non esiste una singola MetaClass, ma ve ne sono differenti tipologie, che possono essere utilizzate a seconda del contesto ed essere associate alla nostra classe con setMetaClass().
  • La MetaClass è associata ad ogni singola istanza dell'oggetto, per cui sarà possibile associare uno specifico comportamento a una specifica istanza di una determinata classe. Potremo quindi avere un oggetto che si "distingue dal branco".

Tramite la MetaClass è possibile, ad esempio, intercettare una chiamata a un metodo di una classe e ridefinirlo con un nuovo comportamento:

void testInterceptedJavaMethodCall() {
    def five = new Integer(5)
    assertEquals "5", five.toString()
    Integer.metaClass.toString = {-> 'intercettato'}
    assertEquals "intercettato", five.toString()
}

alla terza riga del nostro test, abbiamo detto alla MetaClass associata alla classe Integer, che toString è associato ad una closure, la cui implementazione (tra parentesi graffe nel listato) è quella di restituire la stringa "intercettato", come verificato dalla asserzione successiva.

Le closures possono rivelarsi molto potenti in questo contesto: posso definirle altrove assegnando un comportamento parametrico, e quindi agganciarle ad una classe o ad un intera famiglia di classi.

La classe Object

Comincia a essere un po' più chiaro come sia stato possibile aggiungere dei comportamenti alla nostra classe Object: la MetaClass si appoggia alla classe DefaultGroovyMethods, che si comporta come un repository di "tutti i metodi che avreste voluto avere nella classe Object ma che non avete mai osato chiedere".
Con un IDE di ultima generazione, questi metodi appaiono tra le opzioni disponibili (con Ctrl + Space), come se facessero parte della dotazione di serie della nostra classe. Con le precedenti versioni invece si era un po' meno assistiti.

È comunque evidente, che aggiungendo dinamicità al linguaggio, l'IDE fatalmente resta un po' indietro, perchè può assisterci e "leggerci nel pensiero" su quanto accade a tempo di compilazione, ma non su quanto accadrà a run-time.

Expando

Una classe in Groovy che sfrutta all'estremo le potenzialità offerte dalla MetaClass è Expando. Un'Expando è un oggetto "gonfiabile" la cui struttura viene definita a run-time, praticamente un JavaBean ad assetto variabile.

public void testExpandoDynamicPropertyInjection() {
    def expContact = new Expando(nome:"Mario", cognome:"Rossi")
    expContact.email = "mariorossi@groovyrocks.com"
    assert expContact.nome == "Mario"
    assert expContact.cognome == "Rossi"
    assert expContact.email == "mariorossi@groovyrocks.com"
    }

Le due property "nome" e "cognome" sono create direttamente all'esecuzione del costruttore, mentre la property "email", viene creata successivamente. Un oggetto di questo genere è comodo per contenere dati la cui struttura non è nota a priori, riuscendo a trattarlo comunque come un bean.
Tuttavia questo livello di flessibilità ha un prezzo: a differenza delle altre classi Groovy, gli Expando non possono essere usate nelle applicazioni Java. Le loro potenzialità sono sfruttabili solo in contesti Groovy.

Altri punti di indirezione

La caratteristica fondamentale della MetaClass è quella di permettere la manipolazione del comportamento degli oggetti, senza toccare gli oggetti. Tuttavia, esiste anche la possibilità di dichiarare il nostro oggetto come esplicitamente intercettabile, implementando l'interfacca GroovyInterceptable.

public class MyInterceptableClass implements GroovyInterceptable {
    def metodoEsistente() {
        return "implementazione di default"
    }
    public Object invokeMethod(String name, args) {
        return "...potrei fare qualsiasi cosa qui :-)"
    }
}

In questo modo il punto di intercettazione è spostato all'interno della classe stessa nel metodo invokeMethod(...) in cui posso definire le strategie di comportamento per ogni tipologia di metodo: sia quelli esistenti che quelli non esistenti.

void testInterceptingAllMethods() {
    def interceptable = new MyInterceptableClass()
    assertEquals "...potrei fare qualsiasi cosa qui :-)", interceptable.metodoEsistente()
    assertEquals "...potrei fare qualsiasi cosa qui :-)", interceptable.metodoCheNonEsiste()
}

In questo esempio ci siamo limitati a restituire una stringa, ma è possibile sfruttare le informazioni contenute nella chiamata al metodo per decidere il da farsi.

Siamo dinamici!

Il punto fondamentale è che, mentre in Java le chiamate ai metodi, e tutti i relativi controlli, erano risolti a tempo di compilazioni e le strutture delle classi erano immutabili, in Groovy le chiamate ai metodi sono gestite da un Invoker, che si appoggia alle funzionalità del MOP.
Questa funzionalità offerta da Groovy è estremamente potente: in Grails è usata, ad esempio, per permettere la costruzione di metodi finder on-the-fly: il codice è generato sulla base della naming convention. Supponendo di avere definito la classe Contatto come domain-class in Grails scrivendo il codice:

Contatto.findByNomeAndCognome("Mario", "Rossi")

Grails riconosce la presenza di un'invocazione a un metodo della famiglia findBy*, che rispetta una naming convention (basata sui nomi delle properties e sugli operatori logici) e che quindi può essere automatizzata. In altre parole in Grails non dobbiamo implementare quel metodo: basta dichiararlo e magicamente "appare".

Bello, bello ma...

Qual è il prezzo da pagare per questo "dinamismo" del linguaggio? Fondamentalmente la presenza della MetaClass introduce un livello extra di indirezione. È evidente che porterà a un calo nelle prestazioni, sia pure contenuto, rispetto a codice Java statico [4]. La buona notizia è che il rallentamento è localizzato in un'area ben precisa su cui il team di Groovy sta lavorando attivamente e sul quale i miglioramenti sono stati consistenti, release dopo release [5].
L'altro grosso problema legato alla MetaClass è la "sbrodolata": la dimensione del log nel caso di eccezioni cresce notevolmente, e questo si rivela fastidioso, quando ci troviamo a doverlo investigare.
La presenza di "metodi che non esistono" aveva inizialmente messo in crisi gli editor, ma il mercato si sta rapidamente muovendo verso un supporto esteso a Grails e Groovy. Le differenze nel supporto assistito tra Java e Groovy si stanno decisamente assottigliando.

Conclusioni

Procedendo nell'esplorazione delle features caratteristiche del linguaggio Groovy, la sensazione dominante è quella della "molta resa, poca spesa". Gli strumenti messi in campo per trasformare una piattaforma statica come Java in una di tipo dinamico risultano in fin dei conti pragmatici e sensati. L'effetto combinato delle features caratteristiche del linguaggio è tale da rendere Groovy un linguaggio molto interessante, anche in contesti che non hanno nulla a che vedere con le intenzioni iniziali.
Nel prossimo articolo torneremo a occuparci di Grails prendendo in esame il rapporto delle nostre classi di dominio con la persistenza gestita da GORM e da Hibernate.

Riferimenti

[1] Venkat Subramaniam, "Programming Groovy", Pragmatic Bookshelf

[2] Dierk Konig, "Groovy in Action", Manning

[3] http://groovy.codehaus.org/groovy-jdk/

[4] http://docs.codehaus.org/display/GRAILS/Grails+vs+Rails+Benchmark

[5] http://blog.headius.com/2008/05/power-of-jvm.html

[6] Graeme Rocher, "The definitive guide to Grails", APress

 

 

Condividi

Pubblicato nel numero
135 dicembre 2008
Articoli nella stessa serie
Ti potrebbe interessare anche