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!
- Mob-programming
- Configure Kotlin in project
- Dire à Java que @ParametersAreNonnullByDefault
- PinGuesser: Convert Java File to Kotlin File
- PinGuesserTest: Convert Java File to Kotlin File - et corrections manuelles
- Utiliser la librairie Kotlin standard
- Remplacer l'API Streams par la librairie Kotlin standard
- Make val, not var
- Fail fast
- Passer à un style plus fonctionnel
- List.fold()
- La suite?
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 Action
Convert Java File to Kotlin File
. C'est une bonne manière de démarrer ! - Pierre : Ok, j'essaye !
- 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 autoriseraitnull
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;
PinGuesser : Convert Java File to Kotlin File
-
Pierre : J'imagine que maintenant je peux ouvrir
PinGuesser.java
et relancer l'actionConvert 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
- Sarah : commit
Utiliser la librairie Kotlin standard
- Pierre : C'est quoi la prochaine étape ?
-
Moi : Il est possible de créer
List
,Set
etMap
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?
- 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()
- 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 devar property
. - Pierre : Quelle est la différence ?
-
Moi :
val property
est read-only, il n'a pas de setter, c'est comme unfinal field
en Java -
Pierre : Je vois. Donc je remplace juste les
var
pas desval
? - 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
}
- 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)
}
- Moi : commit.
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()
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 :
Top comments (0)