【问题标题】:How to implement Builder pattern in Kotlin?如何在 Kotlin 中实现 Builder 模式?
【发布时间】:2016-07-08 12:54:02
【问题描述】:

您好,我是 Kotlin 世界的新手。我喜欢我目前所看到的,并开始考虑将我们在应用程序中使用的一些库从 Java 转换为 Kotlin。

这些库充满了带有 setter、getter 和 Builder 类的 Pojo。现在我用谷歌搜索找到在 Kotlin 中实现构建器的最佳方法,但没有成功。

第二次更新:问题是如何在 Kotlin 中为带有一些参数的简单 pojo 编写 Builder 设计模式?下面的代码是我尝试编写java代码,然后使用eclipse-kotlin-plugin转换成Kotlin。

class Car private constructor(builder:Car.Builder) {
    var model:String? = null
    var year:Int = 0
    init {
        this.model = builder.model
        this.year = builder.year
    }
    companion object Builder {
        var model:String? = null
        private set

        var year:Int = 0
        private set

        fun model(model:String):Builder {
            this.model = model
            return this
        }
        fun year(year:Int):Builder {
            this.year = year
            return this
        }
        fun build():Car {
            val car = Car(this)
            return car
        }
    }
}

【问题讨论】:

  • 你需要modelyear 是可变的吗?在创建Car 之后您会更改它们吗?
  • 我猜它们应该是不可变的。另外,您要确保它们都设置了而不是空的
  • 您也可以使用这个github.com/jffiorillo/jvmbuilder Annotation Processor 自动为您生成构建器类。
  • @JoseF 将它添加到标准 kotlin 的好主意。它对于用 kotlin 编写的库很有用。
  • 大多数答案都忽略了构建器的一个基本但重要的用途,即增量构建一个不可变对象。这有无数种用途,例如,在解析输入时。在这种情况下,为每个事件创建一个新的数据类将是完全浪费的。

标签: design-patterns kotlin


【解决方案1】:

首先,在大多数情况下,您不需要在 Kotlin 中使用构建器,因为我们有默认参数和命名参数。这使您可以编写

class Car(val model: String? = null, val year: Int = 0)

并像这样使用它:

val car = Car(model = "X")

如果您绝对想使用构建器,可以这样做:

将 Builder 设为 companion object 没有意义,因为 objects 是单例。而是将其声明为嵌套类(在 Kotlin 中默认为静态)。

将属性移动到构造函数,以便对象也可以以常规方式实例化(如果不应该将构造函数设为私有)并使用辅助构造函数,该构造函数接受构建器并委托给主构造函数。代码如下所示:

class Car( //add private constructor if necessary
        val model: String?,
        val year: Int
) {

    private constructor(builder: Builder) : this(builder.model, builder.year)

    class Builder {
        var model: String? = null
            private set

        var year: Int = 0
            private set

        fun model(model: String) = apply { this.model = model }

        fun year(year: Int) = apply { this.year = year }

        fun build() = Car(this)
    }
}

用法:val car = Car.Builder().model("X").build()

此代码可以通过使用builder DSL 来缩短:

class Car (
        val model: String?,
        val year: Int
) {

    private constructor(builder: Builder) : this(builder.model, builder.year)

    companion object {
        inline fun build(block: Builder.() -> Unit) = Builder().apply(block).build()
    }

    class Builder {
        var model: String? = null
        var year: Int = 0

        fun build() = Car(this)
    }
}

用法:val car = Car.build { model = "X" }

如果某些值是必需的并且没有默认值,则需要将它们放入构建器的构造函数中,以及我们刚刚定义的build 方法中:

class Car (
        val model: String?,
        val year: Int,
        val required: String
) {

    private constructor(builder: Builder) : this(builder.model, builder.year, builder.required)

    companion object {
        inline fun build(required: String, block: Builder.() -> Unit) = Builder(required).apply(block).build()
    }

    class Builder(
            val required: String
    ) {
        var model: String? = null
        var year: Int = 0

        fun build() = Car(this)
    }
}

