Introduction aux DSL en Groovy

Si vous ne connaissez pas encore Groovy, voici une bonne occasion de vous y mettre. Au travers d’un exemple, je vais vous présenter plusieurs des concepts offerts par le langage qui vont nous permettre de créer assez simplement un DSL.

Qu’est-ce qu’un DSL ?

Le terme DSL (Domain Specific Language) est apparu il y a quelques années. Il désigne un langage dont les spécifications – et en premier lieu la syntaxe – sont spécifiquement adaptées à un domaine fonctionnel donné contrairement aux langages de programmation généralistes. Un certains nombre de langages que nous connaissons bien sont des DSL même si nous ne sommes pas forcément habitués à les désigner comme tels. Par exemple, HTML est conçu pour la description de pages Web, les expressions régulières sont destinées à décrire des motifs de chaînes de caractères et le SQL permet d’exprimer des requêtes vers une base de données. Martin Fowler donne une bonne définition des DSL dans un petit article sur son blog.
Le langage Groovy offre des possibilités très avancées dans ce domaine. Les produits les plus connus du monde Groovy définissent souvent des DSL spécifiques à leurs besoins. C’est notamment le cas du framework Grails et de l’outil de build Gradle.

La problématique

Nous allons créer un DSL adapté à la description d’un email. Une fois le DSL défini, il s’agira de l’interfacer avec une API d’envoi d’emails, en l’occurrence celle de Spring. L’objectif est de pouvoir envoyer des emails depuis notre code Groovy à l’aide d’une syntaxe intuitive et adaptée à ce besoin.
Voici un exemple de code que je souhaite écrire pour envoyer un email.

Email.send {
	from "foo.bar@zenika.com"
	to "toto@zenika.com"
	cc "titi@zenika.com"
	cc "tutu@zenika.com"
	bcc "tata@zenika.com"
	subject "Ouaiiis !!! un mail envoyé via un DSL en Groovy"
	body "Pas grand chose à dire..."
	attach "file:/E:/file1.txt"
	attach "file:/E:/file2.txt"
}

Il peut y avoir plusieurs lignes to, cc et bcc ; une par destinataire. Il en va de même pour les pièces jointes, une ligne par fichier. L’ordre des lignes est sans importance. Par exemple la ligne from pourrait tout à fait se trouver en dernier et plusieurs lignes to ne sont pas nécessairement adjacentes.
Le bloc de code se trouvant à l’intérieur de la paire d’accolades est une closure Groovy.

Un aperçu des closures

Avant de rentrer dans le vif du sujet, je souhaite introduire les closures pour ceux qui ne seraient pas familiarisés avec ce concept. Pour faire simple, il s’agit d’un bloc de code qui est déclaré pour une exécution ultérieure. Ce bloc de code peut être assigné à une variable afin de pouvoir être référencé par la suite.
On peut voir cela comme une sorte de pointeur sur une fonction mais avec un comportement plus proche d’une classe interne que d’une méthode, notamment au niveau de l’accès aux variables externes. Par exemple, le code d’une closure peut utiliser toutes les variables accessibles depuis de contexte de déclaration de la closure. En terme d’écriture de code, une classe anonyme Java est ce qui se rapproche le plus d’une closure. Elle peut également accéder aux variables externes avec par contre une restriction, les variables doivent avoir été déclarées final.
Une closure peut prendre zéro, un seul ou plusieurs paramètres.
Comme un exemple clair vaut mieux que de longues explications, voici la déclaration d’une closure à un paramètre en Groovy.

def addTva = { prixHt ->
	prixHt * 1.196
}

 
Un appel à cette closure peut être écrit de deux manières différentes

avec la même syntaxe que pour un appel de méthode

en invoquant la méthode call(Object...) de la classe groovy.lang.Closure

prixTtc = addTva(20)
prixTtc = addTva.call(20)

Nous allons plus tard nous apercevoir que ce mécanisme sera le point central du DSL que nous allons construire.

Implémentation du DSL

Il existe plusieurs façons d’implémenter un DSL en groovy, en utilisant les caractéristiques dynamiques du langage ou encore en opérant des transformations AST (abstract syntax tree). Nous allons voir ici une méthode relativement simple qui se rapproche du pattern Builder en Java dans le sens ou nous allons créer un objet qui va stocker des informations grâce à des appels de méthodes successifs pour ensuite produire un résultat.
A titre de comparaison, voici à quoi ressemblerait l’utilisation d’un builder en Java.

