Eine der größten Stärken von Kotlin kann auch seine Schwäche sein. Wenn wir heute über Datenklassen und ebenso über Java-Datensätze sprechen, konzentrieren wir uns – zumindest viele Leute, die ich kenne – auf die vollständige Eliminierung des Boilerplate-Codes. Boilerplate-Code ist seit Ewigkeiten etwas, das Entwickler nervt. Es existiert nicht nur in Java, sondern auch in anderen Sprachen. Wenn wir darüber nachdenken, Datenzugriffsobjekte auf irgendeine Weise zu erstellen, spielt es keine Rolle, ob wir dies in Kotlin, Java oder einer anderen Sprache tun.
Häufig wiederholen wir den gleichen Prozess: Repository-Erstellung, Service und Controller. Vieles von dem, was wir tun, ist sowieso immer noch Standard. Auch wenn Boilerplate-Code Teil der Entscheidung zur Erstellung von Datenklassen oder Java-Datensätzen gewesen sein mag, war ein wichtigeres Paradigma der Softwareentwicklung der eigentliche Schwerpunkt bei der Erstellung dieser Art von Datenstrukturen.
Alles läuft auf Unveränderlichkeit oder zumindest auf die Möglichkeit davon hinaus. Lassen Sie uns die Zeit zurückdrehen und zurück ins Jahr 1995 gehen, wo wir irgendwann damit begannen, Datenstrukturen zu erstellen, in denen wir verschiedene Dinge spezifizierten. Wir würden die Felder, die Getter, die Setter, die Eigenschaften, die Accessoren, die Attribute und die Parameter so definieren, dass wir unsere Argumente entweder an die Methoden, die Funktionen oder die Konstruktoren übergeben können. Diesen Teil werden wir haben einige unterhaltsame Programmierungen mit den Charakteren der Serie „The Golden Girls“ aus der Mitte der 80er Jahre. Lassen Sie uns nun sehen, wie eine Klasse für etwa 15 Jahre aussehen würde:
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 + '\'' + '}'; } }
Es war eine enorme Menge an Code einzugeben, nur um zwei Konstruktoren zu erstellen. Obwohl Hashcode und Equals nicht unbedingt in allen Fällen ein Muss waren, mussten wir fast immer einen Konstruktor ohne Argumente neben einem allgemeineren Konstruktor implementieren. Das Gute an der Erstellung dieses gesamten Standardcodes ist, dass wir sehr genau wussten, wie alles nach der Kompilierung in Bytecode aussehen würde.
Allerdings war Boilerplate-Code für viele Entwickler schon immer ein umstrittenes Thema, und so kam 2009 Lombok auf den Markt und revolutionierte die Art und Weise, wie wir Datenstrukturen erstellen. Es führte das Konzept der Verwendung eines Annotationsprozessors und der Interpretation spezifischer Annotationen ein, die die Qualitäten liefern würden, die wir für unsere Klassen benötigten, und so würde eine mit Lombok annotierte Klasse wie folgt aussehen:
@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"; } }
Und eine Zeit lang freuten sich die Menschen sehr über Lombok! Endlich entfällt der Aufwand, den gesamten Standardcode erstellen zu müssen. Aber dieser Niedergang dauerte etwa sieben Jahre, bis ein neuer Player hinzukam, und dieses Mal war es etwas, was die Softwareentwicklungsbranche nicht erwartet hatte. Es hieß Kotlin und debütierte 2016 mit der sofortigen Einführung von Datenklassen. Unsere Golden Girls-Implementierung in Kotlin würde nun so aussehen:
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" )
Obwohl Kotlin langsam begann, Fans zu gewinnen, erkannte Java andererseits, dass etwas anderes auf dem Markt war, und einige Entwicklungen auf dieser Seite begannen etwas an Fahrt zu gewinnen, wie zum Beispiel das Projekt Loom, das zwar in der Entwicklung war, aber auch eine Weile auf Eis gelegt wurde. Aus diesem Grund führte Java mit der Veröffentlichung von Java 14 im Jahr 20202 Java-Records ein und Datenstrukturen in Java würden nun wie folgt aussehen:
public record GoldenGirlsRecord( String goldenGirl1, String goldenGirl2, String goldenGirl3, String goldenGirl4 ) { public GoldenGirlsRecord() { this( "Dorothy Zbornak", "Rose Nylund", "Blanche Devereaux", "Sophia Petrillo" ); } }
Und bis zum heutigen Tag scheint die Code-Vereinfachung und Code-Reduzierung weiterzugehen. Mit der Reduzierung des Boilerplate-Codes wurden die Konzepte von Feldern, Gettern, Settern, Eigenschaften, Zugriffsmethoden, Attributen und Parametern jedoch weitaus weniger visuell und in unseren Köpfen weniger einfach abzubilden. Ob es uns gefällt oder nicht, diese Konzepte sind immer noch die Art und Weise, wie die JVM funktioniert und Code mit dem Bytecode organisiert. Aber bei der Code-Vereinfachung geht es in Wirklichkeit darum, den Code leichter lesbar zu machen, und im Fall von Datenklassen und Java-Datensätzen besteht die Idee auch darin, Datenstrukturen zu erstellen die unveränderlich oder teilweise unveränderlich sind.
Java-Datensätze sind in dem Sinne wirklich unveränderlich, dass alle darin enthaltenen Werte oder darin enthaltenen Referenzen nicht geändert werden können. Kotlin-Datenklassen können aus den gleichen Gründen auch wirklich unveränderlich sein, müssen es aber nicht, was den Entwicklern in gewisser Weise die Erlaubnis gibt, komplizierten und, was am schlimmsten ist, sowieso veränderlichen Code zu erstellen. Auf jeden Fall ist das alles gut, bis wir es dann brauchen um mit Frameworks wie dem Spring Framework zu arbeiten, die stark auf Annotationen angewiesen sind. Hier ist ein Beispiel:
@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() } }
Dieses Beispiel funktioniert gut. Für den ahnungslosen Blick ist hier nicht viel Besonderes los. Dies funktioniert in Kotlin genauso, wie es in Java funktionieren würde. Schauen wir uns nun ein ähnliches Beispiel an, diesmal jedoch mit Jackson-Anmerkungen:
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 )
Dieses Beispiel schlägt fehl, und der Grund dafür ist, dass es in Kotlin keine Möglichkeit gibt, dem Compiler mitzuteilen, wo genau unsere Annotation angewendet werden soll. Im vorherigen Beispiel mit der Annotation @Entity
, einer Jakarta-Persistenzannotation, werden die Annotationen korrekt auf die Felder angewendet. Wenn wir diesen Code dekompilieren, finden wir Folgendes:
public final class Case { @Id @GeneratedValue( strategy = GenerationType.SEQUENCE ) @Nullable private final Long id; @Nullable private String designation; @Nullable private Long weight;
Aber für Letzteres, das ein Jakarta-Validierungsbeispiel ist, finden wir Folgendes:
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;
Dies bedeutet, dass AccountNumbersDto
von diesen Anmerkungen in seinen Feldern nicht betroffen ist. Im ersten Beispiel hatten wir einfach Glück. Es könnte tatsächlich auch fehlgeschlagen sein. Um anzugeben, wohin unsere Annotation gehen muss, gibt uns Kotlin die Möglichkeit, use-site-Ziele als Präfix für jede Annotation zu verwenden. Voraussetzung ist natürlich, dass die Voraussetzungen erfüllt sind. In diesem Fall besteht eine Möglichkeit, das Problem zu beheben, darin, allen Anmerkungen @field
wie folgt voranzustellen:
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 )
Wenn wir nun versuchen, den resultierenden Bytecode mit IntelliJ zu dekompilieren, werden wir feststellen, dass dies zu diesem Code in Java führt:
public final class AccountNumbersPassiveDto { @NotNull @Nullable private final Long accountNumberLong; @Nullable private final Long accountNumberNullable; @DecimalMax("10") @DecimalMin("5") @org.jetbrains.annotations.NotNull
Das bedeutet, dass die Anmerkungen auf das Feld angewendet wurden und unser Code so funktioniert, wie er soll.
An dieser Stelle können Sie sich wahrscheinlich vorstellen, dass Menschen, die schon lange an Java arbeiten, keine Probleme mit der Umstellung auf Kotlin haben werden, da wir alle wissen, was Felder, Parameter, Eigenschaften usw. sind. Was wir heute beobachten und das beobachte ich auch, ist, dass es den Anschein hat, als ob die Vereinfachung des Codes das Potenzial hat, nach hinten loszugehen. Es gab einige Gelegenheiten, bei denen mir aufgefallen ist, wie viel Zeit ich in Projekten damit verbracht habe, herauszufinden, warum in manchen Projekten oder Modulen die Anmerkungen einfach nicht zu funktionieren scheinen. Und es scheint alles darauf hinauszulaufen, dass die Ziele vor Ort nicht genutzt werden.
Meiner Theorie nach ist es am wahrscheinlichsten, dass neue Generationen von Entwicklern wahrscheinlich Dinge, die meine Generation nicht für kompliziert hielt und aus dem Instinkt heraus lernte, als wirklich kompliziertes Material betrachten werden. Dies ist bei Verwendungsstandortzielen der Fall. Früher waren das Dinge, die wir uns sehr leicht vorstellen konnten. Heutzutage scheint die damit erzeugte Verwirrung jedoch in einigen Fällen dazu zu führen, dass Projekte nicht rechtzeitig entwickelt werden. Ich kann das natürlich nicht verallgemeinern, aber es gibt Potenzial für Gutes und Potenzial für Schlechtes bei Java-Datensätzen und Datenklassen. Wie Datenklassen und Datensätze unsere Zukunft prägen werden, ist schwer zu sagen, aber eines ist klar: Aufgrund dieser Problematik setzen andere Frameworks darauf, völlig annotationsfrei zu werden, wie es bei Ktor der Fall ist.
Ich habe dazu eine Dokumentation auf Scribed: Fields-in-Java-and-Kotlin-and-What-to-Expect und auch auf Slide-Share: Fields-in-Java-and-Kotlin-and-What-to-Expect erstellt.
Sie können die Beispiele, die ich gegeben habe, auch auf GitHub finden. Das Beispiel „Golden Girls“ finden Sie hier: jeorg-kotlin-test-drives und die Beispiele für Jakarta-Persistenz- und Validierungsanmerkungen finden Sie hier: https://github.com/jesperancinha/jeorg-spring-master-test-drives .
Abschließend habe ich dazu auch ein Video auf YouTube erstellt, das ihr euch gleich hier anschauen könnt:
Auchhier veröffentlicht.