A dynamic language for the Java platform

Groovy est un language alternatif qui tourne sur la jvm. Il profite ainsi de la plateforme et des librairies.

C'est un language dynamique dont la synthaxe est proche de java. C'est assez simple d'apprendre le groovy à partir du java.

L'intérêt d'un language dynamique est sa grande souplesse. Groovy intègre aussi de nombreuse fonctionnalité qui permettent d'avoir une très grande productivité.

Il existe de nombreux autres languages alternatifs pour la jvm

Nous allons voir les forces de groovy.

Branche git

On crée la branche mongo à partir de la branche master.

$ git checkout master
Switched to branch 'master'
$ git checkout -b groovy
Switched to a new branch 'groovy'

Script groovy

Pour démarrer nous allons utiliser la capacité de groovy à exécuter du code à la volée.

On installe groovy avec le gestionnaire de package

$ brew install groovy
$ groovy -version
Groovy Version: 2.1.6 JVM: 1.7.0_21 Vendor: Oracle Corporation OS: Mac OS X

Maintenant on peut exécuter du code

$ echo "println 'hello groovy'" > hello.groovy
$ groovy hello.groovy
hello groovy
println est un raccourci pour System.out.println

La command groovy compile le code groovy et l'exécute.

Voici un exemple un peu plus riche

$ echo "def fib(n) {n<2 ? 1 : fib(n-1)+fib(n-2)}" > fib.groovy
$ echo "(0..10).eachWithIndex { it, i -> println ((' ' * i) + fib(it)) }" >> fib.groovy
$ groovy fib.groovy
...

Plugin groovy

Afin de pouvoir intégrer du groovy dans notre projet, nous allons utiliser le plugin Groovy-Eclipse.

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.0</version>
    <configuration>
        <compilerId>groovy-eclipse-compiler</compilerId>
    </configuration>
    <dependencies>
        <dependency>
            <groupId>org.codehaus.groovy</groupId>
            <artifactId>groovy-eclipse-compiler</artifactId>
            <version>2.8.0-01</version>
        </dependency>
        <dependency>
            <groupId>org.codehaus.groovy</groupId>
            <artifactId>groovy-eclipse-batch</artifactId>
            <version>2.1.5-03</version>
        </dependency>
    </dependencies>
</plugin>

Et il faut aussi ajouter la librairie contenant groovy

<dependency>
    <groupId>org.codehaus.groovy</groupId>
    <artifactId>groovy-all</artifactId>
    <version>2.1.7</version>
</dependency>

A partir de là, la documentation du plugin dit

The simplest way to set up your source folders is to do nothing at all: add all of your Groovy files to src/main/java and src/test/java. This requires absolutely no extra configuration and is easy to implement. However, this is not a standard maven approach to setting up your project.

Nous ne serons pas vraiment dans les clous maven, mais cela sera plus pratique.

TagCloudTest.groovy

Nous allons en premier Transformer notre test TagCloudTest

La première chose à faire est de renommer TagCloudTest.java en TagCloudTest.groovy

Après ça, on note que les tests passent toujours sous intellij et maven

$ mvn -Dtest=TagCloudTest test
...
-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running fr.todooz.util.TagCloudTest
Tests run: 10, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.642 sec
...

Simplement, les classes java sont 100% compatibles avec la synthaxe groovy.

assert

On peut changer les Assert de junit en utilisant le mot clé assert de groovy

Assert.assertEquals(2, tagCloud.size());

devient

assert tagCloud.size() == 2

C'est plus simple et en cas d'erreur, le log est plus lisible

Assertion failed:

assert tagCloud.size() == 3
       |        |      |
       |        2      false
       TagCloud{tags=[java, python]}

Faites de même avec toutes les assertions (on note au passage que les ; en fin de ligne sont optionnels).

Visibilité

En groovy, il n'y a que 3 modificateurs de visibilité possibles :

Il n'y a donc pas de notion de package protected comme en java et public est le modificateur par défaut.

