如何在Kotlin中实现Builder模式?

Key*_*han 121 design-patterns kotlin

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

这些库充满了带有setter,getter和Builder类的Pojos.现在我用谷歌搜索了什么是在Kotlin中实施Builders的最佳方法,但没有成功.

第二次更新:问题是如何在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
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

Kir*_*man 235

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

class Car(val model: String? = null, val year: Int = 0)
Run Code Online (Sandbox Code Playgroud)

并像这样使用它:

val car = Car(model = "X")
Run Code Online (Sandbox Code Playgroud)

如果你绝对想要使用构建器,那么你可以这样做:

使构建器成为一个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)
    }
}
Run Code Online (Sandbox Code Playgroud)

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

通过使用构建器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)
    }
}
Run Code Online (Sandbox Code Playgroud)

用法: 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)
    }
}
Run Code Online (Sandbox Code Playgroud)

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

  • 我认为顶部的kotlin示例只解释了一个可能的用例.我使用构建器的主要原因是将可变对象转换为不可变对象.也就是说,当我"构建"然后想出一个不可变对象时,我需要随着时间推移变异.至少在我的代码中,只有一个或两个具有如此多参数变量的代码示例,我将使用构建器而不是几个不同的构造函数.但是为了制作一个不可变的对象,我有一些情况下,构建器绝对是我能想到的最干净的方式. (6认同)
  • 可以从Java调用所有三个版本:`Car.Builder builder = new Car.Builder();`.但是只有第一个版本具有流畅的界面,因此无法链接到第二个和第三个版本的调用. (5认同)
  • 我应该纠正自己,构建器模式有一些优点,例如,您可以将部分构造的构建器传递给另一个方法.但你是对的,我会加上一句话. (3认同)
  • @KirillRakhman如何从java调用构建器?是否有一种简单的方法可以使构建器可用于Java? (3认同)
  • 没什么,但问题的作者专门询问了如何实现构建器模式. (2认同)
  • @ycomp 如果您可以将其设为数据类,Kotlin 会生成一个“copy”函数,其参数与构造函数相同,但具有实例的默认值。因此,如果性能不是问题,您可以使用它。或者你可以自己编写 `copy` 函数: class Person(val name: String, valage: Int) { fun copy( name: String = this.name,age: Int = this.age ) = Person(name,age ) } (2认同)

Dav*_*vra 10

因为我使用Jackson库来解析JSON中的对象,所以我需要一个空构造函数,而且我不能有可选字段.所有字段都必须是可变的.然后我可以使用这个与Builder模式相同的漂亮语法:

val car = Car().apply{ model = "Ford"; year = 2000 }
Run Code Online (Sandbox Code Playgroud)

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

小智 8

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

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)
    }
}
Run Code Online (Sandbox Code Playgroud)

用法样本:

val car = Car.Builder()
  .model("Ford Focus")
  .color("Black")
  .type("Type")
  .build()
Run Code Online (Sandbox Code Playgroud)

  • 因为您可能需要在 Java 代码中实例化 Car 类。 (8认同)
  • 但为什么?这在 Kotlin 中是不必要的,臃肿、不安全且容易出错。:( 您甚至可以通过提供 init {} 块来进行一些验证。请不要将过时的 Java 模式强行引入 Kotlin。 (3认同)

vod*_*dan 7

我个人从未见过Kotlin的建筑师,但也许只是我.

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

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

    init {
        if(year < 1900) throw Exception("...")
    }
}
Run Code Online (Sandbox Code Playgroud)

在这里,我冒昧地猜测你并不是真的想要modelyear变化.这些默认值似乎没有任何意义,(特别是null对于name)但我留下了一个用于演示目的.

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

  • 非常感谢你的回答.我喜欢你的方法,但缺点是对于一个有很多参数的类,使用构造函数并测试类不太友好. (2认同)

Ars*_*rst 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))
        }

    }
}
Run Code Online (Sandbox Code Playgroud)

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