用法:val car = Car.build(required = "requiredValue") { model = "X" }

【讨论】:

  • 没什么,但是题主特意问了如何实现builder模式。
  • 我应该纠正自己,构建器模式有一些优点,例如您可以将部分构造的构建器传递给另一种方法。但你说得对,我会补充一句。
  • @KirillRakhman 从 java 调用构建器怎么样?有没有一种简单的方法可以让构建器对 java 可用?
  • 所有三个版本都可以从 Java 中调用,如下所示:Car.Builder builder = new Car.Builder();。然而,只有第一个版本有一个流畅的接口,所以对第二个和第三个版本的调用不能被链接。
  • 我认为顶部的 kotlin 示例仅说明了一种可能的用例。我使用构建器的主要原因是将可变对象转换为不可变对象。也就是说,我需要在“构建”时随着时间的推移对其进行变异,然后提出一个不可变的对象。至少在我的代码中,只有一两个代码示例具有如此多的参数变化,以至于我会使用构建器而不是几个不同的构造器。但是为了制作一个不可变的对象,我有几个案例,构建器绝对是我能想到的最干净的方式。
【解决方案2】:

一种方法是执行以下操作:

class Car(
  val model: String?,
  val color: String?,
  val type: String?) {

    data class Builder(
      var model: String? = null,
      var color: String? = null,
      var type: String? = null) {

        fun model(model: String) = apply { this.model = model }
        fun color(color: String) = apply { this.color = color }
        fun type(type: String) = apply { this.type = type }
        fun build() = Car(model, color, type)
    }
}

使用示例:

val car = Car.Builder()
  .model("Ford Focus")
  .color("Black")
  .type("Type")
  .build()

【讨论】:

  • 非常感谢!你让我今天一整天都感觉很好!您的答案应标记为 SOLUTION。
  • 但是为什么呢?这在 Kotlin 中是不必要的,臃肿,不安全且容易出错。 :( 你甚至可以通过提供一个 init {} 块来做一些验证。请不要将过时的 Java 模式强加到 Kotlin 中。
  • 因为您可能需要在 Java 代码中实例化 Car 类。
【解决方案3】:

我个人从未见过 Kotlin 的构建器,但也许只有我一个人。

所有需要的验证都发生在init 块中:

class Car(val model: String,
          val year: Int = 2000) {

    init {
        if(year < 1900) throw Exception("...")
    }
}

在这里,我冒昧地猜测您并不真的希望 modelyear 可以更改。而且这些默认值似乎没有意义,(尤其是null 用于name)但我留下了一个用于演示目的。

意见: Java 中使用的构建器模式是一种在没有命名参数的情况下生存的方法。在具有命名参数的语言(如 Kotlin 或 Python)中,让构造函数具有长列表(可能是可选的)参数是一个好习惯。

【讨论】:

  • 非常感谢您的回答。我喜欢你的方法,但缺点是对于一个有很多参数的类,使用构造函数和测试类变得不太友好。
  • +Keyhan 您可以通过另外两种方式进行验证,假设验证不会在字段之间发生:1) 在 setter 进行验证的地方使用属性委托 - 这与拥有进行验证的普通 setter 2) 避免对原始的痴迷并创建新的类型以传入验证自己。
  • @Keyhan 这是 Python 中的经典方法,即使对于具有数十个参数的函数也能很好地工作。这里的技巧是使用命名参数(Java 中不可用!)
  • 是的,这也是一个值得使用的解决方案,似乎不像java那样builder类有一些明显的优势,在Kotlin中就不是那么明显了,和C#开发者谈过,C#也有类似kotlin的特性(默认值,您可以在调用构造函数时命名参数)它们也没有使用构建器模式。
  • @vxh.viet 很多这样的情况都可以用@JvmOverloadskotlinlang.org/docs/reference/…解决
【解决方案4】:

因为我使用 Jackson 库从 JSON 中解析对象,所以我需要一个空的构造函数并且我不能有可选字段。此外,所有字段都必须是可变的。然后我可以使用这种与 Builder 模式做同样事情的好语法:

val car = Car().apply{ model = "Ford"; year = 2000 }

【讨论】:

  • 在杰克逊你实际上不需要有一个空的构造函数,并且字段不需要是可变的。您只需使用 @JsonProperty 注释您的构造函数参数
  • 如果您使用-parameters 开关编译,您甚至不必再使用@JsonProperty 进行注释。
  • 杰克逊实际上可以配置为使用构建器。
  • 如果将jackson-module-kotlin模块添加到您的项目中,您只需使用数据类就可以了。
  • 这如何与构建器模式做同样的事情?您正在实例化最终产品,然后换出/添加信息。 Builder 模式的重点是在所有必要信息都存在之前无法获得最终产品。删除 .apply() 会留下一辆未定义的汽车。从 Builder 中删除所有构造函数参数会为您留下 Car Builder,如果您尝试将其构建到汽车中,您可能会因为尚未指定型号和年份而遇到异常。它们不是一回事。
【解决方案5】:

我已经看到了许多声明作为建设者的额外乐趣的例子。我个人喜欢这种方法。节省编写构建器的工作量。

package android.zeroarst.lab.koltinlab

import kotlin.properties.Delegates

class Lab {
    companion object {
        @JvmStatic fun main(args: Array<String>) {

            val roy = Person {
                name = "Roy"
                age = 33
                height = 173
                single = true
                car {
                    brand = "Tesla"
                    model = "Model X"
                    year = 2017
                }
                car {
                    brand = "Tesla"
                    model = "Model S"
                    year = 2018
                }
            }

            println(roy)
        }

        class Person() {
            constructor(init: Person.() -> Unit) : this() {
                this.init()
            }

            var name: String by Delegates.notNull()
            var age: Int by Delegates.notNull()
            var height: Int by Delegates.notNull()
            var single: Boolean by Delegates.notNull()
            val cars: MutableList<Car> by lazy { arrayListOf<Car>() }

            override fun toString(): String {
                return "name=$name, age=$age, " +
                        "height=$height, " +
                        "single=${when (single) {
                            true -> "looking for a girl friend T___T"
                            false -> "Happy!!"
                        }}\nCars: $cars"
            }
        }

        class Car() {

            var brand: String by Delegates.notNull()
            var model: String by Delegates.notNull()
            var year: Int by Delegates.notNull()

            override fun toString(): String {
                return "(brand=$brand, model=$model, year=$year)"
            }
        }

        fun Person.car(init: Car.() -> Unit): Unit {
            cars.add(Car().apply(init))
        }

    }
}

我还没有找到一种方法可以强制在 DSL 中初始化某些字段,例如显示错误而不是抛出异常。如果有人知道,请告诉我。

