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.
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…
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.
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 !
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...
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...
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.
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"),
Sarah : Les tests sont au vert !
Sarah : Comme promis, le projet est maintenant 100% en Kotlin
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 :
Moi :Je préfère comme ça. Est-ce que les tests sont toujours au vert?
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 :
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().
fungetPINs(observedPin:String):Set<String>{require(observedPin.all{itinmapPins}){"PIN $observedPin is invalid"}returnobservedPin.fold(initial=setOf("")){acc:Set<String>,c:Char->valpinsForChar:Set<String>=mapPins[c]!!combineSolutions(acc,pinsForChar)}}funcombineSolutions(pins1:Set<String>,pins2:Set<String>):Set<String>=pins1.flatMap{pin1->pins2.map{pin2->"$pin1$pin2"}}.toSet()
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/