L’un des plus grands points forts de Kotlin peut aussi être sa faiblesse. Lorsque nous parlons de classes de données aujourd'hui, et de la même manière que les enregistrements Java, nous avons tendance à nous concentrer, du moins pour beaucoup de personnes que je connais, sur l'élimination complète du code passe-partout. Le code passe-partout est quelque chose qui agace les développeurs depuis des lustres. Il existe non seulement en Java, mais d’autres langages l’ont également. Si nous envisageons de créer des objets d'accès aux données d'une manière ou d'une autre, cela n'a pas vraiment d'importance si nous le faisons en Kotlin, en Java ou dans tout autre langage.
Nous répétons fréquemment le même processus : création du référentiel, service et contrôleur. De toute façon, une grande partie de ce que nous faisons est encore passe-partout. Même si le code passe-partout a pu faire partie de la décision de créer des classes de données ou des enregistrements Java, un paradigme plus important du génie logiciel était le véritable point central de la création de ces types de structures de données.
Tout se résume à l'immuabilité ou en tout cas à sa possibilité. Remontons le temps et revenons à 1995 où nous avons commencé à un moment donné à créer des structures de données dans lesquelles nous spécifions différentes choses. Nous définirions les champs, les getters, les setters, les propriétés, les accesseurs, les attributs et les paramètres de manière à pouvoir transmettre nos arguments soit aux méthodes, aux fonctions ou aux constructeurs. Pour cette partie, nous aurons une programmation amusante utilisant les personnages de la série du milieu des années 80 : The Golden Girls. Cela dit, voyons à quoi ressemblerait une classe pendant environ 15 ans :
public class GoldenGirlsJava { public String goldenGirl1; private final String goldenGirl2; private final String goldenGirl3; private final String goldenGirl4; public GoldenGirlsJava() { this.goldenGirl1 = "Dorothy Zbornak"; this.goldenGirl2 = "Rose Nylund"; this.goldenGirl3 = "Blanche Devereaux"; this.goldenGirl4 = "Sophia Petrillo"; } public GoldenGirlsJava( String goldenGirl1, String goldenGirl2, String goldenGirl3, String goldenGirl4 ) { this.goldenGirl1 = goldenGirl1; this.goldenGirl2 = goldenGirl2; this.goldenGirl3 = goldenGirl3; this.goldenGirl4 = goldenGirl4; } public String getGoldenGirl1() { return goldenGirl1; } public void setGoldenGirl1(String goldenGirl1) { this.goldenGirl1 = goldenGirl1; } public String getGoldenGirl2() { return goldenGirl2; } public String getGoldenGirl3() { return goldenGirl3; } public String getGoldenGirl4() { return goldenGirl4; } @Override public String toString() { return "GoldenGirlsJava{" + "goldenGirl1='" + goldenGirl1 + '\'' + ", goldenGirl2='" + goldenGirl2 + '\'' + ", goldenGirl3='" + goldenGirl3 + '\'' + ", goldenGirl4='" + goldenGirl4 + '\'' + '}'; } }
C'était une énorme quantité de code à saisir uniquement pour créer deux constructeurs. Bien que le hashcode et ses égaux ne soient pas nécessairement indispensables dans toutes les occasions, nous avons presque toujours dû implémenter un constructeur sans arguments à côté d'un constructeur plus généraliste. L’avantage de créer tout ce code passe-partout est que nous savions très clairement à quoi tout ressemblerait après la compilation en bytecode.
Cependant, et dans tous les cas, le code passe-partout a toujours été un sujet controversé pour de nombreux développeurs et c'est ainsi qu'en 2009, Lombok est arrivé et a révolutionné la façon dont nous créons des structures de données. Il a introduit le concept de l'utilisation d'un processeur d'annotations et de l'interprétation d'annotations spécifiques qui donneraient les qualités dont nous avions besoin pour nos classes. Une classe annotée Lombok ressemblerait donc à ceci :
@Getter @Setter @AllArgsConstructor @ToString public class GoldenGirlsLombok { public String goldenGirl1; private final String goldenGirl2; private final String goldenGirl3; private final String goldenGirl4; public GoldenGirlsLombok() { this.goldenGirl1 = "Dorothy Zbornak"; this.goldenGirl2 = "Rose Nylund"; this.goldenGirl3 = "Blanche Devereaux"; this.goldenGirl4 = "Sophia Petrillo"; } }
Et pendant un moment, les gens étaient très contents de Lombok ! Finalement, le poids de devoir créer tout ce code passe-partout a disparu. Mais cette impression a duré pendant environ 7 ans, lorsqu'un nouvel acteur est arrivé et cette fois, c'était quelque chose à quoi l'industrie du développement logiciel ne s'attendait pas. Il s’appelait Kotlin et a fait ses débuts en 2016 avec l’introduction immédiate des classes de données. Notre implémentation de Golden Girls dans Kotlin ressemblerait désormais à ceci :
data class GoldenGirls( var goldenGirl1: String = "Dorothy Zbornak", private val goldenGirl2: String = "Rose Nylund", private val goldenGirl3: String = "Blanche Devereaux", private val goldenGirl4: String = "Sophia Petrillo" )
Bien que Kotlin ait lentement commencé à rassembler des fans, Java, de son côté, s'est rendu compte qu'il y avait autre chose sur le marché et certains développements de ce côté ont commencé à prendre de l'ampleur, comme le projet Loom qui était en préparation mais qui a également été laissé en veilleuse pendant un certain temps. C'est pourquoi, avec la sortie de Java 14 en 20202, Java a introduit les enregistrements Java, et les structures de données en Java ressembleraient désormais à ceci :
public record GoldenGirlsRecord( String goldenGirl1, String goldenGirl2, String goldenGirl3, String goldenGirl4 ) { public GoldenGirlsRecord() { this( "Dorothy Zbornak", "Rose Nylund", "Blanche Devereaux", "Sophia Petrillo" ); } }
Et à ce jour, la simplification et la réduction du code semblent se poursuivre. Cependant, avec la réduction du code passe-partout, les concepts de champs, getters, setters, propriétés, accesseurs, attributs et paramètres sont devenus beaucoup moins visuels et moins faciles à cartographier dans notre esprit. Que cela nous plaise ou non, ces concepts restent la façon dont la JVM fonctionne et organise le code avec le bytecode. Mais la simplification excessive du code consiste en réalité à rendre le code plus facile à lire et dans le cas des classes de données et des enregistrements Java, l'idée est également de créer des structures de données. qui sont immuables ou partiellement immuables.
Les enregistrements Java sont véritablement immuables dans le sens où toutes les valeurs qu'ils contiennent ou les références qu'ils contiennent ne peuvent pas être modifiées. Les classes de données Kotlin peuvent également être véritablement immuables pour les mêmes raisons, mais elles ne sont pas obligées de le faire, ce qui donne en quelque sorte la permission aux développeurs de créer du code complexe et, pire encore, mutable. Dans tous les cas, tout va bien jusqu'à ce que nous en ayons besoin. travailler avec des frameworks comme Spring Framework, qui s'appuient fortement sur les annotations. Voici un exemple:
@Entity @Table(name = "shelf_case") data class Case( @Id @GeneratedValue(strategy = GenerationType.SEQUENCE) val id: Long?, var designation: String?, var weight: Long? ) { constructor() : this(0L, null, null) override fun toString(): String { return super.toString() } }
Cet exemple fonctionne bien. Pour un œil non averti, il ne se passe pas grand-chose de spécial ici. Cela fonctionne en Kotlin, de la même manière qu’en Java. Mais regardons maintenant un exemple similaire, mais cette fois avec les annotations de Jackson :
data class AccountNumbersPassiveDto( @NotNull val accountNumberLong: Long?, val accountNumberNullable: Long?, @DecimalMax(value = "10") @DecimalMin(value = "5") val accountNumber: BigDecimal, val accountNumberEven: Int, val accountNumberOdd: Int, @Positive val accountNumberPositive: Int, @Negative val accountNumberNegative: Int, @DecimalMax(value = "5", groups = [LowProfile::class]) @DecimalMax(value = "10", groups = [MiddleProfile::class]) @DecimalMax(value = "15", groups = [HighProfile::class]) @DecimalMax(value = "20") val accountNumberMaxList:Int )
Cet exemple échouera et la raison en est que dans Kotlin, il n'y a aucun moyen de dire au compilateur où exactement nous voulons que notre annotation soit appliquée. Dans l'exemple précédent avec l'annotation @Entity
, qui est une annotation de persistance Jakarta, les annotations sont appliquées correctement aux champs. Si nous décompilons ce code, nous trouverons ceci :
public final class Case { @Id @GeneratedValue( strategy = GenerationType.SEQUENCE ) @Nullable private final Long id; @Nullable private String designation; @Nullable private Long weight;
Mais pour ce dernier, qui est un exemple de validation de Jakarta, on retrouve ceci :
public final class AccountNumbersPassiveDto { @Nullable private final Long accountNumberLong; @Nullable private final Long accountNumberNullable; @NotNull private final BigDecimal accountNumber; private final int accountNumberEven; private final int accountNumberOdd;
Cela signifie que AccountNumbersDto
n'a pas été affecté par ces annotations sur ses champs. Nous avons juste eu de la chance dans le premier exemple. Cela aurait peut-être également échoué. Afin de spécifier où notre annotation doit aller, Kotlin nous donne la possibilité d'utiliser des cibles de site d'utilisation comme préfixe à toute annotation. La condition est bien entendu que les conditions soient remplies. Dans ce cas, une façon de résoudre ce problème consiste à préfixer toutes les annotations avec @field
comme ceci :
data class AccountNumbersPassiveDto( @field:NotNull val accountNumberLong: Long?, val accountNumberNullable: Long?, @field:DecimalMax(value = "10") @field:DecimalMin(value = "5") val accountNumber: BigDecimal, val accountNumberEven: Int, val accountNumberOdd: Int, @field:Positive val accountNumberPositive: Int, @field:Negative val accountNumberNegative: Int, @field:DecimalMax(value = "5", groups = [LowProfile::class]) @field:DecimalMax(value = "10", groups = [MiddleProfile::class]) @field:DecimalMax(value = "15", groups = [HighProfile::class]) @field:DecimalMax(value = "20") val accountNumberMaxList:Int )
Si nous essayons maintenant de décompiler le bytecode résultant à l'aide d'IntelliJ, nous constaterons que cela donne ce code en Java :
public final class AccountNumbersPassiveDto { @NotNull @Nullable private final Long accountNumberLong; @Nullable private final Long accountNumberNullable; @DecimalMax("10") @DecimalMin("5") @org.jetbrains.annotations.NotNull
Cela signifie que les annotations ont été appliquées au champ et que notre code fonctionne comme il est censé le faire.
Vous pouvez probablement imaginer à ce stade que les personnes qui travaillent sur Java depuis longtemps n'auront aucun problème à s'adapter à Kotlin car nous savons tous ce que sont les champs, les paramètres, les propriétés, etc. Ce que nous observons aujourd’hui, et j’en suis également témoin, c’est qu’il semble que la simplification du code ait le potentiel de se retourner contre nous. Il y a eu de nombreuses occasions où j'ai pris conscience du temps passé dans des projets à essayer de comprendre pourquoi, dans certains projets ou modules, les annotations ne semblent tout simplement pas fonctionner. Et tout cela semble se résumer à ne pas utiliser les cibles du site d’utilisation.
Ma théorie sur ce qui est le plus probable est que les nouvelles générations de développeurs considéreront probablement des choses que ma génération ne pensait pas compliquées et apprises par instinct, comme du matériel vraiment compliqué. C’est le cas des cibles de sites d’utilisation. C’étaient des choses que nous pouvions très facilement visualiser. Cependant, à l’heure actuelle, la confusion générée par ce phénomène semble parfois retarder le développement des projets à temps. Je ne pourrais bien sûr pas généraliser, mais il y a un potentiel positif et négatif avec les enregistrements Java et les classes de données. Il est difficile de dire comment les classes de données et les enregistrements façonneront notre avenir, mais une chose est claire : en raison de ce problème, d'autres frameworks parient sur une absence totale d'annotations, comme c'est le cas avec Ktor.
J'ai créé une documentation à ce sujet sur Scribed : Fields-in-Java-and-Kotlin-and-What-to-Expect et également sur slide-share : field-in-java-and-kotlin-and-what-to-expect .
Vous pouvez également trouver les exemples que j'ai donnés sur GitHub. L'exemple des Golden Girls peut être trouvé ici : jeorg-kotlin-test-drives et les exemples d'annotations de persistance et de validation de Jakarta peuvent être trouvés ici : https://github.com/jesperancinha/jeorg-spring-master-test-drives .
Enfin, j'ai également créé une vidéo à ce sujet sur YouTube que vous pouvez consulter juste ici :
Également publiéici .