Tutoriel pour apprendre à utiliser le langage Kotlin dans une application backend

David Wursteisen

Depuis l'annonce par Google du support de Kotlin sur Android, le langage de l'éditeur tchèque fait le bonheur des développeurs Android. Kotlin est puissant, simple à mettre en œuvre et apporte de nombreuses nouveautés. Pourtant, les développeurs mobiles n'ont pas l'exclusivité de ce langage.

Kotlin peut en effet être utilisé dans n'importe quel applicatif Java. Le langage peut même compiler du code Kotlin en JavaScript voire en natif si le cœur vous en dit. Mais c'est le point qui nous intéressera le plus ici : comment positionner ce langage par rapport à Java dans une application Backend ? S'intègre-t-il facilement avec une application existante ? Comment l'écosystème Java actuel va-t-il assimiler ce nouveau langage ? Peut-il avoir un impact sur l'architecture de nos applicatifs, mais surtout, qu'apporte ce langage qui soit suffisamment intéressant pour l'intégrer dans ses futurs projets voire ses projets actuels ?

Commentez ♪

- Titre : Tutoriel pour apprendre à utiliser le langage Kotlin dans une application backend

- Auteur : David Wursteisen

- Parution : 16 mai 2018

- Licence : Copyright ® 2018 Soat. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Compatibilité

- I-A. Bytecode

- I-B. Frameworks

- I-B-1. Spring Framework

- I-B-2. JPA

- I-B-3. Mockito

- II. Features

- II-A. NullPointer Safety

- II-B. Alias

- II-C. TypeAlias

- III. Architecture

- IV. Écosystème

- IV-A. Spring 5

- IV-B. Gradle

- V. Adoption massive ?

I - Compatibilité

I-A - Bytecode

Comment s'intègre le code Kotlin avec le code Java ? Peut-on mixer Java et Koltin et si oui, quelles en sont les limites ? Kotlin peut cohabiter sans problème avec Java. C'est-à-dire que vous pouvez progressivement ajouter du code Kotlin dans votre application, sans avoir de problème. Appeler du code Java depuis votre code Kotlin se fait facilement et le contraire tout aussi facilement. Toutefois, certaines fonctionnalités spécifiques à Kotlin ne sont pas accessibles telles quelles depuis une classe Java.

Prenons par exemple les méthodes d'extensions. Une méthode d'extension offre la possibilité de rajouter une méthode sur une API sans modifier cette même API. L'exemple ci-dessous rajoute la méthode toColor() sur le type String. Cette méthode devient alors directement accessible sur les objets de type String en Kotlin.

1. // ColorExts.ktfun

2. String.toColor(): Color = Color.valueOf(this)

3. val color = "#FFFFFF".toColor()

Mais qu'en est-il en Java ? Cette méthode d'extension est utilisable en Java, mais elle ne le sera pas directement depuis le type String. Le compilateur va en effet transformer notre méthode d'extension en méthode static.

1. // ColorTest.java

2. Color color = ColorExtsKt.toColor("#FFFFFF");

3. Assert.assertEquals(color, Color.WHITE);

D'autres fonctionnalités, quant à elles, pour des raisons techniques ne vont tout simplement pas être disponibles depuis une classe Java. Ces fonctionnalités sont rares et concernent un usage avancé de Kotlin. L'exemple typique est l'utilisation des méthodes inlinées. Une fonction inlinée est une fonction dont le corps de la méthode va être copié à l'emplacement de l'appel. Cette notion n'existe pas en Java et n'est pas transposable. De par ce fait, vous ne pouvez pas appeler une fonction inlinée en Kotlin depuis votre code Java. Pour vous empêcher de le faire, Kotlin va marquer la fonction comme private final au niveau du code compilé la rendant donc inaccessible pour les développeurs Java.

1. // ServiceLocator.kt

2. ?object ServiceLocator {

3. inline fun <reified T> locate(): T {

4. return this[T::class.java]

5. }

6. }

8. ?// ServiceLocator.class

