De Java à Kotlin en 20 minutes

jmfayard

Jean-Michel 🕵🏻‍♂️ Fayard

Posted on May 27, 2021

De Java à Kotlin en 20 minutes

Cet article est dédié aux développeurs Java qui ont entendu parler de Kotlin, mais ne s'y sont encore jamais frottés.

À quoi ressemblerait leur découverte de Kotlin ?

Une séance de mob-programming avec des collègues m'a permis d'en avoir une petite idée....

Show Me the Code !

Vous pouvez lire l'article, mais vous en tirerez davantage encore en suivant dans votre IDE l'évolution du code.

Pour cela, vous avez besoin de IntelliJ Community Edition. Qui est gratuit !

Sur MacOS par exemple, vous pouvez l'installer avec $ brew install intellij-idea-ce

Le repository se trouve ci-dessous, et les commits décrits sont présents dans cette pull-request.

Kata: the observed PIN

https://www.codewars.com/kata/5263c6999e0f40dee200059d/train/java

Alright, detective, one of our colleagues successfully observed our target person, Robby the robber. We followed him to a secret warehouse, where we assume to find all the stolen stuff. The door to this warehouse is secured by an electronic combination lock. Unfortunately our spy isn't sure about the PIN he saw, when Robby entered it.

The keypad has the following layout:

┌───┬───┬───┐
│ 1 │ 2 │ 3 │
├───┼───┼───┤
│ 4 │ 5 │ 6 │
├───┼───┼───┤
│ 7 │ 8 │ 9 │
└───┼───┼───┘
    │ 0 │
    └───┘

He noted the PIN 1357, but he also said, it is possible that each of the digits he saw could actually be another adjacent digit (horizontally or vertically, but not diagonally). E.g. instead of the 1 it could also be the 2 or 4. And instead of the 5 it could also be the…

Mais tout d'abord un peu de contexte.

Mob-programming

Mes collègues Sarah et Pierre sommes en train de faire avec moi une session Mob programming.

Le but est de résoudre le kata du PIN observé approximativement, dans lequel un espion moyennement fiable nous indique avoir vu le PIN 1357. Mais il n'est pas très sûr de lui. Chaque chiffre pourrait être à la place un de ses voisins sur le pavé numérique. Le code pourrait donc être 1357 mais aussi par exemple 2357 ou 1368.

Le projet est un projet Java/Maven. Il contient deux fichiers : PinGuesser.java et PinGuesserTest.java. Le projet a un temps de compilation et d'exécution des tests qui se comptent en secondes, pas en minutes comme dans beaucoup d'applications Android. Plus sympa en tant que développeur à mon avis.

Nous utilisons le plugin Code With Me d'IntelliJ pour pouvoir faire du mob-programming.

Nous nous débrouillons bien : nous avons réussi à résoudre le Kata puis à le refactorer à un état satisfaisant.

Mais il nous reste 20 minutes !

  • Sarah : Vous voyez quelque chose à améliorer ?
  • Pierre : Pas trop, ça m'a lair bien comme ça.
  • Moi : Il nous reste 20 minutes, pourquoi pas tout réécrire en Kotlin ?
  • Sarah : Oh, j'ai entendu parler de Kotlin, mais n'ai jamais eu l'occasion de l'essayer. Tout de même, 20 minutes, est-ce que c'est faisable ?
  • Moi : Lançons-nous, on verra bien ce que ça donne !

Configure Kotlin in project

  • Pierre : Ok, moi je n'ai jamais fait de Kotlin de ma vie. Dis-moi quoi faire.
  • Moi : IntelliJ a une ActionConvert Java File to Kotlin File. C'est une bonne manière de démarrer !
  • Pierre : Ok, j'essaye !

https://user-images.githubusercontent.com/459464/118158571-42ef6000-b41c-11eb-89df-c32de3ffe8f0.png

https://user-images.githubusercontent.com/459464/118158602-4d115e80-b41c-11eb-8cb6-ee85143251ae.png

  • Pierre : IntelliJ me dit que Kotlin n'est pas configuré dans le projet. Il n'a pas tort !
  • Pierre : Comment je configure Kotlin dans un projet Maven ?
  • Moi : Je ne sais pas, j'ai toujours utilisé Gradle.
  • Moi : Laisse IntelliJ le faire !
  • Moi : Au passage, c'est équivalent à exécuter l'action Tools > Kotlin > Configure Kotlin in project
  • Pierre : Allons-y.
  • Pierre : Ça a l'air d'avoir marché, le fichier pom.xml a été modifié.
  • Pierre : first commit