new EmailBuilder()
	.from("foo.bar@zenika.com")
	.to("toto@zenika.com")
	.subject("Ouaiiis !!! Un mail envoyé via un DSL en Groovy")
	.body("Pas grand chose à dire...")
	.send();

 
Afin de découpler notre DSL de l’implementation du gestionnaire d’envoi d’emails que nous allons écrire ensuite, commençons par définir une interface qui déclare une unique méthode permettant d’envoyer un email. Nous fournirons plus tard une implémentation de cette interface.

interface EmailSender {
	def send(Email email)
}
class Email { }

 
Nous pouvons à présent passer aux choses sérieuses et nous lancer dans l’implémentation du DSL.
Nous allons créer une classe Email qui contient :

Pour chacun des mots-clés qui constituent notre langage une méthode du même nom (from, to, cc, bcc, subject, body, attach) qui prend une chaîne de caractères en paramètre

Un attribut de type EmailSender (dans lequel une instance de la classe d’implémentation sera plus tard injectée par Spring)

Une méthode statique send qui sera le point d’entrée de notre DSL

Chacune des méthodes du DSL va se contenter de stocker dans un champ de la classe la valeur reçue en paramètre. Ainsi, on pourra par la suite passer en paramètre de la méthode EmailSender.send(Email) la référence de l’instance de la classe Email qui contiendra toutes les informations.

class Email {
	private String from
	private def to = []
	private def cc = []
	private def bcc = []
	private String subject
	private String body
	private def attachedFile = []
	private static EmailSender sender
	def from(String from) {
		this.from = from
	}
	def to(String recipient) {
		to << recipient
	}
	def cc(String recipient) {
		cc << recipient
	}
	def bcc(String recipient) {
		bcc << recipient
	}
	def subject(String subject) {
		this.subject = subject
	}
	def body(String body) {
		this.body = body
	}
	def attach(String uri) {
		attachedFile << uri
	}
	def send() {
		sender.send(this)
	}
}

Pour chaque envoi d’email on va construire une instance de la classe Email et chacune des instructions présentes dans la closure va devenir un appel de méthode sur cette instance. Pour cela, on utilise la caractéristique de délégation des closures.
La classe Closure dispose d’un champ delegate qui contient la référence de l’objet sur lequel vont s’appliquer les appels de méthodes fait à l’intérieur de la closure. Par défaut, cet attribut est valorisé avec la référence de l’instance qui a exécuté la déclaration de la closure. Sa valeur peut être modifiée et c’est précisément ce que nous allons faire ici avant d’appeler la closure en lui assignant la référence de l’objet de type Email nouvellement créé.

def static send(Closure closure) {
	Email email = new Email()
	closure.delegate = email       // Mise en place de la délégation à l'objet email
	closure.call()                     // Appel de la closure
	email.send()
}

Rendons le constructeur privé car il n’y a aucune raison d’instancier la classe Email en dehors de la méthode send(Closure)

private Email() {
}

Ecrivons à présent un stub de l’interface EmailSender afin de pourvoir d’ores et déjà tester notre DSL.

class EmailSenderStub implements EmailSender {
	def send(Email email) {
		println email
	}
}

Etant donnée l’implémentation du stub, il est évidement souhaitable d’implémenter la méthode toString() sur le classe Email.

@Override
public String toString() {
        """
      from     : ${from}
      to       : ${to}
      cc       : ${cc}
      bcc      : ${bcc}
      subject  : ${subject}
      body     : ${body}
      attached : ${attachedFile}
     """
}

Injectons une instance du stub dans l’attribut Email.sender.

Email.sender = new EmailSenderStub()

Nous sommes maintenant prêt à tester notre DSL

Email.send {
	from "foo.bar@zenika.com"
	to "toto@zenika.com"
	cc "titi@zenika.com"
	cc "tutu@zenika.com"
	bcc "tata@zenika.com"
	subject "Ouaiiis !!! un mail envoyé via un DSL en Groovy"
	body "Pas grand chose à dire..."
	attach "file:/E:/file1.txt"
	attach "file:/E:/file2.txt"
}