9. public final class ServiceLocator {

10. ?

11. ?private final <T> T locate() {

12. ;

13. Intrinsics.reifiedOperationMarker(4, "T");return (T)get(Object.class);

14. }

15. }

Sur le langage en lui-même, Kotlin s'exploite parfaitement avec Java, sans interférer. L'intégration de Kotlin dans un projet existant est donc simple : intégrer la chaîne de compilation spécifique au langage. Le plus dur étant fait, vous pouvez alors commencer de nouveaux développements en Kotlin et migrer progressivement les anciens développements vers ce nouveau langage. On préfèrera migrer ici du code mis à jour : migrer du code Java en Kotlin uniquement pour la forme n'apporte pas de valeur ajoutée.

I-B - Frameworks

Grâce à la compatibilité Java/Kotlin, vous êtes assurés de pouvoir utiliser Kotlin sans douleur sur votre projet. Toutefois les conventions par défaut de Kotlin ne sont pas les mêmes que celles de Java. Cela peut-il poser des problèmes d'intégration avec différents Frameworks, qui eux, ont été conçus pour Java ?

I-B-1 - Spring Framework

Spring est parfaitement compatible avec Kotlin à condition de faire quelques adaptations au niveau de vos classes de configuration Spring écrites en Kotlin. En effet, Spring crée un proxy sur certaines classes via CGLib. Pour créer ces proxies, les classes ne doivent pas être marquées comme final. Or justement, par défaut, les classes sont final avec Kotlin.

C'est un problème que l'on rencontre également avec Mockito (voir ci-dessous). Il s'agit alors soit de marquer votre classe Kotlin comme open ; soit d'utiliser un plugin développé par Jetbrains pour votre outil de build pour justement marquer, automatiquement, les classes Kotlin comme non finales. À vous de choisir la solution qui vous semble la plus adaptée.

1. @Configuration

2. open class MyConfiguration {

4. }

L'autre point d'intention est l'injection des différentes dépendances. La dépendance va être injectée en tant que property d'une classe. Naïvement on pourrait marquer la property comme val. Dans cette configuration, Kotlin vous imposera d'initialiser le champ avec une valeur qui ne pourra être changée. Cette option n'est pas envisageable. En effet, la classe doit être créée puis les champs vont être injectés.

De même, l'utilisation du marqueur var n'est pas suffisante : cela implique toujours de forcer l'initialisation avec une valeur, qui reste pour l'instant inconnue. Cette valeur peut être null. Mais votre type doit alors devenir nullable (exemple : MyRepository? à la place de MyRepository). Cela fonctionne, mais l'utilisation reste fastidieuse : à chaque utilisation de votre dépendance, vous serez obligés de vérifier sa nullité.

Kotlin propose le marqueur lateinit pour indiquer qu'une variable sera déclarée plus tard. À la charge du développeur d'orchestrer correctement l'initialisation de cette variable avant son utilisation. Le code devient un petit peu plus verbeux, mais est complètement fonctionnel avec le Framework Spring.

1. @Repository

2. class MyService {

4. @Autowired

5. private lateinit var repository: MyRepository

6. // ...

7. }

I-B-2 - JPA

Comment se comporte une entité JPA avec Kotlin ? Sans problème ! Prenons par exemple l'entité ci-dessous en Java :

1. @Entity

2. class Example {

4. @Id

5. private String id;

7. private String otherField;

9. public String getId() {

10. return id;

11. }

13. public String getOtherField() {

14. return otherField;

15. }

16. }

Cette classe peut être transformée en Kotlin de cette manière :

@Entity
data class Example(@Id var id: String, var otherField: String)

Si vous essayez d'utiliser le code tel quel, une erreur sera tout de même lancée au moment de la désérialisation de l'objet. Il manque un constructeur par défaut qui est, dans la version Kotlin, manquant.

1. @Entity

2. data class Example(@Id var id: String = "", var otherField: String = "")

Le constructeur par défaut est nécessaire pour que l'implémentation de JPA instancie la classe (ici Example) puis initialise les différents champs. Pour que cela soit également fonctionnel avec votre code, vous pouvez également utiliser le marqueur lateinit, comme vu précédemment. Mais cela ne se fera pas sans conséquence. Votre classe ne pourra plus être une data class, vos champs étant marqués par lateinit. De plus, il vous faudra spécifier explicitement le constructeur par défaut que seul JPA utilisera.