Dire à Java que @ParametersAreNonnullByDefault

  • Moi : Avant d'essayer le convertisseur Java -> Kotlin, il y a une étape préalable. Comme vous en avez peut-être entendu parler, Kotlin intègre la gestion de la nullabilité dans son système de type. Mais pas Java qui par défaut autorise null partout. Du coup le convertisseur autoriserait null partout lui aussi. Ce qui est techniquement correct, mais pas ce que nous voulons.
  • Sarah : Mais il y a des annotations en Java pour dire si la valeur null est autorisée ou non, pas vrai ?
  • Moi : Exactement, et l'annotation qui nous intéresse est @ParametersAreNonnullByDefault de la JSR 305. Elle s'applique à tout un package et informe que par défaut les paramètres sont non-nulls. Ça tombe bien, c'est exactement comme cela que ça marche en Kotlin !
  • Moi : commit
diff --git a/pom.xml b/pom.xml
     <dependencies>
+        <dependency>
+            <groupId>com.google.code.findbugs</groupId>
+            <artifactId>jsr305</artifactId>
+            <version>3.0.2</version>
+        </dependency>

+++ b/src/main/java/pin/package-info.java
@@ -0,0 +1,4 @@
+@ParametersAreNonnullByDefault
+package pin;
+
+import javax.annotation.ParametersAreNonnullByDefault;
Enter fullscreen mode Exit fullscreen mode

PinGuesser : Convert Java File to Kotlin File

  • Pierre : J'imagine que maintenant je peux ouvrir PinGuesser.java et relancer l'action Convert Java File to Kotlin File ?
  • Moi : Correct
  • Pierre : Apparemment.... ça a marché ? En tout cas il y a un fichier PinGuesser.kt.
  • Moi : Comment peux-tu t'assurer que ça a vraiment marché ?
  • Sarah : Tu devrais lancer les tests unitaires.
  • Pierre : Ah oui...

https://user-images.githubusercontent.com/459464/117936889-aaff5280-b305-11eb-9c84-be7205e9673c.png

  • Pierre : Tout est au vert ! C'est dingue, j'ai écrit mon premier code en Kotlin, et il est bug-free du premier coup.
  • Sarah : Bravo !
  • Pierre : Il reste les tests. On doit les convertir aussi, non ?
  • Moi : Pas forcément, ça marche aussi comme ça. Java et Kotlin peuvent co-exister pacifiquement dans le même repository grâce à leur interopérabilité.
  • Sarah : Ok, mais ça a l'air fun et je veux moi aussi essayer !
  • Pierre : Je te donne le clavier, juste après ce commit.

PinGuesserTest : Convert Java File to Kotlin File - et corrections manuelles

  • Sarah : Donc j'ouvre PinGuesserTest.java et j'exécute l'action... Comment s'appelle t'elle ?
  • Pierre : Convert Java File to Kotlin File
  • Sarah : C'est parti !
  • Sarah : J'ai maintenant un fichier PinGuesserTest.kt . Il contient des erreurs ceci dit...

https://user-images.githubusercontent.com/459464/117937632-80fa6000-b306-11eb-931b-7642e4aac07a.png

  • Pierre : À ta place j'appliquerais la suggestion d'optimiser les imports.
  • Sarah : Ok.
  • Sarah : Ça a marché.
  • Moi : Comme vous voyez le convertisseur n'est pas parfait. Mais je trouve que c'est un formidable outil d'apprentissage car il vous permet de prendre quelque-chose que vous connaissez déjà - Java - et de le convertir en ce que vous voulez apprendre.
  • Sarah : Je lance les tests unitaires par acquis de conscience.
  • Sarah : Ouh j'ai des erreurs bizarres dans jUnit.