public class TagCloudTest devient donc simplement class TagCloudTest

Faites de même pour toutes les méthodes.

def

Le mot clé def permet d'omettre le type.

Par exemple, au lieu de String value = "sample", on peut écrire def value = "sample.

def est une sorte de synonyme pour Object.

Cela permet d'omettre le type. On peut donc appeler une méthode sur un objet sans vraiment savoir son type.

If it walks like a duck and quacks like a duck, then it's a duck

Duck typing

Il faut attendre le runtime afin de savoir si appel est valide.

Par exemple pour le test add()

@Test
void add() {
    TagCloud tagCloud = new TagCloud();

    tagCloud.add();
}

On peut écrire

@Test
void add() {
    def tagCloud = new TagCloud()

    tagCloud.add()
}

Ici le gain est faible, mais intellij continue de proposer de la complétion car il est capable d'inférer le type de tagCloud.

En pratique, il est déconseillé de l'utiliser pour les paramètres de méthodes, car cela peut rendre le code plus complexe à lire.

TagCloud.groovy

Comme pour le test, on passe le TagCloud en groovy

Appliquez tous les points mis en place pour le tests.

[ ]

[] est une notation qui en fait indique un ArrayList<Object>

On peut changer la déclaration de la liste des tags

private def tags = []

La vérité

Les tests booléens sont simplifiés (voir Groovy Truth)

Par exemple, au lieu de

if (tags == null) {
    return;
}

On peut écrire

if (!tags) {
    return;
}

each() { }

Pour parcourir une liste, au lieu de faire un for, on peut utiliser un each

tags.each {
    if (canAddTag(it)) {
        this.tags.add(it);
    }
}

it est le nom par défaut de la variable d'itération.

On peut le changer pour plus de lisibilité

tags.each { tag ->
    if (canAddTag(tag)) {
        this.tags.add(tag);
    }
}

@ToString

Il est possible de demander a groovy de générer la méthode toString() en annotant la classe avec @ToString

@ToString
class TagCloud { ... }

==

