Reprenant les forces de Lisp en s’appuyant sur la JVM, Clojure peut être une alternative intéressante aux langages actuels. Il se démarque par sa simplicité de lecture malgré son approche fonctionnelle.
Plutôt que d’arpenter les particularités syntaxiques et fonctionnelles de Clojure, cet article se concentre sur le développement pas à pas d’une API REST. Il vise donc à montrer que le langage possède un écosystème suffisamment riche pour répondre à des problématiques réelles.
Ce guide est décomposé en 3 parties. D’abord une présentation du langage et des pré-requis syntaxiques, ainsi que la mise en place de l’environnement et du projet. Puis une description complète des développements pas à pas. La dernière partie évoque les outils de qualité, de migration de la base de données, et la préparation pour la mise en production.
Comprendre les concepts de base
Connaître les principes clés
La toute première version de Lisp voit le jour en 1958 grâce à John McCarthy, et c’est presque 50 ans plus tard, en 2007, que Rich Hikey s’inspire de sa syntaxe pour créer Clojure, sous license Eclipse. Clojure diffère de la plupart des langages par sa notation préfixée et s’inscrit dans le paradigme fonctionnel. Nous nous contenterons de rappeler certains principes clés ici, le sujet de l’article n’étant pas la programmation fonctionnelle. Pour plus de détails, je vous renvoie à cet article du blog Ippon.
Clojure encourage l’utilisation de variables et collections immuables : on ne manipule pas l’état mémoire de la même façon qu’avec un langage impératif classique. Cela permet, entre autres, de créer des systèmes plus adaptés à la programmation concurrente. Il est également possible de faire de la métaprogrammation, c’est-à-dire que l’on peut générer des méthodes en fonction de structures de données. Un autre concept hérité de Lisp est le data as code, c’est-à-dire que Clojure utilise ses structures de données (listes, vecteurs …) pour exprimer du code.
Enfin, Clojure compile vers du bytecode Java, et se base donc sur la JVM. Scala a pu montrer qu’une plateforme de déploiement populaire peut permettre à un langage de faire la différence. Inutile de vendre un environnement de production spécifique pour Clojure à vos clients potentiels. Cela peut faciliter son utilisation en production. De plus, l’inter-opérabilité avec Java est assez simple à mettre en place, de simples importations suffisent. Tout est possible en Clojure : scripting, web scraping, développement de sites web … Le langage se base sur des fondations solides, permet au développeur de travailler avec une certaine liberté, tout en imposant des contraintes raisonnables.
Apprivoiser la Syntaxe
Si vous n’êtes pas familier avec le langage, cette partie vous permettra d’avoir des bases suffisantes avant d’aborder le développement d’une API REST. Si vous souhaitez tester les commandes décrites ci-dessous, vous pouvez utiliser la commande lein repl (Voir la partie “Installer son environnement” plus bas).
Une fois la barrière de la notation préfixée franchie, la syntaxe de Clojure reste assez simple à lire et à comprendre pour des opération simples.
Opérations basiques
Par exemple, si vous souhaitez afficher du texte à l’écran, il faut entrer le code suivant :
(println "Bonjour Ippon")
=> Bonjour Ippon
Si vous souhaitez effectuer des opérations arithmétiques et des tests de base :
(+ 1 1)
=> 2
(- 1 1)
=> 0
(/ 2 4)
=> 1/2
(= 1 1)
=> true
(> 10 50)
=> false
Comme vous l’avez remarqué, on commence toujours par une parenthèse, puis le nom de la fonction. Puis viennent ses paramètres.
Variables
Lorsqu’on évolue dans un langage fonctionnel, on ne crée pas de variables mutables. Plutôt que de manipuler l’état mémoire directement, il faut créer une nouvelle variable à chaque modification. Même si le langage vous permet de fonctionner de façon plus “classique”, je vous conseille de suivre ce principe, votre code sera plus idiomatique.
Il existe différentes façon de déclarer une variable en Clojure. Cependant il est important de bien choisir. La première méthode :
(def mavar "Valeur")
(def mavar2 42)
def permet aussi de définir des variables dynamiques. Je ne vous montrerai pas d’exemples ici mais vous donnerai le conseil suivant : utilisez def pour définir des constantes globales et faire de la configuration. C’est d’ailleurs ce que nous ferons par la suite. Pour des variables utilisées dans vos fonctions, il vaut mieux avoir recours à let.
(let [var1 valeur1
var2 valeur2])
let *offre la possibilité de déclarer plusieurs variables à la fois via des bindings. De plus, les variables déclarées ne le sont que dans le scope des parenthèses du let, par exemple :
(let [var-let "Valeur à afficher"] (println var-let))
=> Valeur à afficher
(let [var1 40
var2 (+ 2 var1)] (println var2))
=> 42
(let [var-let "Valeur à afficher"]) (println var-let)
=> CompilerException java.lang.RuntimeException: Unable to resolve symbol: var-let in this context
Attention à bien fermer vos parenthèses au bon endroit pour éviter de vous retrouver dans le deuxième cas de figure ci-dessus.
Maintenant que nous savons utiliser des variables, concentrons-nous sur la déclaration de fonctions.
Fonctions
Pour définir une fonction en Clojure, la syntaxe est la suivante :
(defn nom-de-la-fonction "Description détaillée" [param-1 param-2] commande-1 commande-2)
Il n’y a pas d’instruction return en Clojure, c’est la dernière instruction exécutée qui correspond à la valeur retournée. Voir les exemples ci-dessous :
(defn add "Effectue une addition" [a b]
(+ a b))
(defn direBonjour "Dit bonjour à la chaîne en entrée" [personne]
(str"Bonjour" personne))
(add 40 2)
=> 42
(println (direBonjour "Ippon"))
=> Bonjour Ippon
Collections
On retrouve dans Clojure la plupart des structures de données courantes : lists, maps, sets … Nous verrons le plus souvent des lists, des vectors, et beaucoup de maps, ces dernières nous servant à représenter le JSON en entrée et en sortie de notre API, ainsi que les différentes configurations.
Une liste est une collection ordonnée. Il y a deux façons de déclarer une liste:
(list 1 2 3 4)
=> (1 2 3 4)
'(1 2 3 4)
=> (1 2 3 4)
Il est ensuite possible d’accéder au premier, dernier élément, et au reste de la liste :
(first (list 1 2 3 4))
=> 1
(last (list 1 2 3 4))
=> 4
(rest (list 1 2 3 4))
=> (2 3 4)
Enfin, on peut construire une nouvelle liste à partir de la tête et du reste grâce à la commande cons:
(cons 1 (list 2 3 4))
=> (1 2 3 4)
Un vecteur est une collection ordonnée comme une liste, mais il est optimisé pour un accès par “position” d’un élément. On entoure un vecteur avec des crochets pour le déclarer :
[1 2 3 4]
=> [1 2 3 4]
On retrouve alors les mêmes fonctions que pour les listes, avec en plus la possibilité de récupérer le n-ième élément, ou de concaténer des vecteurs entre eux :
(first [1 2 3 4])
=> 1
(last [1 2 3 4])
=> 4
(nth [1 2 3 4] 1)
=> 2
(nth [1 2 3 4] 3)
=> 4
(concat [1 2] [3 4])
=> (1 2 3 4)
En Clojure idiomatique, on utilise les listes pour du code, et les vecteurs pour des données. De plus, comme vous l’avez remarqué, la concaténation retourne ce qui ressemble à une liste. Or dans ce cas, c’est une séquence. Une séquence diffère légèrement d’une liste car ses éléments sont évalués de façon lazy (évalué seulement quand c’est nécessaire). Les séquences possèdent d’autres caractéristiques que je ne détaillerai pas ici.
Pour terminer sur les collections, parlons des maps. En Clojure, une map est délimitée par des accolades, et constitue une association clé-valeur classique.
{:key1 "value1" :key2 "value2"}
=> {:key1 "value1" :key2 "value2"}
Les “:” permettent de définir des keywords. Ce n’est pas obligatoire, il est aussi possible de mettre des clés du type de votre choix.
Comme toutes les autres collections en clojure, une map est une fonction. Pour accéder à des éléments, on procède comme suit :
(def map-test {:key1 "value1" :key2 "value2"})
=> {:key1 "value1" :key2 "value2"}
(map-test :key1)
=> value1
Il est bien entendu possible d’effectuer d’autres types d’opérations sur les maps, mais nous n’irons pas plus loin ici.
Un peu de fonctionnel
Je ne vous montrerai ici que deux fonctionnalités offertes par Clojure : map, filter, qui sont familières à ceux qui ont déjà travaillé en Scala/Haskell, ou même avec l’API Stream de Java.
Map permet d’appliquer une fonction sur les éléments d’une collection :
(def map-test {:key1 "value1" :key2 "value2"})
=> {:key1 "value1" :key2 "value2"}
(map-test :key1)
=> value1
Filter permet d’enlever des éléments d’une collection selon un prédicat :
(filter odd? [1 2 3 4 5]) ; Ne garde que les nombres impairs
=> (1 3 5)
(filter #(< % 3) [1 2 3 4 5]) ; Ne garde que les nombres < 3
=> (1 2)
Utilisation des namespaces
Clojure utilise la notion de namespace pour mapper les variables et les méthodes d’un fichier à un autre. Pour ce faire, il faut utiliser la macro ns. Par exemple :
src/clojure_rest/handler.clj
(ns clojure-rest.handler
(:require [compojure.core :refer :all]
[compojure.handler :as handler]
[compojure.route :as route]
[clojure-rest.config.routes :as config-routes]))
(def app config-routes/app-routes)
Cet exemple illustre l’utilisation des :require. Ce mot-clé vous permet d’importer des dépendances dans le namespace actuel. En utilisant :refer :all, je fais référence à l’ensemble des éléments présents dans compojure.core. En utilisant :as>, je serai contraint par la suite de mentionner de quelle dépendance je souhaite utiliser la variable ou fonction.
Attention à l’utilisation de :refer :all, cela peut causer des conflits entre bibliothèques qui importent des éléments aux noms identiques. :refer vous permet donc d’importer seulement ce dont vous avez besoin, comme suit :
(ns votre-namespace (:require [package1.dep.autredep :refer [methode-a-importer]]))
Dans le projet vous verrez également l’utilisation de :use, à titre d’exemple. Lorsque ce mot-clé est utilisé, il fait référence au namespace demandé à l’aide de clojure.core/refer. À vous de choisir votre façon de procéder. Je préfère personnellement l’utilisation de :refer avec :as pour être certain d’appeler la bonne dépendance, mais cela peut alourdir les notations et rendre le code moins lisible.
Installer son environnement :
Clojure
Tout d’abord, Java doit être installé sur votre poste (en version 6, 7 ou 8). Il existe ensuite plusieurs moyens d’utiliser clojure :
- Via un .jar exécutable disponible sur le site officiel de clojure.
- En utilisant Leiningen, un script permettant d’automatiser la gestion d’un projet Clojure
Comme feraient Maven ou Gradle, Leiningen permet au développeur d’injecter des dépendances aisément, mais aussi de mettre en place un projet en se basant sur des templates. Maven et Gradle sont également utilisables et fonctionnent avec la plupart des plugins, à quelques exceptions près. Je vous conseille donc d’utiliser Leiningen pour commencer, car c’est l’outil de build natif de Clojure pour l’instant.
Pour l’installation, rendez-vous sur le Site officiel de Leiningen et suivez les instructions.
Éditeurs
Les EDI et éditeurs les plus populaires vous permettront de développer en Clojure, en utilisant les plugins suivants :
- Cursive pour Intellij, le plugin LaClojure n’étant plus maintenu par Jetbrains
- Counterclockwise pour Eclipse
- Une liste de plugins pour Lisp et Clojure sur Atom, disponibles à cette adresse
Emacs est aussi recommandé car il donne accès à de nombreux plugins liés à Lisp, dont Clojure s’inspire. L'IDE lighttable offre également le support Clojure.
Dans le cadre de cet article, tous les développements ont été effectués sur Atom. Les plugins disponibles permettent de travailler plus vite et d’éviter certaines erreurs, notamment en terme d’auto-complétion des parenthèses, ce qui peut s’avérer fastidieux à faire manuellement. Attention cependant, le plugin ParInfer qui auto-complète les parenthèses et crochets automatiquement peut s’avérer intrusif et fait quelquefois des erreurs. Le support Clojure de base est suffisant car il vous indiquera quand vos parenthèses sont incomplètes. J'utilise également swackets pour colorer chaque paire de parenthèses différemment.
Mettre en place le projet
Importer un template leiningen
En utilisant le template compojure, la bibliothèque de routing utilisée par Clojure, nous pouvons générer un premier jet de notre application. Pour ce faire, lancez la commande suivante à la racine de votre futur projet :
lein new compojure clojure-rest
Cette commande met en place un projet de type “Hello World” avec dossiers source, test, ressource classiques, avec deux routes configurées : affichage d’un hello world sur le “/” et “Not found” sur le reste. Pour lancer votre API, entrez la commande suivante :
lein ring server
Parcourez les fichiers et constatez comment Clojure utilise les namespaces. Notamment, si vous avez nommé votre projet “clojure-rest”, Clojure utilise par convention des underscores dans le nom des dossiers et des tirets dans les namespaces. Rappelez-vous en quand vous créerez de nouveaux dossiers et de nouveaux namespaces.
Ajouter les dépendances du projet
L’API REST développée doit comporter des fonctionnalités permettant de mettre en place des développements propres facilement, tout en répondant aux problématiques de base : support HTTP classique, communication en JSON … L’ensemble des fonctionnalités est décrite dans la partie ”Ajouter les dépendances du projet”.
Le code complet est disponible sur le dépôt GitHub suivant. Deux versions “finales” existent, mais nous ne nous tournerons que vers celle dont le tag est article-ippon-blog_all-features. Pour ceux d’entre vous qui seraient curieux de voir le fonctionnement sans outil de migration ni ORM, mais avec du JDBC “traditionnel”, rendez-vous au commit comportant le tag article-ippon-blog_jdbc-connection.
Pour les développeurs habitués du monde Java, un dépôt GitHub répertorie l’ensemble des librairies et plugins disponibles en Clojure, en faisant un parallèle avec l’équivalent Java. Très utile pour débuter.
Dans la suite de cet article, j’utiliserai une arborescence qui m’est propre, et qui dérive de celle par défaut. Je donnerai les fichiers pour chaque extrait de code, mais je vous invite à suivre sur mon dépôt GitHub pour plus de clarté.
La configuration du build d’un projet Clojure/Lein se fait dans le fichier project.clj. Pour commencer, il faut ajouter le code suivant pour mettre en place toutes les dépendances nécessaires :
project.clj
(defproject clojure-rest "0.1.0-SNAPSHOT"
:description "Simple clojure rest service for ippon article"
:url "https://github.com/matthieusb/clojure-simple-api"
:min-lein-version "2.0.0"
:dependencies [[org.clojure/clojure "1.8.0"]
[org.clojure/tools.logging "0.4.0"]
[compojure "1.6.0"]
[metosin/compojure-api "1.1.11"]
[prismatic/schema "1.1.6"]
[cheshire "5.8.0"]
[org.clojure/java.jdbc "0.7.1"]
[com.h2database/h2 "1.4.196"]
[korma "0.4.3"]
[ragtime "0.7.1"]
[ring/ring-json "0.4.0"]
[ring/ring-defaults "0.3.1"]
[jarohen/nomad "0.7.3"]
]
Faisons un rapide descriptif de chaque élément :
- org.clojure/clojure: support clojure de base
- org.clojure/tools.logging: support d’un logger simple
- metosin/compojure-api: facilite l’utilisation de la librairie de routing compojure et l’intégration de swagger
- primatics/schema: support bean validation, utilisée par compojure-api
- cheshire: une librairie performante pour manipuler du JSON
- org.clojure/jdbc: support JDBC, utilisé dans les tests
- com.h2database/h2: driver pour la connexion à une base h2
- korma: support orm pour communiquer avec notre base plus facilement
- ring/ring-json et ring/ring-defaults: abstraction du serveur HTTP et support du JSON
- jarohen/nomad: utilitaire de configuration depuis un fichier de ressources
Par la suite, nous pouvons ajouter quelques plugins et les configurer, toujours dans le même fichier :
project.clj
:plugins [[lein-ring "0.12.1"]
[lein-pprint "1.1.2"]
[lein-cloverage "1.0.9"]
[jonase/eastwood "0.2.4"]
[lein-bikeshed "0.4.1"]
[venantius/ultra "0.5.1"]
]
:profiles { :test {:resource-paths ["resources/test"]}
:dev
{:resource-paths ["resources/dev"]
:dependencies [[javax.servlet/servlet-api "2.5"]
[ring/ring-mock "0.3.1"]
]}}
:ring { :handler clojure-rest.handler/app}
Note : Attention à bien ajouter la parenthèse fermante en fin de fichier pour que le fichier soit valide.
J’attire votre attention sur les plugins suivants :
- lein-cloverage: gestion de la couverture des tests via lein
- venantius-ultra: résultat des tests erronés plus clair et en couleur, entre autres
Le plugin ring permet d’intégrer notre serveur HTTP plus facilement pour le lancer directement depuis lein. pprint facilite l’affichage lors du debug. Nous verrons les plugins eastwood et bikeshed plus loin dans cet article (Voir la partie “Configurer les linter”).
L’entrée :profiles définit les chemins des ressources selon que l’on soit en test ou en dev. Il est également possible de rajouter un profil de production, qui ne se déclenche qu’à l’exécution du .jar final. Les plugins fournis par le profil dev assurent un support du mocking pour nos tests et l’interop avec servlets Java, ce qui est transparent pour nous.
Enfin, le mot-clé :ring définit le handler, c’est le point d’entrée de notre API REST, qui reste à paramétrer.
C’est fini pour aujourd’hui
Cette partie s’arrête ici. Maintenant que notre squelette de projet est prêt, il nous reste à effectuer les développements. La deuxième partie de cet article sera bientôt disponible.