1. @Entity

2. class Example {

3. constructor() { }

5. @Id

6. lateinit var id: String

8. lateinit var otherField: String

9. }

Tout de suite, on arrive à un code très proche de la version Java, avec beaucoup de bruit lié à du code technique. De plus, le constructeur par défaut sera sûrement marqué comme non utilisé par votre IDE : il serait tentant de le supprimer. Même si cette alternative fonctionne, elle ne contentera pas tout le monde, et ce à juste titre.

I-B-3 - Mockito

Mockito est une bibliothèque de mock : vous pouvez configurer le comportement d'une classe ou d'une interface pour un test spécifique. Mais pour cela, Mockito présente quelques contraintes : les classes ne doivent pas être « final ». Cette limitation empêche de mocker directement les classes créées en Kotlin, qui sont par défaut « final ».

1. org.mockito.exceptions.base.MockitoException:

2. Cannot mock/spy class com.example.account.MockitoTest$RemoteServiceImpl

3. Mockito cannot mock/spy following:

4. - final classes

5. - anonymous classes

6. - primitive types

Deux solutions s'offrent alors à vous : marquer vos classes open ou alors migrer vers la version 2 de Mockito. La première solution n'est pas idéale, mais elle est simple à mettre en œuvre. Elle oblige en effet à modifier votre code Kotlin et cela uniquement pour les tests, sans aucune valeur directe pour la production. La version 2 de Mockito est capable de mocker des classes, final mais uniquement via une option qui n'est pas activée par défaut.

II - Features

II-A - NullPointer Safety

Kotlin propose une déclinaison de tous ses types de base : les types nullables. De base, aucun des types présents dans Kotlin n'accepte la valeur null. Le compilateur sera en erreur par exemple avec le code suivant :

val name: String = null

Le langage essaye de minimiser l'utilisation de valeur null ou sinon, de l'expliciter. Ainsi, si null est une valeur possible, vous devez utiliser un type nullable, qui est représenté par le type suivi d'un point d'interrogation :

val name: String? = null

Ainsi, à partir du type, vous saurez si la valeur null est acceptée ou non et ainsi éviterez l'erreur à un milliard de dollars : la NullPointerException. On pourrait rapprocher un type nullable au type Optional de Java 8. Pourtant, il n'en est rien : un type nullable n'offre pas les méthodes de transformation d'Optional, mais plutôt une syntaxe qui lui est propre. À l'utilisation, cette syntaxe peut prêter à débat : en effet, pour accéder par exemple au champ d'un objet, le champ peut être accessible en utilisant le symbole '?', transformant ainsi le type de ce champ en nullable. Mais, à moins d'utiliser l'opérateur !!, le type nullable se propage dans un arbre d'objets où la base de cet arbre est nullable : tout le monde n'appréciera pas…

Toujours est-il que grâce à ce type nullable, vous pourrez exprimer vos intentions : votre API accepte-t-elle une valeur null ou non ?

II-B - Alias

Dans une application backend, les différentes couches techniques possèdent leurs propres modèles. L'API expose un modèle qui est transformé pour les couches inférieures et ainsi de suite. Un objet métier a ainsi souvent le même nom d'une couche à l'autre, mais depuis des packages différents. La manipulation de ces classes au sein d'un même fichier génère un clash de nommage, les classes ayant le même nom. Même si techniquement cela ne pose pas de problème, la lecture se révèle difficile, car la différenciation se fait alors via le nom des packages des classes.

MyClass apiMyClass = ...
fr.soat.example.persistance.MyClass myClass = new fr.soat.example.persistance.MyClass(apiMyClass.getName());

Kotlin propose la notion d'alias à l'import. Vous pouvez importer une classe et utiliser un nom différent pour cet import.

1. import fr.soat.example.persistance.MyClass as ModelMyClass

2. // ...

3. MyClass apiMyClass = ...

