Sviluppo rapido di applicazioni con Groovy & Grails

II parte: Groovy, un linguaggio "carico"di

Nel precedente articolo abbiamo sperimentato la possibilità di sviluppare un‘applicazione web in tempi decisamente ridotti, con Grails e Groovy. In questo secondo articolo della serie, diamo un‘occhiata un po‘ più in profondità al linguaggio che permette le "magie" di Grails.

Installiamo Groovy

Premessa: per lavorare con Grails non è necessario installare Groovy. Grails viene rilasciato con la propria versione di Groovy ed è quindi già operativo senza la necessità di ulteriori installazioni. Per poter utilizzare comodamente Groovy, è comunque comodo disporre di una specifica installazione [1].
Groovy può essere scaricato, nella versione più comoda per la nostra piattaforma di destinazione, dal sito ufficiale di Groovy.
L'installer di Groovy - almeno nella versione per Windows - fa il suo dovere ed è piuttosto trasparente nel comportamento:


Figura 1 - Una schermata dell'installer di Groovy, molto "onesta".

È inoltre possibile installare alcuni componenti opzionali (tra cui la documentazione in PDF - corrispondente all'immagine del wiki), ma per il momento non dovrebbero servire... dipende da quanto spazio libero avete su disco.

Pasticciamo con Groovy

Groovy offre qualche strumento comodo per "prendere le misure" al nuovo linguaggio di programmazione. In particolare, l'installazione rende disponibili i seguenti comandi:

  • groovysh: una shell a riga di comando che permette di eseguire codice codice Groovy interattivamente
  • groovyConsole: un'interfaccia grafica che può eseguire codice Groovy interattivamente, oltre a caricare file .groovy
  • groovy: lancia l'interprete groovy che esegue gli scripts.

Probabilmente, per effettuare le prime prove, lo strumento migliore è la groovyConsole, che ci fornisce una valutazione on-the-fly di una sequenza di istruzioni, senza che queste debbano necessariamente fare parte di un'applicazione strutturata.

Figura 2 - La GroovyConsole in azione: apparentemente è tutto OK.

Per passare a una valutazione un po' più strutturata delle potenzialità del linguaggio, non dobbiamo fare altro che usare il nostro buon vecchio IDE. Nel caso di Eclipse possiamo scaricare il plugin ufficiale da http://groovy.codehaus.org/Eclipse+Plugin, ma praticamente tutti gli IDE in questo momento stanno offrendo un supporto di buon livello per Groovy (con IntelliJ IDEA a fare la parte del leone).

Elementi fondamentali

Ok, siamo pronti a "smanettare", è il momento di impratichirci con gli elementi fondamentali del linguaggio.

Groovy Objects

Scrivere un articolo per MokaByte mi "impone" di parlare di Groovy usando Java come pietra di paragone: bene, a differenza di Java, in Groovy tutto è un oggetto. In Java è presente una fastidiosa asimmetria tra oggetti e tipi primitivi, a cui nel corso degli anni abbiamo finito per fare il callo, ma che si faceva notare sin dai primi attimi in cui iniziavamo a programmare una classe, sotto forma della domanda "quale tipo scelgo per questo attributo?": i numeri sono tipi primitivi, ma per scrivere applicazioni finanziarie serie, era necessario utilizzare BigDecimal che invece è una classe, Integer è un oggetto, ma int è più performante, devo usare un array o una ArrayList? ...e così via.
La scelta di Groovy è di avere solamente oggetti. Spariscono i tipi primitivi e questo permette di semplificare drasticamente il quadro rendendo Groovy un linguaggio più Object Oriented rispetto a Java. In altre parole, scrivere

int a = 1
float b = 1.0f

è assolutamente equivalente a scrivere

Integer a = 1
Float b = 1.0f

Non si tratta dell'autoboxing tipico di Java 5: ogni tipo numerico è istanziato come un oggetto, che quindi potremmo interrogare con un getClass() ottenendo come risposta java.lang.Integer e java.lang.Float, rispettivamente. È possibile che avvengano operazioni di unboxing e boxing dietro le quinte (al momento di interagire con le librerie Java) ma, dal punto di vista del programmatore, in Groovy abbiamo sempre e comunque a che fare con oggetti. Questo, come vedremo, consente di semplificare notevolmente la sintassi, permettendo a metodi ed operatori di agire uniformemente su tutte le entità del linguaggio.

Tipizzazione implicita

In realtà, Groovy ci permette una semplificazione ulteriore: utilizzando la parola chiave def, posso delegare al linguaggio la scelta del tipo da utilizzare per la memorizzazione di una variabile. In altre parole, non decido io il tipo di una variabile, ma lascio che sia Groovy a farlo.

def a = 1
def b = 1.0f
def c = 25289.315

Le prime due istruzioni sono assolutamente identiche agli esempi precedenti. La variabile "c" viene istanziata come BigDecimal, che in Groovy è la scelta di default ogni volta che è presente un decimale. Per chi ha lavorato con Java su applicazioni finanziarie ed ha dovuto combattere perche' venissero usati i BigDecimal, invece che float o double ...è come avere una birra gratis!
Se la scelta di usare unicamente oggetti rende Groovy più Object Oriented rispetto a Java, allora possiamo anche dire che la possibilità di differire la scelta del tipo (o di non farla proprio) è un elemento che conferisce al linguaggio maggiore agilità limitando il numero delle scelte che devono essere compiute in anticipo, permettendo di applicare il principio del "decide as late as possible" [2].

Operatori

In Java, i tipi primitivi avevano 2 vantaggi rispetto agli oggetti: una gestione più efficiente della memoria - senza l'ambaradàn che caratterizza gli oggetti - e la possibilità di essere manipolati direttamente tramite operatori ("+","-",">>","++", etc.) mentre le loro controparti in forma di classe hanno goduto di questo privilegio solo dall'avvento dell'autoboxing, nella versione 5 di Java. Altre classi numeriche, quali BigInteger e BigDecimal restano però tuttora escluse da questo privilegio, così come eventuali classi numeriche da noi definite, che potrebbero tornarci utili. In altre parole, l'autoboxing sposta la linea di confine tra oggetti e tipi primitivi, creando una zona franca per le wrapper class.
L'eliminazione dei tipi primitivi rende inoltre più semplice l'introduzione del polimorfismo anche a livello di operatori: se tutti i possibili operandi sono oggetti, allora anche l'implementazione degli operatori risulta grandemente semplificata.

Overriding degli operatori

Uno dei meccanismi che permette di infondere coerenza alle librerie Java, anche in punti in cu ne erano prive in maniera imbarazzante, è la possibilità di ridefinire il comportamento degli operatori.
In Java, gli operatori - complice la distinzione tra oggetti e tipi primitivi - potevano essere utilizzati principalmente sui tipi primitivi. Sono presenti alcune eccezioni (significativa ad esempio la possibilità di utilizzare l'operatore "+" per concatenare le stringhe, sia pure in modo penalizzante per la performance...), ma la gran parte degli operatori agisce unicamente sui tipi primitivi. Il comportamento degli operatori ricade tra le caratteristiche predefinite del linguaggio. In Groovy, è stata re-introdotta una caratteristica tipica del C++, volutamente ignorata dai padri del linguaggio Java. In C++ infatti è possibile ridefinire il comportamento degli operatori (con una sintassi simile a quella dei metodi, ma con qualche accorgimento in più). Purtroppo, in molti casi, questo meccanismo si rivelava utilissimo per rendere il proprio codice assolutamente illeggibile ai colleghi.
Java ha rinunciato a questa feature (rendendo - ahimè - necessario ricorrere agli offuscatori, per rendere incomprensibile il codice), Groovy invece approfitta della semplificazione introdotta abolendo la distinzione tra oggetti e tipi primitivi per reintrodurre la possibilità di ridefinire il comportamento degli operatori, sia pure limitandolo a un set predefinito di funzionalità.
Ad ogni operatore corrisponde implicitamente un metodo. Definendo l'implementazione di un metodo nella nostra classe possiamo permetterle di utilizzare la sintassi basata sul corrispondente operatore, oltre ovviamente a quella tradizionale. Prendiamo ad esempio gli operatori aritmetici e in figura 3 vediamo una tabella delle corrispondenze.

 Figura 3 - Tabella con gli operatori aritmetici base in Groovy

La presenza di una conversione implicita dall'operatore al metodo consente (finalmente) di ovviare a quella fastidiosa caratteristica di Java di non permettere un'aritmetica leggibile con tipi numerici non banali: quantità finanziarie sono spesso implementate con una classe apposita, che tenga conto anche della valuta; ad esempio la classe Money in Java può essere implementata come segue:

import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Currency;
/**
 * La classe Money in Java.
 */
public class Money implements Serializable {
    private BigDecimal amount;
    private Currency currency;
    ...
    /**
     * Restituisce un'istanza di Money somma di questa e del parametro.
     */
    public Money plus(Money other) {
        if (this.getCurrency().equals(other.getCurrency())) {
            return new Money(this.amount.add(other.amount), this.currency);
        } else {
            throw new IllegalArgumentException("Currencies do not match");
        }
    } 
    ...

Usare plus() oppure add() è in definitiva una scelta di stile... ma noi vorremmo usare il buon vecchio "+"!!! Senza bisogno di ripassare i nomi delle operazioni in inglese o di chiederci se in italiano dobbiamo chiamare il metodo piu() oppure più().
In Groovy, possiamo ottenere lo stesso risultato con:

/**
 * La classe Money in Groovy
 */
  class Money {
   Currency currency
   BigDecimal amount
   public Money plus(Money other) {
    if (other.currency == currency) {
     return new Money(amount + other.amount, currency)
    } else {
     throw new IllegalArgumentException("Currencies do not match");
    }  
  }
}

E questo ci permette di scrivere qualcosa di molto pulito, come:

void testPlus() {
   Money E25 = new Money(25, Currency.getInstance("EUR"))
   Money E50 = new Money(50, Currency.getInstance("EUR"))
   Money E100 = new Money(100.00, Currency.getInstance("EUR"))
    assertEquals(E50, E25 + E25)
   assertEquals(E100, E50 + E25 + E25)
}

Decisamente mooolto più leggibile. Per fare funzionare il codice di test appena presentato, è necessario implementare correttamente il metodo equals(), che vedremo tra poco. Ciò che è interessante notare è che il l'operatore "+" è applicabile sulla nostra classe Money, semplicemente dopo avere ridefinito il metodo plus().
A differenza di quanto accade in C++, non è necessario ricorrere a nuovi costrutti per la ridefinizione del comportamento degli operatori: gli operatori sono essenzialmente delle scorciatoie per accedere a normalissimi metodi, sia pure limitati a un set predefinito.

Equals

Il metodo equals() è uno degli esempi più eclatanti della differente filosofia di Groovy rispetto a Java: esiste un solo operatore di uguaglianza in Groovy: l'operatore "==". La sua implementazione è fornita dal metodo equals(), per cui possiamo tranquillamente implementare il nostro test come

assertEquals(E50, E25 + E25)
assertEquals(E100, E50 + E25 + E25)
assert (E100 == (E50 + E50))

e, ancora una volta, il risultato è decisamente più leggibile. In Java la differente filosofia dei due diversi operatori di uguaglianza finiva per avere lo stesso compito di una porta a vetri in ufficio: prima o poi qualcuno ci va a sbattere.
Una tipica implementazione del metodo equals() nella nostra classe Money non risulta particolarmente ostica:

/**
 * Verifica l'uguaglianza con un'altra istanza di Money
 */
boolean equals(Object other) {
    if (null == other)     return false
    if (! other instanceof Money)  return false
    if (currency != other.currency)   return false
    if (amount != other.amount)  return false
    return true
}

La logica non è particolarmente diversa dai classici metodi equals() che siamo abituati a scrivere in Java. Notiamo però che in Groovy non è necessario effettuare il casting, che normalmente è necessario in Java per accedere ai campi del parametro other.
Ovviamente, in assenza dell'implementazione del metodo (che anche in Groovy è compito nostro), il comportamento di default si appoggia sull'implementazione di equals() di java.lang.Object(), cioè la classica uguaglianza per riferimento di Java.

Collezioni

Parlare di omogeneità degli operatori, raffrontandoci a Java è un po' come sparare sulla croce rossa (o su John Travolta mentre legge una rivista in bagno [3]): il fatto è che le strutture dati in Java espongono comportamenti disomogenei, come possiamo notare nella tabella seguente.

 Figura 4 - Tabella con riepilogo dei differenti operatori che restituiscono la lunghezza di una struttura dati in Java.

Probabilmente, il probelma deriva da un cattivo coordinamento fra diversi gruppi di lavoro, risalente ai primi anni di Java, in cui era necessario arricchire rapidamente la piattaforma Java di tutti gli strumenti necessari a sostenere le richieste del mercato enterprise ("troppi cuochi rovinano il brodo" direbbe Matsumoto [4] - il padre di Ruby - se avesse origini italiane), oppure di una sottigliezza semantica che ancora non abbiamo debitamente apprezzato, secondo la quale una stringa è lunga, mentre una collezione è grande.
Ancora una volta, c'è una omogeneità locale (ad esempio, nel Collection framework), ma non è possibile rintracciare un comportamento coerente a livello dell'intero linguaggio.
In Groovy, gli operatori ed i metodi agenti sulle più disparate strutture dati sono omogenei tra loro. Ad esempio, l'operatore .size() agisce indistintamente su stringhe, collezioni, files, e così via. È evidente che questa caratteristica costituisce un vantaggio sia in termini di interoperabilità, che di tempi di apprendimento del linguaggio, che risulta più coerente e che permette di limitare il numero di costrutti che è necessario conoscere per svolgere le operazioni di tutti i giorni.

Ranges

Avvertenza: i programmatori affezionati al ciclo for potrebbero restare delusi dai seguenti paragrafi.
In aggiunta alle collezioni tradizionali, Groovy permette di considerare anche gli intervalli, come parte integrante del linguaggio. Ovviamente si tratterà di oggetti, ma, soprattutto, godranno di una sintassi particolarmente carina: un Range in Groovy può essere in effetti specificato come:

 (1..12)

La sintassi ('inizio'..'fine') si applica praticamente a qualsiasi implementazione dell'interfaccia Comparable che esponga i metodi next() e previous() (nel caso di range discendenti) che in Groovy corrispondono all'implementazione degli operatori "++" e "--", permettendo quindi di avere ranges anche di tipi complessi quali Date o String.
Come detto in precedenza, il range è un oggetto a tutti gli effetti, anche se la sintassi ne permette una dichiarazione implicita, e può essere interrogato in maniera analoga agli altri contenitori.

(1..12).contains(7)
('a'..'z').size() == 26

La presenza dei ranges come tipi caratteristici del linguaggio rivoluziona la sintassi del ciclo for: in Java è necessario specificare l'inizio dell'intervallo, la condizione di terminazione e la funzione di incremento. In Groovy posso tranquillamente scrivere:

def today = new Date()
def oneYearAgo = today-365;
           
for (day in oneYearAgo..today) {
   System.out.println(day);
}

Che produce come output l'elenco dei giorni dell'anno appena trascorso (notiamo anche il fatto che Groovy aggiunga gli operatori plus() e minus() alla classe Date, e di conseguenza, gli operatori più e meno).
In aggiunta alla parola chiave "in" è possibile utilizzare l'operatore each che risulta particolarmente potente se applicato in congiunzione con un altro costrutto caratteristico di Groovy: le closures.

List

La stessa logica sta dietro all'implementazione di altri tipi di contenitori: per costruire una List in Groovy è sufficiente dichiarare un elenco di oggetti all'interno di parentesi quadre, separati da virgole.

def myList = ['a','b','c','d']
def myLost = [4,8,15,16,23,42]

Ancora una volta, si tratta di oggetti a tutti gli effetti (Java.util.ArrayList, per la precisione), su cui posso invocare metodi ed operatori (omogenei), in maniera analoga a quanto visto per i ranges.

assert myList.size() == 4
assert myLost[3] == 16

Notiamo anche che sparisce la distinzione sintattica tra l'operatore "[ ]" ed il metodo elementAt(), tipico delle Collections: il metodo getAt() corrisponde infatti all'operatore "[ ]" in lettura, mentre il medodo putAt() permette l'assegnamento.

assert myList.size() == 4
assert myLost[3] == 16   // basato su getAt()
myList[4] = 'e'          // basato su putAt()

In realtà gli ingredienti per qualcosa di decisamente cool ci sono tutti, ed in effetti Groovy sfrutta anche l'overloading sugli operatori per permettere la manipolazione di sublists, conversioni da e verso ranges e così via. Notiamo inoltre che ci stiamo muovendo in direzione opposta rispetto ai generics e alla possibilità di tipizzare i nostri contenitori. Groovy adotta la filosofia del duck-typing tipica di Ruby: il controllo dell'applicabilità dei metodi non è delegato al controllo statico sul tipo a tempo di compilazione, ma viene effettuato a run-time. Vedremo nei prossimi articoli della serie che ci sono ottime ragioni per questa scelta.

Maps

Da un punto di vista concettuale, una mappa non è altro che un contenitore di coppie chiave-valore. In Groovy la sintassi per definire una mappa non differisce particolarmente da quanto appena visto.

def myMap = [a:1, b:2, c:3]
assert myMap['a'] == 1
assert myMap.b == 2
assert myMap.get('c') == 3

Particolarmente interessante è la possibilità di referenziare la nostra mappa utilizzando la chiave (b nel nostro esempio) come se fosse un attributo della classe myMap.

Closures

L'ultimo elemento sintattico rilevante di Groovy - e probabilmente il più spiazzante per i programmatori Java - è dato dalle Closures. Le closures (non riesco a chiamarle "chiusure", perdonatemi: è più forte di me) sono frammenti di codice che possono essere applicati ripetutamente in differenti contesti. In generale, sono strumenti che risultano convenienti all'interno di un'iterazione o della gestione di risorse che necessitano di una gestione dedicata. In tutti i contesti in cui operazioni specifiche si mescolano ad operazioni ripetute, in Java si finisce per costruire soluzioni un po' più raffinate basate su

  • template method pattern
  • command pattern
  • inner class anonime

o sulla combinazione delle tre. Ciascuna di queste soluzioni ha vantaggi e svantaggi, ma in generale tutte comportano un certo livello di complessità e difettano in leggibilità. In Groovy, la logica che sta dietro a questi costrutti è già fornita di serie dal linguaggio mediante le closures: frammenti di codice che possono essere manipolati come oggetti. La sintassi è quanto di più conciso si possa immaginare:

def log = ''
(1..10).each { counter -> log += counter }

Le parentesi graffe delimitano la porzione di codice che rappresenta la nostra closure. L'operatore " -> " permette di definire il nome della variabile che rappresenta il parametro della nostra esecuzione (quella ritornata dall'operatore each sul range (1..10). Nel caso volessimo  essere ancora più pigri, il nome di default per il parametro della closure è "it".

def log = ''
(1..10).each { log += it }
assert log == '12345678910'

Il risultato è lo stesso del caso precedente: l'operatore each permette di iterare su tutti gli elementi di un range (ma si tratta solo dell'esempio più semplice), restituendoli uno alla volta. La closure viene applicata su ciascuno degli elementi restituiti, all'interno della closure l'elemento restituito è chiamato "it" oppure con il nome attribuitogli dal programmatore (counter, nell'esempio precedente).
La cosa sembra interessante, ma stiamo solo grattando la superficie: le closures sono oggetti a tutti gli effetti, possiamo associargli un nome e manipolarle, applicarle a oggetti non ancora noti, indipendentemente dal tipo (una sorta di metodi non associati ad una classe... o di funzioni globali non tipate).

Conclusioni

In questo articolo ci siamo concentrati sugli elementi sintattici che rendono Groovy al tempo stesso simile a e differente da Java. La struttura del linguaggio è potente, omogenea ed elegante e permette tempi di apprendimento più brevi rispetto a Java (dopo tutta la fatica che abbiamo fatto, dobbiamo ricominciare da zero?).  Tuttavia non abbiamo ancora risposto alla domanda suprema: "Ma non si poteva fare Grails direttamente in Java?". Per questo sarà necessario esplorare il funzionamento di Groovy ed il paradosso di costruire un linguaggio dinamico a partire da una piattaforma basata sullo static Type Checking. Ma questo sarà argomento del prossimo articolo.

Riferimenti

[1] Il download di Groovy

[2] M. Poppendieck - T. Poppendieck, "Lean Software Development an Agile Toolkit" Addison Wesley

[3] Quentin Tarantino, "Pulp Fiction", 1994

[4] Andrea Nucci, "Ruby - I parte: Introduzione al linguaggio", MokaByte 114, Gennaio 2007

[5] Dierk Koenig, "Groovy in Action", Manning

[6] Venkat Subramanian, "Programming Groovy", Pragmatic Bookshelf

 

 

Condividi

Pubblicato nel numero
132 settembre 2008
Articoli nella stessa serie
Ti potrebbe interessare anche