Kotlin の最大の長所の 1 つは、同時に弱点にもなります。今日、データ クラスについて話すとき、そして Java レコードについて話すとき、少なくとも私が知っている多くの人は定型コードの完全な削除に焦点を当てる傾向があります。ボイラープレート コードは、長年にわたって開発者を悩ませてきました。これは Java にだけ存在するものではなく、他の言語にも同様に存在します。データ アクセス オブジェクトを何らかの方法で作成することを考えた場合、それを Kotlin で行うか、Java で行うか、またはその他の言語で行うかは、あまり重要ではありません。
多くの場合、リポジトリの作成、サービス、コントローラーという同じプロセスを繰り返します。私たちが行っていることの多くは、依然として定型的なものです。データ クラスまたは Java レコードを作成する決定には定型コードが含まれていた可能性がありますが、ソフトウェア エンジニアリングのより重要なパラダイムは、これらのタイプのデータ構造を作成する実際の焦点でした。
それはすべて、不変性、またはいずれにしてもその可能性について要約されます。時間を巻き戻して、ある時点でさまざまなものを指定するデータ構造の作成を開始した 1995 年に戻りましょう。メソッド、関数、コンストラクターのいずれかに引数を渡せるように、フィールド、ゲッター、セッター、プロパティ、アクセサー、属性、パラメーターを定義します。この部分については次のようになります。 80 年代半ばのシリーズ、ゴールデン ガールズのキャラクターを使った楽しいプログラミング。これを踏まえて、約 15 年間のクラスがどのようになるかを見てみましょう。
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 + '\'' + '}'; } }
2 つのコンストラクターを作成するためだけに入力するコードは膨大な量でした。ハッシュコードとイコールは必ずしもすべての場合に必須というわけではありませんが、ほとんどの場合、より一般的なコンストラクターの隣に引数なしのコンストラクターを実装する必要がありました。この定型コードをすべて作成して良かった点は、バイトコードにコンパイルした後にすべてがどのように見えるかが非常に明確にわかっていたことです。
しかし、いずれにせよ、定型コードは多くの開発者にとって常に議論の的となる問題であったため、2009 年に lombok が登場し、データ構造の作成方法に革命をもたらしました。アノテーション プロセッサを使用し、クラスに必要な品質を与える特定のアノテーションを解釈するという概念が導入されました。そのため、lombok アノテーション付きクラスは次のようになります。
@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"; } }
そしてしばらくの間、人々はロンボク島について非常に満足していました。ついに、定型コードをすべて作成しなければならないという重みがなくなりました。しかし、その低迷は約 7 年間続き、新しいプレーヤーが到着しました。そして今度はソフトウェア開発業界が予想していなかったことでした。これは Kotlin と呼ばれ、2016 年にデータ クラスを即座に導入してデビューしました。 Kotlin でのゴールデン ガールズの実装は次のようになります。
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" )
Kotlin はゆっくりとファンを集め始めましたが、一方で Java は市場に何か他のものがあることに気づき、開発中だったもののしばらく後回しにされていたプロジェクト Loom のように、その側の開発の一部が勢いを増し始めました。そのため、20202 年の Java 14 のリリースでは、Java に Java レコードが導入され、Java のデータ構造は次のようになりました。
public record GoldenGirlsRecord( String goldenGirl1, String goldenGirl2, String goldenGirl3, String goldenGirl4 ) { public GoldenGirlsRecord() { this( "Dorothy Zbornak", "Rose Nylund", "Blanche Devereaux", "Sophia Petrillo" ); } }
そして今日に至るまで、コードの簡素化とコード削減は続くようです。しかし、ボイラープレート コードが削減されると、フィールド、ゲッター、セッター、プロパティ、アクセサー、属性、パラメーターの概念がはるかに視覚的でなくなり、頭の中でマッピングするのが難しくなりました。私たちが好むと好まざるにかかわらず、これらの概念は依然として JVM の動作方法であり、コードをバイトコードで編成する方法です。しかし、コードの過度の単純化は実際にはコードを読みやすくすることを目的としており、データ クラスと Java レコードの場合は、データ構造を作成するという考え方もあります。不変または部分的に不変です。
Java レコードは、それに含まれるすべての値や参照が変更できないという意味で、真に不変です。同じ理由で、Kotlin データ クラスも真に不変にすることもできますが、その必要はなく、ある意味、開発者に複雑で最悪の、変更可能なコードを作成する権限を与えます。いずれにせよ、必要になるまではこれで十分です。アノテーションに大きく依存する Spring Framework のようなフレームワークを使用します。以下に例を示します。
@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() } }
この例は正常に動作します。疑いを持たない目には、ここで特別なことが起こっているわけではありません。これは、Java で動作するのと同じように、Kotlin でも動作します。ここで、同様の例を見てみましょう。今回はジャクソンの注釈が付けられています。
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 )
この例は失敗します。その理由は、Kotlin では、アノテーションを正確にどこに適用するかをコンパイラーに伝える方法がないためです。 @Entity
注釈 (jakarta 永続性注釈) を使用した前の例では、注釈はフィールドに正しく適用されます。そのコードを逆コンパイルすると、次のことがわかります。
public final class Case { @Id @GeneratedValue( strategy = GenerationType.SEQUENCE ) @Nullable private final Long id; @Nullable private String designation; @Nullable private Long weight;
しかし、後者のジャカルタ検証の例では、次のことがわかります。
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;
これは、 AccountNumbersDto
フィールド上のこれらの注釈の影響を受けていないことを意味します。最初の例では単に幸運だっただけです。実際には失敗した可能性もあります。アノテーションの宛先を指定するために、Kotlin では use-site ターゲットをアノテーションのプレフィックスとして使用することができます。もちろん条件を満たしていることが条件となります。この場合、これを修正する 1 つの方法は、次のようにすべての注釈の前に@field
を付けることです。
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 )
IntelliJ を使用して結果のバイトコードを逆コンパイルしようとすると、Java では次のコードが生成されることがわかります。
public final class AccountNumbersPassiveDto { @NotNull @Nullable private final Long accountNumberLong; @Nullable private final Long accountNumberNullable; @DecimalMax("10") @DecimalMin("5") @org.jetbrains.annotations.NotNull
これは、注釈がフィールドに適用され、コードが想定どおりに動作することを意味します。
おそらくこの時点で、長い間 Java に取り組んできた人は、フィールド、パラメータ、プロパティなどが何であるかを誰もが知っているため、問題なく Kotlin に適応できるだろうと想像できるでしょう。私たちが今日観察し、私もそれを目撃しているのは、コードの簡素化が裏目に出る可能性があるように見えるということです。一部のプロジェクトやモジュールで注釈が機能しないように見える理由を理解するために、プロジェクトで時間が費やされていることに気づく機会がかなりありました。そしてそれは結局のところ、使用場所ターゲットを活用していないことに尽きるようです。
最も可能性が高いことについての私の理論は、新しい世代の開発者はおそらく、私の世代が複雑だと考えず、直感で学んだものを、本当に複雑な素材として見るだろうということです。使用場所ターゲットの場合も同様です。これらは以前は非常に簡単に視覚化できたものでした。しかし現在では、これによって生じた混乱により、場合によってはプロジェクトの予定通りの開発が遅れているようです。もちろん一般化することはできませんが、Java レコードとデータ クラスには良い影響もあれば悪い影響もある可能性があります。データクラスとレコードが私たちの将来をどのように形作るのかを語るのは難しいですが、1つ明らかなことは、この問題があるため、他のフレームワークはKtorの場合と同様に完全にアノテーションフリーになることに賭けているということです。
これに関するドキュメントを Scribed: Fields-in-Java-and-Kotlin-and-What-to-Expect で作成し、 slide-share: fields-in-java-and-kotlin-and-what-to-expect にも作成しました。
GitHub で私が挙げた例を見つけることもできます。 Golden Girls の例はjeorg-kotlin-test-drivesにあり、jakarta 永続性と検証アノテーションの例はhttps://github.com/jesperancinha/jeorg-spring-master-test-drivesにあります。
最後に、これに関するビデオも YouTube に作成しましたので、こちらでご覧ください。
ここでも公開されています。