4. val myClass: ModelMyClass = ModelMyClass(apiMyClass.getName());

Grâce à cette notion d'alias, la conversion d'un domaine à l'autre reste nécessaire, mais la lecture, elle, se retrouve fortement simplifiée : le bruit généré par l'ajout du package n'a alors plus lieu d'être.

II-C - TypeAlias

Sur une API, les types primitifs sont souvent utilisés à tort, pouvant nuire à la compréhension de cette même API.

1. void action(String id, String session, String action) {

2. // ...

3. }

Seul le nom permet de différencier les paramètres de cette API. Pour casser cette ambiguïté, de nouveaux types peuvent être créés : PlayerId, SessionId, ActionName… Les paramètres utilisent alors des types spécifiques explicitant clairement la nature de chaque paramètre.

1. void action(PlayerId id, SessionId session, ActionName action) {

2. // ...

3. }

Si cette gestion de nouveaux types rebute certaines personnes, Kotlin propose la notion d'alias de type qui peut servir de solution intermédiaire.

1. typealias PlayerId = String

Cela ajoute uniquement un alias sur le type String. Vous pouvez alors utiliser une String directement comme PlayerId. Nous retrouvons alors la nature des types dans la signature des méthodes, sans la mise en œuvre de nouveaux types spécifiques.

1. typealias PlayerId = String

2. typealias SessionId = String

3. typealias ActionName = String

5. fun action(id: PlayerId, session: SessionId, name: ActionName) {

6. // ...

7. }

9. val id: PlayerId = "playerId"

10. val session: SessionId = "sessionId"

11. val action: ActionName = "action"

12. action(id, session, action)

Mais attention. Aucune vérification de type n'est faite par le compilateur. Vous pouvez utiliser n'importe quelle String pour chacun de ces 'types', qui sont eux-mêmes des String. Cela peut aboutir à la situation inconfortable ci-dessous (mais techniquement valide). Utilisez donc les typealias avec parcimonie.

1. val id: PlayerId = "playerId"

2. val session: SessionId = "sessionId"

3. val action: ActionName = "action"

4. action(action, id, id)

III - Architecture

DDD définit la notion de Value Object. Ces objets gardent des propriétés, et des objets ayant les mêmes propriétés devraient être égaux entre eux. Prenons l'exemple d'un objet Point : deux points ayant les mêmes coordonnées devraient être égaux. En Java, cela est représenté par une classe où les méthodes equals et hashcode sont redéfinies. De plus, ces Value Object doivent être immuables, ce qui est exprimé en Java grâce à des champs final :

1. public class Point {

2. private final int x;

3. private final int y;

6. public Point(int x, int y) {

7. this.x = x;

8. this.y = y;

9. }

11. public int getX() {

12. return x;

13. }

15. public int getY() {

16. return y;

17. }

19. @Override

20. public boolean equals(Object o) {

21. if (this == o) return true;

22. if (o == null || getClass() != o.getClass()) return false;

24. Point point = (Point) o;

26. if (x != point.x) return false;

27. return y == point.y;

28. }

30. @Override

31. public int hashCode() {

32. int result = x;

33. result = 31 * result + y;

34. return result;

35. }

36. }

On se retrouve vite avec du code essentiellement technique n'apportant pas de valeur fonctionnelle. Kotlin propose, pour représenter ces Value Object, la notion de data class : ces class définissent automatiquement les getter et les méthodes hashcode et equals à partir des champs qui constituent cette classe. Une data class définit également une méthode copy qui permet de créer une copie exacte d'un objet, copie que l'on peut directement modifier via cette méthode. Enfin, pour rendre l'objet immuable, les champs doivent seulement être annotés avec le mot clé val.

1. data class Point(val x: Int, val y: Int)

2. val point = Point(2, 3)

3. val point2 = point.copy(y = 5)

IV - Écosystème

Kotlin reste un langage relativement nouveau. Pourtant quelques projets existent déjà autour de ce langage. Une adaptation voire une migration vers ce langage est également observable sur différents projets venant de l'écosystème Java. Changement que l'on peut constater notamment à travers la nouvelle version de Spring Framework et les modifications actuellement réalisées sur l'outil de build Gradle.