Il est à noter ici que l’ordre dans lequel les champs sont renseignés lors de la construction de l’email est sans importance puisque chaque appel de méthode va uniquement permettre de stocker une nouvelle information. Et ce n’est qu’une fois que la closure aura terminé sont exécution que l’appel à la méthode send() sera effectué.
On peut voir que la closure passée en paramètre de la méthode statique Email.send(Closure) est une succession d’appels aux différentes méthodes déclarées sur la classe Email, d’où la nécessité de déléguer à celle-ci les appels de méthodes fait à l’intérieur de la closure.
La syntaxe de Groovy, plus souple que celle de Java nous autorise à supprimer les parenthèses des appels de méthodes et les points-virgules en fin d’instruction. Grâce à cela, la syntaxe du DSL se rapproche plus du langage naturel. A titre de comparaison, voici le même code écrit dans une syntaxe à la façon Java.

Email.send ({
	from("foo.bar@zenika.com");
	to("toto@zenika.com");
	cc("titi@zenika.com");
	cc("tutu@zenika.com");
	bcc("tata@zenika.com");
	subject("Ouaiiis !!! un mail envoyé via un DSL en Groovy");
	body("Pas grand chose à dire...");
	attach("file:/E:/file1.txt");
	attach("file:/E:/file2.txt");
});

En terme d’exécution ces deux syntaxes sont strictement équivalentes.

Implémentation du gestionnaire d’envoi d’emails

Voici une implémentation simple de l’interface EmailSender utilisant l’API de Spring.

import org.springframework.mail.javamail.JavaMailSender
import org.springframework.mail.javamail.JavaMailSenderImpl
import org.springframework.mail.javamail.MimeMessageHelper
class EmailSenderImpl implements EmailSender {
	private static JavaMailSender sender
	EmailSenderImpl() {
		sender = new JavaMailSenderImpl()
		sender.host = "smtp.myserver.fr"
	}
	def send(Email email) {
		MimeMessageHelper message = new MimeMessageHelper(sender.createMimeMessage(), true)
		message.setText(email.body, false)
		message.setFrom(email.from)
		message.setSubject(email.subject)
		message.setTo(email.to as String[])
		message.setBcc(email.bcc as String[])
		message.setCc(email.cc as String [])
		email.attachedFile.each {
			File file = new File(new URI(it))
			message.addAttachment(file.name, file)
		}
		sender.send(message.mimeMessage)
	}
}

Jusqu’à présent nous n’avions d’adhérence que sur les librairies standard Groovy et Java. L’implémentation ci-dessus tire de nouvelles dépendances. Nous allons indiquer à groovy la liste des dépendances afin qu’il puisse les résoudre durant l’exécution en annotant la classe EmailSenderImpl.

@Grapes([
	@Grab(group='org.springframework', module='spring-beans', version='3.1.0.RELEASE'),
	@Grab(group='org.springframework', module='spring-context-support', version='3.1.0.RELEASE'),
	@Grab(group='javax.mail', module='mail', version='1.4.4')
])

Pour finir, on remplace le stub par l’implémentation réelle.

Email.sender = new EmailSenderImpl()

Pour conclure

J’ai souhaité aborder le sujet de manière pratique sans véritablement entrer dans la mécanique interne du langage qui n’est pas forcément évidente à appréhender en première approche. Nous avons pu voir que Groovy est adapté à l’implémentation de DSL de part sa syntaxe, son côté dynamique et ses closures.
Un DSL étant par définition spécifique, nos éditeurs et autres ateliers de développement ne supportent pas nativement ces langages pour fournir des services de coloration syntaxique, d’auto-complétion ou encore de documentation en ligne. Pour résoudre ce problème, Groovy permet de définir des grammaires pour les DSL appelées DSLD (le dernier “D” pour “Description”) qui sont exploitées nativement par le plugin Groovy-Eclipse, ce qui offre un bon point en plus pour Groovy en matière de DSL.
Bref, cet article ne montre évidement qu’une petite partie de ce qu’il est possible de faire mais j’espère vous avoir convaincu de l’intérêt de Groovy pour développer des DSL.
Voici le code source complet de cet article.

Auteur/Autrice

Laisser un commentaire

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.

%d blogueurs aiment cette page :