En groovy, == est un équivalent de equals sans risque de null pointer (on dit qu'il est null safe).

Au lieu de

private boolean canAddTag(String tag) {
    return !contains(tag) && tag != null && !"".equals(tag);
}

On peut donc écrire

private boolean canAddTag(String tag) {
    return !contains(tag) && tag != null && tag != ""
}

Mais comme null et "" sont faux en groovy

private boolean canAddTag(String tag) {
    return !contains(tag) && tag
}

return

Le mot clé return est optionnel. En son absence, groovy retourne la dernière valeur évaluée de la méthode.

Par exemple, pour ma méthode size()

int size() { tags.size() }

Faites de même lorsque c'est approprié.

Au résultat

Ma classe finale est la suivante

@ToString
class TagCloud {
    def tags = []

    void add(String... tags) {
        tags?.each {
            if (canAddTag(it)) {
                this.tags.add(it)
            }
        }
    }

    int size() { tags.size() }

    boolean contains(String tag) { tags.contains(tag) }

    void top(int count) {
        count = Math.max(count, 0)
        count = Math.min(count, tags.size())

        tags = tags.subList(0, count)
    }

    void shuffle() { Collections.shuffle(tags) }

    private boolean canAddTag(String tag) { tag && !contains(tag) }
}

En gagnant en lisibilité, la version groovy est aussi plus courte.

TagCloudServiceTest.groovy

Nous allons faire de même avec le TagCloudServiceTest et TagCloudService

Utilisez déjà les simplifications suivantes : point virgule, assert et each {}

Constructeur [:]

Pour construire une Task, on peut utiliser le code suivant

new Task(date: new Date(), title: 'Read Effective Java', text: "Read Effective Java before it's too late", tags: tags)

C'est équivalent à

Task task = new Task()
task.date = new Date()
task.title = 'Read Effective Java'
task.text = "Read Effective Java before it's too late"
task.tags = tags

La notation [prop: 'value'] construit en fait une Map

Lorsque l'on passe une Map à un constructeur, groovy va affecter les propriétés de la classe en lisant la Map.

Accès aux propriétés

En groovy, l'accès aux propriétés est simplifiée. Par exemple, au lieu d'écrire :

sessionFactory.getCurrentSession()

on peut écrire

sessionFactory.currentSession

Automatiquement, groovy va utiliser l'accesseur (getXXX())

En fait, voici tout ce que fait groovy pour les beans (Groovy beans)

Closures

Une closure est comme un bloc de code ou un pointeur sur une méthode. Par exemple

def addOne = { param -> param + 1 }
assert addOne(2) == 3

Il est donc facile de déclarer un morceau de code et de l'appeler.

C'est comme ça que fonctionne each :

Iterates through an aggregate type or data structure, passing each item to the given closure. Custom types may utilize this method by simply providing an "iterator()" method. The items returned from the resulting iterator will be passed to the closure.

C'est ce que nous avons fait dans le TagCloudServiceImpl.buildTagCloud().

public TagCloud buildTagCloud() {
    TagCloud tagCloud = new TagCloud()

    findTags().each {
        tagCloud.add(StringUtils.split(it, ","))
    }

    tagCloud
}

Comme each, il existe une méthode collect (voir le gdk) qui collecte les valeurs retournées et retourne une liste.

On essaye donc d'écrire

findTags().collect { StringUtils.split(it, ",") }

Mais comme split renvoie un tableau de string, on ne peut pas injecter directement le résultat dans le TagCloud

En s'aidant des méthodes flatten() et unique() et en utilisant tokenize() on obtient le résutlat suivant

public TagCloud buildTagCloud() {
    new TagCloud(tags: findTags().collect { it.tokenize(',') }.flatten().unique())
}

On fini donc avec un one liner dont la lisibilité peut être débatue : )

Task.groovy

Coté objet du domaine, on peut profiter à plein des Groovy Beans.

@Entity
@Table(name = "task")
@ToString
public class Task {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    Long id

    @Column
    Date createdAt = new Date()

    @Column
    @NotBlank
    @Size(min = 1, max = 255)
    String title

    @Column(length = 4000, nullable = true)
    @Size(max = 4000)
    String text

    @Column
    @NotNull
    Date date = new Date()

    @Column(nullable = true)
    String tags

    String[] getTagArray() { tags.tokenize(',') }
}

Tous les setters et getters sont générés automatiquement et notre classe est beaucoup plus synthétique.

Intérêt de groovy

Avec groovy, il est possible d'écrire beaucoup moins de code et d'être plus productif.

Mais cela vient avec un prix :

Un volume de code important en groovy est donc plus difficile à maintenir et à faire évoluer.

Il demande alors une plus grande maitrise individuelle et une meilleure couverture de tests

Il est un point ou groovy excelle : l'écriture de script

Voici un exemple de script utilisé en production pour mettre à jour les applications

def war = new File(tempDir, "ROOT.war")

new NexusArtifact(name: name, version: version, extension: 'war').downloadTo(war)

stopTomcat()

updateTomcatConf(version, tomcatConfResolver)

new File("$webappDir/webapps").deleteDir()
new File("$webappDir/work").deleteDir()

new File("$webappDir/webapps").mkdirs()
new File("$webappDir/work").mkdirs()

log "Copying war to $webappDir/webapps/ROOT.war"

new AntBuilder().move(file: war, toFile: new File(webappDir, "webapps/ROOT.war"), overwrite: true)

startTomcat()

Fonctionnalités du language

Le labguage groovy est plus riche que le language java.

Voici un guide du language groovy pour les développeurs java

On y retrouve les points suivants :

La liste est déjà longue et on peut rajouter la surcharge de certains opérateurs et le méta programming

L'ecosystème groovy

A partir du language est né toute une famille d'outils