【讨论】:

    【解决方案6】:

    对于一个简单的类,您不需要单独的构建器。您可以使用 Kirill Rakhman 描述的可选构造函数参数。

    如果你有更复杂的类,那么 Kotlin 提供了一种创建 Groovy 风格的 Builders/DSL 的方法:

    Type-Safe Builders

    这是一个例子:

    Github Example - Builder / Assembler

    【讨论】:

    • 谢谢,但我也想从 java 中使用它。据我所知,可选参数不适用于 java。
    【解决方案7】:

    现在人们应该查看 Kotlin 的 Type-Safe Builders

    使用上述对象创建方式将如下所示:

    html {
        head {
            title {+"XML encoding with Kotlin"}
        }
        // ...
    }
    

    一个很好的“实际”使用示例是vaadin-on-kotlin 框架,它利用类型安全构建器来assemble views and components

    【讨论】:

      【解决方案8】:

      我想说,Kotlin 中的模式和实现几乎相同。由于默认值,您有时可以跳过它,但对于更复杂的对象创建,构建器仍然是一个不能省略的有用工具。

      【讨论】:

      • 对于具有默认值的构造函数,您甚至可以使用initializer blocks 验证输入。但是,如果您需要有状态的东西(这样您就不必预先指定所有内容),那么构建器模式仍然是可行的方法。
      • 你能给我一个简单的例子吗?说一个简单的用户类,其中包含名称和电子邮件字段以及电子邮件验证。
      【解决方案9】:

      我正在处理一个 Kotlin 项目,该项目公开了 Java 客户端使用的 API(无法利用 Kotlin 语言结构)。我们必须添加构建器以使它们在 Java 中可用,所以我创建了一个 @Builder 注释:https://github.com/ThinkingLogic/kotlin-builder-annotation - 它基本上是 Kotlin 的 Lombok @Builder 注释的替代品。

      【讨论】:

        【解决方案10】:

        我迟到了。如果我必须在项目中使用 Builder 模式,我也遇到了同样的困境。后来,经过研究,我意识到这是绝对没有必要的,因为 Kotlin 已经提供了命名参数和默认参数。

        如果您真的需要实施,Kirill Rakhman 的回答是关于如何以最有效的方式实施的可靠答案。您可能会发现它有用的另一件事是https://www.baeldung.com/kotlin-builder-pattern,您可以在实现上与 Java 和 Kotlin 进行比较和对比

        【讨论】:

          【解决方案11】:
          class Foo private constructor(@DrawableRes requiredImageRes: Int, optionalTitle: String?) {
          
              @DrawableRes
              @get:DrawableRes
              val requiredImageRes: Int
          
              val optionalTitle: String?
          
              init {
                  this.requiredImageRes = requiredImageRes
                  this.requiredImageRes = optionalTitle
              }
          
              class Builder {
          
                  @DrawableRes
                  private var requiredImageRes: Int = -1
          
                  private var optionalTitle: String? = null
          
                  fun requiredImageRes(@DrawableRes imageRes: Int): Builder {
                      this.intent = intent
                      return this
                  } 
          
                  fun optionalTitle(title: String): Builder {
                      this.optionalTitle = title
                      return this
                  }
          
                  fun build(): Foo {
                      if(requiredImageRes == -1) {
                          throw IllegalStateException("No image res provided")
                      }
                      return Foo(this.requiredImageRes, this.optionalTitle)
                  }
          
              }
          
          }
          

          【讨论】:

            【解决方案12】:

            我在 Kotlin 中实现了一个基本的 Builder 模式,代码如下:

            data class DialogMessage(
                    var title: String = "",
                    var message: String = ""
            ) {
            
            
                class Builder( context: Context){
            
            
                    private var context: Context = context
                    private var title: String = ""
                    private var message: String = ""
            
                    fun title( title : String) = apply { this.title = title }
            
                    fun message( message : String ) = apply { this.message = message  }    
            
                    fun build() = KeyoDialogMessage(
                            title,
                            message
                    )
            
                }
            
                private lateinit var  dialog : Dialog
            
                fun show(){
                    this.dialog= Dialog(context)
                    .
                    .
                    .
                    dialog.show()
            
                }
            
                fun hide(){
                    if( this.dialog != null){
                        this.dialog.dismiss()
                    }
                }
            }
            

            最后

            Java:

            new DialogMessage.Builder( context )
                   .title("Title")
                   .message("Message")
                   .build()
                   .show();
            

            科特林:

            DialogMessage.Builder( context )
                   .title("Title")
                   .message("")
                   .build()
                   .show()
            

            【讨论】:

              【解决方案13】:

              你可以在 kotlin 中使用可选参数 示例:

              fun myFunc(p1: String, p2: Int = -1, p3: Long = -1, p4: String = "default") {
                  System.out.printf("parameter %s %d %d %s\n", p1, p2, p3, p4)
              }
              

              然后

              myFunc("a")
              myFunc("a", 1)
              myFunc("a", 1, 2)
              myFunc("a", 1, 2, "b")
              

              【讨论】:

                猜你喜欢
                • 1970-01-01
                • 2018-02-02
                • 1970-01-01
                • 1970-01-01
                • 2017-10-02
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                相关资源
                最近更新 更多