https://user-images.githubusercontent.com/459464/118160523-b3977c00-b41e-11eb-8151-a4dea10aa9e2.png

  • Moi : Je crois que je comprends le message d'erreur : là où Java a des méthodes static, Kotlin utilise des méthodes définies dans le companion object { ... } de la classe. En général c'est presque la même chose, mais là jUnit veut vraiment avoir à faire à des méthodes statiques, ce qui peut se corriger avec une annotation :

-        fun testSingleDigitParameters(): Stream<ArguMents> {
+        @JvmStatic fun testSingleDigitParameters(): Stream<Arguments> {
             return Stream.of(
                 Arguments.of("1", java.util.Set.of("1", "2", "4")),
                 Arguments.of("2", java.util.Set.of("1", "2", "3", "5")),
@@ -61,7 +58,7 @@ internal class PinGuesserTest {
             )
         }

-        fun invalidParams(): Stream<Arguments> {
+        @JvmStatic  fun invalidParams(): Stream<Arguments> {
             return Stream.of(
                 Arguments.of("   "),
                 Arguments.of("A"),

Enter fullscreen mode Exit fullscreen mode
  • Sarah : Les tests sont au vert !
  • Sarah : Comme promis, le projet est maintenant 100% en Kotlin
  • Sarah : commit

Utiliser la librairie Kotlin standard

  • Pierre : C'est quoi la prochaine étape ?
  • Moi : Il est possible de créer List, Set et Map comme on le fait traditionnellement en Java. Mais la librairie Kotlin standard contient plein de fonctions utilitaires qui résolvent élégamment des petits problèmes courants. Je vous montre ça :

https://user-images.githubusercontent.com/459464/118299183-b6f33c00-b4e0-11eb-9458-c8322d65cae9.png

  • Moi :Je préfère comme ça. Est-ce que les tests sont toujours au vert?
  • Moi : Oui ils le sont, donc commit.

Remplacer l'API Streams par la librairie Kotlin standard

  • Moi : Une autre chose que contient la librairie Kotlin standard sont les fonctions .map(), .filter(), .flatmap() - et bien d'autres encore - qu'on retrouve dans les langages fonctionnels.
  • Sarah : Un peut comme l'API stream() que nous avons utilisé en Java ?
  • Moi : C'est ça, mais plus performant dans son implémentation et moins verbeux :
-    fun combineSolutions(pins1: Set<String>, pins2: Set<String>): Set<String> {
-        return pins1.stream()
-            .flatMap { pin1: String ->
-                pins2
-                    .stream()
-                    .map { pin2: String -> pin1 + pin2 }
-            .collect(Collectors.toSet())
-    }

+    fun combineSolutions(pins1: Set<String>, pins2: Set<String>): Set<String> =
+        pins1.flatMap { pin1 ->
+            pins2.map { pin2 ->
+                "$pin1$pin2"
+             }
+        }.toSet()
Enter fullscreen mode Exit fullscreen mode
  • Sarah : Les tests passent toujours.
  • Sarah : commit.

Make val, not var

  • Moi : Dans le code Kotlin idiomatique, on essaye d'utiliser le plus possible les val property au lieu de var property.
  • Pierre : Quelle est la différence ?
  • Moi : val property est read-only, il n'a pas de setter, c'est comme un final field en Java
  • Pierre : Je vois. Donc je remplace juste les var pas des val ?
  • Moi : Oui c'est ça.
  • Pierre : Facile.
  • Pierre : commit.

Fail fast

  • Sarah : Est-ce qu'il y a un moyen idiomatique en Kotlin de vérifier les paramètres ?
  • Sarah : On s'attend à ce que le PIN soit quelque-chose comme 7294, c'est à dire qu'il ne contiennent que des chiffres.
  • Moi : Oui, tu peux utiliser require(condition) { "Message d'erreur" } .
  • Sarah : Cela donnerait quoi ici ?
fun getPINs(observedPin: String): Set<String> {
    require(observedPin.all { it in '0'..'9' }) { "PIN $observedPin is invalid" }
    // rest goes here
}
Enter fullscreen mode Exit fullscreen mode
  • Sarah : Merci !
  • Sarah : commit.

Passer à un style plus fonctionnel

  • Sarah : Quelle est la prochaine étape ?
  • Moi : Je voudrais libérer les fonctions.
  • Pierre : C'est à dire ?
  • Moi : Regardez, nous avons cette classe PinGuesser . Mais qu'est-ce qu'elle fait exactement ? Elle ne fait rien d'autre qu'être un bête namespace.
  • Moi : Cette classe un nom qui nous empêche d'accéder directement aux verbes - les fonctions - qui font tout le travail.
  • Moi : C'est ce que montre un de mes essais favoris sur la programmation : "Execution in the kingdom of nouns" de Steve Yegge.
  • Sarah : Je connais cette diatribe, elle est hilarante !
  • Sarah : Alors comment libère-t'on les verbes / fonctions ?
  • Moi : Nous supprimons la classe et transformons ses méthodes en fonctions top-level :
diff --git a/src/main/java/pin/PinGuesser.kt b/src/main/java/pin/PinGuesser.kt
index 17a20b3..38e457c 100644
--- a/src/main/java/pin/PinGuesser.kt
+++ b/src/main/java/pin/PinGuesser.kt
@@ -1,9 +1,5 @@
 package pin

-import java.util.stream.Collectors
-
-class PinGuesser {
-    companion object {
         val mapPins = mapOf(
             "1" to setOf("1", "2", "4"),
             "2" to setOf("1", "2", "3", "5"),
@@ -16,7 +12,6 @@ class PinGuesser {
             "9" to setOf("6", "8", "9"),
             "0" to setOf("0", "8"),
         )
-    }

     fun getPINs(observedPin: String): Set<String> {
         for (c in observedPin.toCharArray()) {
@@ -38,5 +33,4 @@ class PinGuesser {
             pins2.map { pin2 ->
                 "$pin1$pin2"
             }
-        }.toSet()
-}


--- a/src/test/java/PinGuesserTest.kt
+++ b/src/test/java/PinGuesserTest.kt
class PinGuesserTest {
-    val pinGuesser = PinGuesser()

     @ParaMoiterizedTest
     @MoithodSource("testSingleDigitParaMoiters")
     fun testSingleDigit(observedPin: String?, expected: Set<String?>?) {
-        val actual = pinGuesser.getPINs(observedPin!!)
+        val actual = getPINs(observedPin!!)
         Assertions.assertEquals(expected, actual)
     }
Enter fullscreen mode Exit fullscreen mode

List.fold()

  • Pierre : Est-ce qu'on peut prendre un peu de recul là ? À quoi cela sert-il de rendre le code plus "beau", plus idiomatique ? Au final, nos clients s'en foutent.
  • Moi : Et bien je ne sais pas vous, mais moi cela m'arrive fréquemment de ne pas vraiment comprendre le code sur lequel je suis censé travailler. Dans ce cas là, je m'investis pour simplifier le code, et à un moment donné le code tient dans ma tête et la solution devient évidente.
  • Pierre : Et qu'est-ce qui est devenu évident là par exemple ?
  • Moi : Et bien maintenant que nous avons un code Kotlin idiomatique clean, je me rends compte que la solution du Kata peut s'exprimer par une simple construction fonctionnelle : List.fold().
  • Sarah : Show me the code
  • Moi : commit.
fun getPINs(observedPin: String): Set<String> {
    require(observedPin.all { it in mapPins }) { "PIN $observedPin is invalid" }

    return  observedPin.fold(initial = setOf("")) { acc: Set<String>, c: Char ->
        val pinsForChar: Set<String> = mapPins[c]!!
        combineSolutions(acc, pinsForChar)
    }
}

fun combineSolutions(pins1: Set<String>, pins2: Set<String>): Set<String> =
    pins1.flatMap { pin1 ->
        pins2.map { pin2 ->
            "$pin1$pin2"
        }
    }.toSet()
Enter fullscreen mode Exit fullscreen mode

La suite ?

J'espère que vous avez aimé cet article.

Si vous voulez me contacter, vous trouverez mon mail à l'adresse https://jmfayard.dev/

Le code est disponible à l'adresse https://github.com/jmfayard/from-java-to-kotlin

Vous devez démarrer dans la branche java et comparer avec ce que contient la branche kotlin. Voir cette pull-request

Si vous voulez apprendre Kotlin, je vous réfère à mon article :

💖 💪 🙅 🚩
jmfayard

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related