【问题标题】:How to make a builder for a Kotlin data class with many immutable properties如何为具有许多不可变属性的 Kotlin 数据类创建构建器
【发布时间】:2017-08-06 18:36:34
【问题描述】:

我有一个 Kotlin 数据类,我正在使用许多不可变属性构建它,这些属性是从单独的 SQL 查询中获取的。如果我想使用构建器模式构建数据类,我该如何做到这一点而不使这些属性可变?

例如,而不是通过构造

var data = MyData(val1, val2, val3)

我想用

builder.someVal(val1)
// compute val2
builder.someOtherVal(val2)
// ... 
var data = builder.build()

同时仍然使用 Kotlin 的数据类功能和不可变属性。

【问题讨论】:

  • 为什么不创建 Java 类并使用 Lombok 之类的东西,而不是 copy hack 或滚动您自己的构建器模式?

标签: kotlin builder-pattern


【解决方案1】:

我同意 Grzegorz 答案中的数据复制块,但它本质上与使用构造函数创建数据类的语法相同。如果您想使用该方法并保持所有内容清晰易读,您可能会事先计算所有内容并最终将所有值一起传递。

要拥有更像建造者的东西,您可以考虑以下几点:

假设您的数据类是

data class Data(val text: String, val number: Int, val time: Long)

你可以像这样创建一个可变的构建器版本,使用构建方法来创建数据类:

class Builder {
    var text = "hello"
    var number = 2
    var time = System.currentTimeMillis()

    internal fun build()
            = Data(text, number, time)

}

以及像这样的构建器方法:

fun createData(action: Builder.() -> Unit): Data {
    val builder = Builder()
    builder.action()
    return builder.build()
}

Action 是一个函数,您可以从中直接修改值,createData 之后会直接为您构建一个数据类。 这样,您可以创建一个数据类:

val data: Data = createData {
    //execute stuff here
    text = "new text"
    //calculate number
    number = -1
    //calculate time
    time = 222L
}

没有设置方法,但您可以直接为可变变量分配新值并在构建器中调用其他方法。

您还可以通过为每个变量指定自己的函数来使用 kotlin 的 get 和 set,这样它可以做的不仅仅是设置字段。

也不需要返回当前的构建器类,因为您始终可以访问它的变量。

补充说明:如果您关心,createData 可以缩短为:

fun createData(action: Builder.() -> Unit): Data = with(Builder()) { action(); build() }.

“使用新的构建器,应用我们的操作并构建”

【讨论】:

  • 您的构建器中包含无效/临时数据,这意味着您可以使用该数据轻松实例化数据类,然后使用copy 构造函数完全避免构建器。相反,请使用lateinit 和/或Delegates.notNull(用于原语)。除非设置了所有必要的变量,否则构建器将在运行时产生异常,IMO 是正确的行为!
【解决方案2】:

我认为 Kotlin 没有原生构建器。您始终可以计算所有值并在最后创建对象。

如果您仍想使用构建器,则必须自己实现它。检查this question

【讨论】:

  • 我目前正在像你说的那样在最后创建对象,但是跟踪将传入的所有构造函数参数似乎违反直觉。基本上我必须维护相同的列表数据类和构建数据类实例的代码块中的参数。
  • @BrianVoter 但这也是构建器模型在 Java 中的工作方式。这些值位于构建器类中,并在创建时复制到数据类中。
【解决方案3】:

无需在 Kotlin 中创建自定义构建器 - 为了实现类似构建器的语义,您可以利用 copy 方法 - 它非常适合您想要通过小改动获取对象副本的情况。

data class MyData(val val1: String? = null, val val2: String? = null, val val3: String? = null)

val temp = MyData()
  .copy(val1 = "1")
  .copy(val2 = "2")
  .copy(val3 = "3")

或者:

val empty = MyData()
val with1 = empty.copy(val1 = "1")
val with2 = with1.copy(val2 = "2")
val with3 = with2.copy(val3 = "3")

由于您希望一切都是不可变的,因此必须在每个阶段进行复制。

此外,只要生成器产生的结果是不可变的,在构建器中具有可变属性就可以了。

【讨论】:

  • 让我们记住,在 JVM 中,每次执行 copy 都会创建一个 data class 的新实例。所以例如val temp = MyData().copy(val1 = "1").copy(val2 = "2").copy(val3 = "3") 创建MyData四个 实例以创建单个val。这 + 事实是 data class 属性的 default 值可能是复杂且昂贵的表达式,这意味着所有这些都可以很快失控(性能方面)。
【解决方案4】:

可以使用注释处理器机械化构建器类的创建。 我刚刚创建了ephemient/builder-generator 来演示这一点。

请注意,目前,kapt 可以很好地生成 Java 代码,但生成的 Kotlin 代码存在一些问题(请参阅KT-14070)。出于这些目的,这不是问题,只要可空性注释从原始 Kotlin 类复制到生成的 Java 构建器(以便使用生成的 Java 代码的 Kotlin 代码看到可空/不可空类型,而不仅仅是平台类型)。

【讨论】:

  • 您是否出于任何特殊原因使用 JavaPoet 而不是 KotlinPoet?我希望 Kotlin 有一个我错过的更标准的方法,但这真的很有趣!
  • @BrianVoter 注解处理器 API 提供了 Java 类的视图,恢复原始 Kotlin 语义(例如默认参数)并非易事,因此我发现使用 Java 更容易。
猜你喜欢
  • 1970-01-01
  • 2012-10-16
  • 2021-03-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多