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.
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'
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
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 ...
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.
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.
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).
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.
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.
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 = []
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; }
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);
}
}
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
}
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é.
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.
Nous allons faire de même avec le TagCloudServiceTest
et TagCloudService
Utilisez déjà les simplifications suivantes : point virgule, assert et each {}
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.
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)
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 : )
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.
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()
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
A partir du language est né toute une famille d'outils