IV-A - Spring 5

C'est au début de l'année 2017 que Pivotal communique sur le support Kotlin par le Framework Spring Framework 5. Spring est un Framework très utilisé sur les applicatifs backend. Le support de Kotlin par ce dernier n'est pas uniquement une garantie que le Framework fonctionne avec Kotlin : cela passe également par l'ajout d'API simplifiant l'utilisation de Spring, avec Kotlin.

Pour récupérer un bean d'un type spécifique dans un contexte Spring, il est systématiquement nécessaire en Java, d'indiquer le type du bean que l'on veut récupérer.

1. @Service

2. class MyService {

4. @Autowired

5. ApplicationContext ctx;

7. // ...

8. public void doSomething() {

9. MyCustomBean bean = ctx.getBean(MyCustomBean.class);

10. }

11. }

Ce code peut être légèrement simplifié grâce aux méthodes d'extensions ajoutées spécifiquement pour Kotlin, avec l'utilisation de type réifié. Le compilateur peut déduire, à partir du code, le type du bean qui vous intéresse, vous évitant ainsi de devoir le spécifier par vous-même.

1. @Service

2. class MyService {

4. @Autowired

5. lateinit var ctx: ApplicationContext

7. // ...

8. public void doSomething() {

9. val bean: MyCustomBean = ctx.getBean()

10. }

11. }

Ce genre de mécanisme se retrouve également sur d'autres composants du Framework, comme sur l'API RestTemplate ou WebClient.

L'autre point intéressant concerne la majorité des mises à jour d'API qui n'interfère pas avec l'API Java : aucun risque de se retrouver avec une API spécifique à Kotlin dans votre code Java. En effet, ces mises à jour d'API sont majoritairement proposées via des méthodes d'extensions. Elles ne sont donc pas directement sur les classes du Framework et ne sont donc pas proposées au développeur Java. Pour pouvoir les exploiter, il vous faudra depuis votre code Kotlin, importer ces méthodes d'extensions, opération assimilable à un import standard en Java.

IV-B - Gradle

Gradle se base actuellement sur Groovy pour son DSL (Domain Specific Language). Groovy, par son approche dynamique, permet d'avoir une approche très souple dans l'écriture des scripts, ces Scripts étant relativement simples à écrire grâce à tout ce qu'apporte le langage par défaut. Pourtant sous cette simplicité, il est facile de se perdre dans ce DSL. Certains scripts Gradle sont difficilement compréhensibles. Gradle Inc, la société derrière Gradle, développe actuellement un nouveau plugin pour Gradle. Ce plugin permet de construire des scripts de build non plus en Groovy, mais cette fois en Kotlin. L'objectif étant de le proposer comme alternative à Groovy, voire à terme, de faire en sorte que Kotlin soit le langage principal utilisé pour construire des builds Gradle.

V - Adoption massive ?

Kotlin est facilement intégrable grâce à sa compatibilité directe avec Java. La configuration avec les outils actuels est simple. De plus, la valeur ajoutée par Kotlin dans un projet se fait immédiatement ressentir. Malheureusement, à elles seules, ces fonctionnalités ne sont pas suffisantes pour transformer un langage en langage massivement adopté.

Pourtant Kotlin a actuellement le vent en poupe, ce qui pourrait l'aider à se développer. C'est sans compter sur le coup de pouce de Google qui en a fait son nouveau langage de référence sur la plateforme Android pour différents outils et Frameworks. En parallèle, la dernière version de Spring Framework a adapté ses API pour Kotlin. De son côté, Gradle propose une variation de son DSL Groovy en Kotlin. Ainsi de nombreuses sociétés valorisent leurs nouveaux Frameworks, basés sur Kotlin.

Si cette tendance à construire un écosystème autour du langage Kotlin continue, alors oui, Kotlin pourrait devenir le premier langage alternatif sur la JVM, avant de peut-être devenir LE langage qui détrônera Java. Mais ça, seul le futur nous le dira.