paint-brush
如何解决处理使用站点目标时的 Kotln 问题经过@jesperancinha
106 讀數

如何解决处理使用站点目标时的 Kotln 问题

经过 João Esperancinha10m2024/03/21
Read on Terminal Reader
Read this story w/o Javascript

太長; 讀書

我们为什么以及在哪里应该在 Kotlin 中使用使用站点目标,以及在使用 Spring 框架等框架时,Kotlin 中的使用站点目标有何不同。
featured image - 如何解决处理使用站点目标时的 Kotln 问题
João Esperancinha HackerNoon profile picture
0-item
1-item
2-item
3-item


Kotlin 最大的优点之一也可能是它的弱点。当我们今天谈论数据类时,以及以同样的方式谈论 Java 记录时,我们倾向于(至少我认识的许多人)将重点放在完全消除样板代码上。样板代码多年来一直困扰着开发人员。它不仅存在于Java中,其他语言也有。如果我们考虑以任何方式创建数据访问对象,那么使用 Kotlin、Java 或任何其他语言来创建并不重要。


我们经常重复相同的过程:存储库创建、服务和控制器。无论如何,我们所做的很多事情仍然是样板文件。然而,样板代码可能是创建数据类或 Java 记录决策的一部分,软件工程的一个更重要的范例是创建这些类型的数据结构的实际焦点。


这一切都归结为不变性或在任何情况下其可能性。让我们回滚时间并回到 1995 年,当时我们开始创建数据结构,在其中指定不同的事物。我们将定义字段、getter、setter、属性、访问器、属性和参数,以便能够将参数传递给方法、函数或构造函数。在这一部分,我们将拥有一些有趣的节目,使用 80 年代中期系列中的角色:The Golden Girls。话虽如此,我们来看看一个类在 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 + '\'' + '}'; } }


仅为了创建两个构造函数就需要输入大量代码。尽管 hashcode 和 equals 不一定在所有情况下都是必须的,但我们几乎总是必须在更通用的构造函数旁边实现一个无参数构造函数。创建所有这些样板代码的好处是我们非常清楚地知道编译为字节码后一切会是什么样子。


然而,无论如何,样板代码对于许多开发人员来说始终是一个有争议的问题,因此 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 中的 Golden girls 实现现在看起来像这样:

 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" ); } }


直到今天,代码简化和代码减少似乎还在继续。然而,随着样板代码的减少,字段、getter、setter、属性、访问器、属性和参数的概念变得不那么直观,也更不容易在我们的脑海中映射。无论我们是否喜欢,这些概念仍然是 JVM 如何工作并使用字节码组织代码。但是代码过度简化实际上是为了使代码更易于阅读,对于数据类和 java 记录,这个想法也是为了创建数据结构是不可变的或部分不可变的。


Java 记录确实是不可变的,因为它包含的所有值或引用都无法修改。出于同样的原因,Kotlin 数据类也可以是真正不可变的,但它们不必这样做,这在某种程度上允许开发人员创建复杂且最糟糕的可变代码。无论如何,这一切都很好,直到我们需要与 Spring 框架等严重依赖注释的框架一起使用。这是一个例子:

 @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() } }


这个例子效果很好。对于毫无戒心的人来说,这里并没有发生什么特别的事情。这在 Kotlin 中有效,就像在 Java 中一样。但现在让我们看一个类似的例子,但这次带有 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 )


这个例子将会失败,原因是在 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 使我们可以使用使用站点目标作为任何注释的前缀。要求当然是满足条件。在这种情况下,解决此问题的一种方法是为所有注释添加@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 记录和数据类可能有好的一面,也有可能坏的一面。数据类和记录将如何塑造我们的未来很难说,但有一点是明确的,由于这个问题,其他框架正在押注于完全无注释,就像 Ktor 的情况一样。


我在 Scribed: Fields-in-Java-and-Kotlin-and-What-to-Expect上创建了有关此内容的文档,也在幻灯片共享上创建了相关文档: 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 上制作了一个有关此内容的视频,您可以在这里观看:

也发布在这里