|
Il Blog di Fabio Collini: blog2j Groovy è un linguaggio di programmazione di nuova concezione che può essere considerato una valida alternativa a java per diversi motivi: - usa una sintassi molto simile a quella java ma con alcuni costrutti ispirtati a python e ruby che ne semplificano l'utlizzo e che rendono il codice più facile da leggere e da mantenere
- è un linguaggio dinamico ma può essere utilizzato anche con oggetti tipizzati
- può essere usato semplicemente per creare script lanciabili da shell o da un task di ant
- è basato sul jdk java, tutte le classi utilizzabili in java sono disponibili anche in groovy
- il compilatore produce file .class compatibili con quelli generati dal compilatore java, per questo motivo nello stesso progetto possono coesistere (e interagire fra di loro) classi java e classi groovy
Uno sviluppatore java si troverà subito a suo agio con groovy e soprattutto non dovrà riiniziare da zero potendo usare una sintassi familiare, le stesse classi di base del linguaggio e perfino lo stesso ambiente di sviluppo. Groovy nasce da una idea di James Strachan che per la prima volta ne parlò nel suo blog nell'agosto del 2003. L'idea è poi diventata la JSR 241 e nel giro di alcuni anni sono state rilasciate varie versioni. Il 2 gennaio 2007 è stata rilasciata la versione 1.0, il 9 dicembre 2007 la versione 1.5. In questa introduzione vedremo come aggiungere ad un progetto java esistente alcune classi scritte in groovy. Per scrivere il codice ho utilizzato Eclipse con installato il plugin per groovy, se avete necessità leggete l’ howto su come istallare il plugin Groovy su Eclipse. Iniziamo analizzando un semplice esempio: un modulo di importazione esportazione di voci di una rubrica. Potete scaricare il codice sorgente completo, comprende 3 package: interfacce principali, implementazione delle interfacce in java e implementazione delle stesse interfacce in Groovy. Le interfacce comuni sono: - due bean usati per racchiudere i dati
public interface Persona { String getNome(); void setNome(String nome); String getCognome(); void setCognome(String cognome); Date getDataDiNascita(); void setDataDiNascita(Date dataDiNascita); Indirizzo getIndirizzo(); void setIndirizzo(Indirizzo indirizzo); } public interface Indirizzo { String getViaNumero(); void setViaNumero(String viaNumero); String getCitta(); void setCitta(String citta); } - una interfaccia da implementare per importare i dati
public interface Importatore<T> { List<T> importa(); } - una interfaccia da implementare per esportare i dati
public interface Esportatore<T> { void esporta(List<T> l); } Iniziamo ad aggiungere un po' di Groovy, per aggiungere al class path le librerie necessarie basta cliccare col tasto destro sul progetto nel package explorer di eclipse e selezionare Groovy -> Add Groovy nature. Adesso siamo pronti a scrivere la prima classe Groovy, per fare ciò usiamo il menù “New” di Eclipse e selezioniamo Groovy class. Andiamo a creare l'implementazione delle interfacce dei bean che contengono i dati: class PersonaGroovyImpl implements Persona { String nome String cognome Date dataDiNascita Indirizzo indirizzo } class IndirizzoGroovyImpl implements Indirizzo { String viaNumero String citta } A prima vista sembra che manchi qualcosa... Invece c'è tutto! Analizziamo nei dettagli la classe IndirizzoGroovyImpl, la stessa classe java è la seguente: public class IndirizzoJavaImpl implements Indirizzo { private String viaNumero; private String citta; public String getViaNumero() { return viaNumero; } public void setViaNumero(String viaNumero) { this.viaNumero = viaNumero; } public String getCitta() { return citta; } public void setCitta(String citta) { this.citta = citta; } } 4 righe di codice contro 18, scopriamo come è possibile: - la classe non è definita public infatti in groovy public è il comportamento di default per le classi
- groovy usa le properties, un campo senza definizione di visibilità (public, private o protected) è una property, non devono essere definiti i metodi get e set che vengono aggiunti in automatico
- il punto e virgola a fine riga è opzionale
Grazie a queste caratteristiche la scrittura di bean è immediata e non ripetitiva, i bean sono semplicissimi da leggere e ogni informazione (nome dei campi, tipi) è scritta in un unico posto. Proseguiamo con la classe che esegue l'importazione da file di testo, una implementazione java è public class ImportatoreTxtJavaImpl implements Importatore<Persona> { private final String fileName; public ImportatoreTxtJavaImpl(String fileName) { this.fileName = fileName; } private static SimpleDateFormat format = new SimpleDateFormat("dd/MM/yyyy"); public List<Persona> importa() { List<Persona> l = new ArrayList<Persona>(); BufferedReader reader = null; try { reader = new BufferedReader(new FileReader(fileName)); String line = reader.readLine(); while (line != null) { String[] split = line.split("\|"); Persona p = new PersonaJavaImpl(); p.setNome(split[0]); p.setCognome(split[1]); p.setDataDiNascita(format.parse(split[2])); Indirizzo indirizzo = new IndirizzoJavaImpl(); indirizzo.setViaNumero(split[3]); indirizzo.setCitta(split[4]); p.setIndirizzo(indirizzo); l.add(p); line = reader.readLine(); } } catch (IOException e) { throw new RuntimeException(e); } catch (ParseException e) { throw new RuntimeException(e); } finally { if (reader != null) { try { reader.close(); } catch (IOException e) { throw new RuntimeException(e); } } }(ad esempio il blocco finally) return l; } } Le eccezioni sono state gestite per semplicità con eccezioni runtime, nonostante questo la gestione degli errori non è banale. Analizzando il metodo importa si nota che una parte è di gestione e lettura del file di testo e un'altra parte rappresenta la conversione in oggetto di una riga del file. Quindi possiamo creare una classe che racchiude il codice riutilizzabile in altri contesti per gestire la lettura del file: public class ImportatoreTxtGenericoJavaImpl<T> implements Importatore<T> { private final String fileName; private final ImportatoreRiga<T> importatoreRiga; public ImportatoreTxtGenericoJavaImpl(String fileName, ImportatoreRiga<T> importatoreRiga) { this.fileName = fileName; this.importatoreRiga = importatoreRiga; } public List<T> importa() { List<T> l = new ArrayList<T>(); BufferedReader reader = null; try { reader = new BufferedReader(new FileReader(fileName)); String line = reader.readLine(); while (line != null) { l.add(importatoreRiga.importa(line)); line = reader.readLine(); } } catch (IOException e) { throw new RuntimeException(e); } finally { if (reader != null) { try { reader.close(); } catch (IOException e) { throw new RuntimeException(e); } } } return l; } } una interfaccia usata da questa classe che si occupa di trasformare in oggetto una singola riga: public interface ImportatoreRiga<T> { T importa(String s); } e una implementazione di questa interfaccia utilizzata nel nostro esempio che crea un oggetto Persona partendo da una riga: public class ImportatorePersona implements ImportatoreRiga<Persona> { private static SimpleDateFormat format = new SimpleDateFormat("dd/MM/yyyy"); public Persona importa(String s) { try { String[] split = s.split("\|"); Persona p = new PersonaJavaImpl(); p.setNome(split[0]); p.setCognome(split[1]); p.setDataDiNascita(format.parse(split[2])); Indirizzo indirizzo = new IndirizzoJavaImpl(); indirizzo.setViaNumero(split[3]); indirizzo.setCitta(split[4]); p.setIndirizzo(indirizzo); return p; } catch (ParseException e) { throw new RuntimeException(e); } } } Il codice interessante della classe ImportatorePersona è il metodo importa, capiamo il perché. ImportatoreTxtGenericoJavaImpl ha bisogno di sapere quale metodo invocare per trasformare una riga di un file di testo in un oggetto. Quindi l'interfaccia ImportatoreRiga esiste solo per wrappare il metodo da passare. Questo comportamento in java è molto frequente (per esempio in tutti i listener), per poterlo implementare più velocemente sono state create le classi interne anonime. Per esempio: possiamo utilizzare la classe ImportatoreTxtGenericoJavaImpl senza definire esplicitamente una implementazione di ImportatoreRiga: List<Persona> persone = new ImportatoreTxtGenericoJavaImpl<Persona>( "resources/persone.txt", new ImportatoreRiga<Persona>() { public Persona importa(String s) { Persona p = new PersonaJavaImpl(); //.... return p; } } ).importa(); Usando le classi anonime si ottiene codice più compatto ma meno leggibile soprattutto per i non esperti. La classe Groovy che implementa l'importazione è la seguente: class ImportatoreTxtGroovyImpl implements Importatore<Persona> { String nomeFile private static SimpleDateFormat format = new SimpleDateFormat("dd/MM/yyyy") List<Persona> importa() { List<Persona> l = new ArrayList<Persona>() new File(nomeFile).eachLine { String[] split = it.split("\|") Persona p = new PersonaGroovyImpl() p.nome = split[0] p.cognome = split[1] p.dataDiNascita = format.parse(split[2]) Indirizzo indirizzo = new IndirizzoGroovyImpl() indirizzo.viaNumero = split[3] indirizzo.citta = split[4] p.indirizzo = indirizzo l.add(p) } return l } } La classe java ImportatoreTxtGenericoJavaImpl è in pratica già messa a disposizione da Groovy con il metodo eachLine di File, da notare che File è una classe java standard a cui sono stati aggiunti dei metodi di utilità utilizzabili da codice Groovy. Ma cos'è quel blocco di codice scritto dopo il metodo eachLine? E' una closure, il costrutto nativo Groovy per passare un blocco di codice come argomento di un metodo. In pratica una closure è un oggetto contenente un blocco di codice, nell'esempio appena visto viene passato al metodo eachLine (le parentesi tonde dopo il metodo nel caso delle closure sono opzionali). Quando una closure ha un solo parametro in ingresso la definizione di tale parametro può essere omessa, all'interno della closure è possibile accedere al parametro con la variabile it. I parametri di un metodo possono essere specificati in una lista seguita dai caratteri ->, per esempio: def map = [ 'a', 7, 'b', new Date(), new Date() + 1 ].groupBy{ it.class } map.eachWithIndex { e, indice -> println "${indice} Classe ${e.key} valori ${e.value}" } Questo codice sfrutta due closure (la seconda con una definizione esplicita dei parametri) e due metodi di List e Map che prendono una clusure in ingresso. Groovy definisce molti metodi utili sulle collection che sfruttano le closure, per esempio each, collect, find, findAll, min, max. Il metodo groupBy itera sugli oggetti della lista e raggruppa gli elementi in base al valore di ritorno della closure passata, in un metodo o una closure groovy la keyword return può essere omessa in quanto viene ritornata la valutazione dell'ultimo comando. l metodo eachWithIndex itera sugli elementi di una collection e passa alla closure l'i-esimo elemento e l'indice dell'elemento. L'istruzione all'interno della closure sfrutta le GString di groovy, usando la sintassi ${nomeVariabile} è possibile creare facilmente stringhe contenenti valori delle variabili disponibili. Come ultimo esempio vediamo come implementare in Groovy la classe che esporta una lista di persone in un file xml. Per il corrispondente esempio in java dovremmo utilizzare dom o librerie esterne tipo jdom, jaxb, castor o altre ancora. L'implementazione groovy è la seguente: class EsportatoreXmlGroovyImpl implements Esportatore { private String nomeFile EsportatoreXmlGroovyImpl(String nomeFile) { this.nomeFile = nomeFile } private SimpleDateFormat formatter = new SimpleDateFormat("dd/MM/yyyy"); void esporta(List<Persona> persone) { def writer = new FileWriter(nomeFile); MarkupBuilder builder = new MarkupBuilder(writer) try { builder.listaPersone(totale: persone.size()) { persone.eachWithIndex { p, pos -> builder.persona(indice: pos) { nome(p.nome) cognome(p.cognome) dataDiNascita(formatter.format( p.dataDiNascita)) indirizzo { viaNumero(p.indirizzo.viaNumero) citta(p.indirizzo.citta) } } } } } finally { if (writer != null) { writer.close(); } } } } Questa classe fa uso dei builder Groovy, un concetto un po' particolare ma utilissimo. In pratica possono essere invocati sull'oggetto builder dei metodi con nome corrispondente al nome dell'elemento xml che si vuole creare. Tali metodi non sono definiti normalmente, le chiamate vengono intercettate e viene creato il codice xml in automatico in base al nome del metodo e ai parametri passati. Se al metodo viene passata una coppia, viene aggiunto un attributo all'elemento xml, se viene passato un valore tale valore viene inserito all'interno dell'elemento. Per creare elementi annidati basta passare una closure che crea gli elementi interni. Insomma è molto più difficile spiegare il funzionamento interno di un builder che non utilizzarlo :-) Infatti c'è una relazione molto stretta fra il codice appena visto e l'xml che viene generato, l'indentazione aiuta molto in quanto il codice Groovy è indentato nello stesso modo del codice xml generato. Concludiamo creando una semplice classe java che legge un file di testo e esporta il contenuto in xml: public class MainJava { public static void main(String[] args) { List<Persona> persone = new ImportatoreTxtJavaImpl("resources/persone.txt").importa(); new EsportatoreXmlGroovyImpl("resources/persone.xml").esporta(persone); } } Si può notare come nel metodo main siano presenti sia chiamate a metodi di classi java sia chiamate a metodi di classi groovy, addirittura la lista di persone importate da un metodo java (quindi oggetti java) viene passata a un metodo Groovy che la esporta in xml. Groovy può essere usato anche come un linguaggio di scripting, quindi è possibile definire codice all'esterno di una classe, per esempio: package groovytutorial.groovyimpl def dir = "resources" def persone = new ImportatoreTxtGroovyImpl(nomeFile: "${dir}/persone.txt").importa() new EsportatoreXmlGroovyImpl("${dir}/persone.xml").esporta(persone) In questo codice sono presenti anche variabili non tipizzate per le quali è stata usata la keyword def (l'analogo di var in javascript). Su internet si trovano pareri discordanti riguardo all'uso di variabili non tipizzate in Groovy, molto dipende dall'esperienza personale, probabilmente uno sviluppatore java si troverà molto più a suo agio usando variabili tipizzate. Il nostro esempio si è concluso, è il momento di qualche considerazione (ovviamente personale): - Groovy è l'ideale se avete bisogno di un linguaggio di scripting da usare all'interno di un progetto per modellare comportamenti dinamici definibili ad esempio dall'utente
- la sintassi Groovy è uno dei suoi punti più potenti, molte cose che in java sono difficili o poco intuitive in Groovy si scrivono al volo
- l'integrazione perfetta con codice java esistente permette l'introduzione graduale di Groovy, un buon inizio potrebbe essere utilizzare Groovy per scrivere i test junit di un applicativo java
- il supporto per Groovy dei vari ide è ancora ben lontano da quello esistente per java. Tanto per fare un esempio: in Eclipse è assente il menù refactor e il menù source è quasi vuoto (in pratica non è presente molto di quello “che fa la differenza” in Eclipse)
Le mie uniche perplessità sono dovute al supporto ancora incompleto in Eclipse, per il resto è sicuramente un linguaggio da provare. Riferimenti: Sito ufficiale: Groovy.codehaus.org Libro: Groovy in action Blog di Fabio Collini: blog2j
|