如何在Kotlin中实现Builder模式?


144

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

这些库充满了带有setter,getter和Builder类的Pojo。现在,我已经在Google上搜寻以找到在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
        }
    }
}

1
您需要model并且year变得易变吗?Car创建后您会更改它们吗?
voddan

我想他们应该是一成不变的。另外,您还想确保它们都设置为非空
Keyhan

1
您还可以使用此github.com/jffiorillo/jvmbuilder批注处理器为您自动生成构建器类。
JoseF

@JoseF好的将其添加到标准kotlin中。这对于用kotlin编写的库很有用。
凯恩

Answers:


271

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

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

并像这样使用它:

val car = Car(model = "X")

如果您绝对要使用构建器,请按以下步骤操作:

使Builder为a companion object没有意义,因为object s是单例。而是将其声明为嵌套类(在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()

可以使用构建器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" }


2
没什么,但是问题的作者专门询问了如何实现构建器模式。
Kirill Rakhman

4
我应该纠正自己,构建器模式具有一些优点,例如,您可以将部分构建的构建器传递给另一种方法。但是你是对的,我再说一句话。
Kirill Rakhman

3
@KirillRakhman如何从Java调用构建器?有没有一种简单的方法可以使生成器可用于Java?
Keyhan '16

6
可以从Java调用这三个版本,如下所示:Car.Builder builder = new Car.Builder();。但是,只有第一个版本具有流畅的界面,因此无法链接到第二个和第三个版本的调用。
Kirill Rakhman '16

10
我认为顶部的kotlin示例仅说明一种可能的用例。我使用生成器的主要原因是将可变对象转换为不可变对象。也就是说,我在“构建”时需要随着时间对其进行变异,然后提出一个不可变的对象。至少在我的代码中,只有一个或两个示例代码具有如此多的参数变化,因此我将使用构建器而不是几个不同的构造器。但是要创建一个不变的对象,在某些情况下,构建器绝对是我能想到的最干净的方法。
ycomp

19

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

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

非常感谢!你让我今天一整天都感觉很好!您的答案应标记为“解决方案”。
sVd

9

因为我正在使用Jackson库从JSON解析对象,所以我需要有一个空的构造函数,并且不能有可选字段。而且所有字段都必须是可变的。然后,我可以使用这种不错的语法,该语法与Builder模式相同:

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

8
在Jackson中,您实际上不需要具有空的构造函数,并且字段不需要是可变的。您只需使用@JsonProperty
Bastian Voigt

2
@JsonProperty如果您使用该-parameters开关进行编译,则甚至不必再进行注释。
阿米尔·阿比里

2
实际上,可以将Jackson配置为使用构建器。
Keyhan

1
如果将jackson-module-kotlin模块添加到您的项目中,则只需使用数据类即可。
Nils Breunese

2
这与Builder Pattern如何做相同的事情?您要实例化最终产品,然后换出/添加信息。Builder模式的重点是在所有必要信息都出现之前才能获得最终产品。删除.apply()会留下不确定的汽车。从Builder中删除所有构造函数参数后,您将得到Car Builder,如果尝试将其构建到汽车中,则可能会因未指定模型和年份而遇到异常。它们不是同一件事。
ZeroStatic

7

我个人从未在Kotlin见过建筑商,但也许只有我一个人。

所有验证一项需求都发生在该init块中:

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

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

在这里,我冒昧地猜测您并不是真的想要model并且year可以改变。这些默认值似乎也没有意义(尤其是null对于name),但我出于示范目的保留了一个默认值。

意见: Java中使用的构建器模式作为没有命名参数的生存手段。在具有命名参数的语言中(例如Kotlin或Python),最好让构造函数带有一长串(可能是可选的)参数的构造函数。


2
非常感谢您的回答。我喜欢您的方法,但缺点是对于带有许多参数的类,使用构造函数并测试该类变得不太友好。
Keyhan

1
+ Keyhan您可以通过两种其他方式进行验证,假设在字段之间不进行验证:1)在setter进行验证的地方使用属性委托-这与使用普通setter进行验证的东西几乎一样2)避免原始的迷恋并创建新的类型以通过验证自己。
雅各布·齐默尔曼

1
@Keyhan这是Python中的经典方法,即使对于带有数十个参数的函数,它也能很好地工作。这里的技巧是使用命名参数(Java中不可用!)
voddan

1
是的,它也是一个值得使用的解决方案,似乎与Java相比,builder类具有明显的优势,在Java中它并不那么明显,与C#开发人员交谈,C#也具有类似于kotlin的功能(默认值,您可以在调用构造函数),他们也没有使用构建器模式。
Keyhan '16

1
@ vxh.viet可以使用@JvmOverloads kotlinlang.org/docs/reference/…
voddan

4

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

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中初始化某些字段,例如显示错误而不是引发异常。让我知道是否有人知道。




1

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

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


0

我要说的是,模式和实现在Kotlin中几乎保持不变。有时由于默认值而可以跳过它,但是对于更复杂的对象创建而言,构建器仍然是一个不可忽略的有用工具。


至于具有默认值的构造函数,您甚至可以使用初始化程序块来验证输入。但是,如果您需要有状态的东西(这样就不必预先指定所有内容),那么仍然可以使用构建器模式。
mfulton16年

您能给我一个简单的代码示例吗?用名称和电子邮件字段说一个简单的User类,其中包含电子邮件验证。
Keyhan

0

您可以在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")

0
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)
        }

    }

}

0

我使用以下代码在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()

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.