如何在Kotlin中实现Builder模式?

221

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

这些库充满了具有 setter、getter 和生成器类的 Pojo。现在我已经搜索了一下如何在 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
你需要将 modelyear 设为可变吗?在创建 Car 实例之后,你会修改它们吗? - voddan
我猜它们应该是不可变的。此外,您要确保它们都被设置了而且不为空。 - Keyhan
1
你也可以使用这个 https://github.com/jffiorillo/jvmbuilder 注解处理器来自动生成构建器类。 - JoseF
https://github.com/ThinkingLogic/kotlin-builder-annotation - Daniil Iaitskov
1
大多数答案都忽略了构建器的一个基本但重要的用途,即逐步构建不可变对象。这有无数的用途,例如,在解析输入时。在这种情况下,为每个事件创建一个新数据类将是完全浪费的。 - Abhijit Sarkar
显示剩余2条评论
18个回答

418

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

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

并这样使用它:

val car = Car(model = "X")

如果你非常想使用构建器,以下是你可以这样做的方式:

将构建器声明为嵌套类(在Kotlin中默认为静态),而不是companion object,因为object是单例的,这样做没有意义。

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

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
4
@KirillRakhman,从Java中调用构建器怎么样?有没有简单的方法使构建器对Java可用? - Keyhan
7
所有三个版本都可以像这样从Java中调用:Car.Builder builder = new Car.Builder();,但只有第一个版本有流畅接口,因此无法链接第二个和第三个版本的调用。 - Kirill Rakhman
14
我认为顶部的Kotlin示例只解释了一种可能的用途。我使用构建器的主要原因是将可变对象转换为不可变对象。也就是说,在“构建”过程中,我需要随时间而改变它,然后得到一个不可变的对象。至少在我的代码中,只有一个或两个示例具有如此多变参数的代码,我才会使用构建器而不是几个不同的构造函数。但要创建不可变对象,我有几种情况下,构建器绝对是我能想到的最干净的方法。 - ycomp
显示剩余14条评论

76

一种方法是采用以下方式:

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

1
非常感谢!你让我的一天都变得美好了!你的答案应该被标记为解决方案。 - sVd
3
为什么要这样做?在 Kotlin 中这是多余的,会使代码变得臃肿,不安全且容易出错。您甚至可以通过提供一个 init {} 块来进行一些验证。请不要将过时的 Java 模式强加到 Kotlin 中。 - spyro
8
因为你可能需要在Java代码中实例化Car类。 - Marcos

12

个人而言,我从未见过使用 Kotlin 的建造者,但这可能只是我的个人经验。

所有必要的验证都在 init 块中进行:

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

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

我猜想您并不想让modelyear可变。此外,这些默认值似乎没有意义(特别是null用于name),但出于演示目的,我保留了其中一个。

个人观点: Java中使用建造者模式来避免使用命名参数。在具有命名参数的语言中(如Kotlin或Python),构造函数最好具有长列表的(也许是可选的)参数。


3
非常感谢您的回答。我喜欢您的方法,但缺点是对于具有许多参数的类,使用构造函数并测试该类变得不太友好。 - Keyhan
1
+Keyhan,假设验证不在字段之间进行,您可以使用另外两种验证方法:1)使用属性委托,在setter中进行验证-这与具有正常setter的验证几乎相同;2)避免原始类型并创建新类型进行传递,以自我验证。 - Jacob Zimmerman
1
@Keyhan 这是 Python 中的经典方法,即使针对具有数十个参数的函数,它也能很好地工作。这里的诀窍在于使用命名参数(这在 Java 中不可用!) - voddan
1
是的,这也是一个值得使用的解决方案,似乎与 Java 不同,在 Kotlin 中,构建器类没有明显的优势。我曾经和 C# 开发人员交谈过,C# 也有类似 Kotlin 的特性(默认值和在调用构造函数时可以命名参数),但他们也没有使用构建器模式。 - Keyhan
1
@vxh.viet 许多这样的情况可以通过使用 @JvmOverloads 解决。http://kotlinlang.org/docs/reference/java-to-kotlin-interop.html#overloads-generation - voddan
显示剩余2条评论

10

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

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

10
在Jackson中,实际上不需要有一个空的构造函数,并且字段不需要是可变的。你只需要用@JsonProperty注释你的构造函数参数即可。 - Bastian Voigt
3
如果你使用 -parameters 开关编译,甚至不需要再使用 @JsonProperty 进行注释。 - Amir Abiri
2
Jackson 实际上可以配置使用构建器。 - Keyhan
3
如果您将jackson-module-kotlin模块添加到项目中,就可以直接使用数据类,它将正常工作。 - Nils Breunese
2
这个做法和建造者模式有什么相同之处?你正在实例化最终产品,然后交换/添加信息。建造者模式的整个重点在于,在所有必要的信息都存在之前,无法获得最终产品。删除.apply()会使你得到一个未定义的汽车。 从Builder中删除所有构造函数参数,你会得到一个Car Builder,如果你试图将其构建成一辆汽车,你可能会因为尚未指定型号和年份而遇到异常。它们不是同一件事情。 - ZeroStatic

7

我看过很多将额外的函数声明为构造器的例子,个人认为这种方法很不错。可以省去编写构造器的麻烦。

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中初始化某些字段,例如显示错误而不是抛出异常。如果有人知道,请告诉我。


3
对于简单的类,您不需要单独的构建器。您可以像Kirill Rakhman描述的那样利用可选的构造函数参数。
如果您有更复杂的类,则Kotlin提供了一种创建Groovy样式Builders/DSL的方法: 类型安全的构建器 这是一个示例: Github示例 - Builder / Assembler

谢谢,但我想在Java中使用它。据我所知,可选参数在Java中不起作用。 - Keyhan

2
现今的人们应该查看 Kotlin 的 类型安全构建器
使用这种对象创建方式会像这样:
html {
    head {
        title {+"XML encoding with Kotlin"}
    }
    // ...
}

一个不错的“实际运用”例子是 vaadin-on-kotlin框架,它利用类型安全的构建器来组装视图和组件

1

以下是稍作修改和改进的答案

class MyDialog {
  private var title: String? = null
  private var content: String? = null
  private var confirmButtonTitle: String? = null
  private var rejectButtonTitle: String? = null

  @DrawableRes
  private var icon: Int? = null


  fun show() {
    // set dialog content here and show at the end
  }

  class Builder {
      private var dialog: MyDialog = MyDialog()

      fun title(title: String) = apply { dialog.title = title }

      fun icon(@DrawableRes icon: Int) = apply { dialog.icon = icon }

      fun content(content: String) = apply { dialog.content = content }

      fun confirmTitle(confirmTitle: String) = apply { dialog.confirmButtonTitle = confirmTitle }

      fun rejectButtonTitle(rejectButtonTitle: String) = apply { dialog.rejectButtonTitle = rejectButtonTitle }

      fun build() = dialog
  }
}

使用方法

MyDialog.Builder()
        .title("My Title")
        .content("My content here")
        .icon(R.drawable.bg_edittext)
        .confirmTitle("Accept")
        .rejectButtonTitle("Cancel")
        .build()
        .show()

老兄,你的代码不行,你在build()方法中返回了一个新创建的MyDialog实例 :) - beretis

1
我认为在 Kotlin 中,模式和实现基本保持不变。有时候由于默认值,您可以跳过它,但对于更复杂的对象创建,构建器仍然是一个有用的工具,不能省略。

就默认值构造函数而言,您甚至可以使用初始化块验证输入。但是,如果您需要某些有状态的内容(这样您就不必一开始就指定所有内容),则建议使用构建器模式。 - mfulton26
你能给我一个简单的带有代码的例子吗?比如一个包含姓名和电子邮件字段以及电子邮件验证的简单用户类。 - Keyhan

1
我来晚了。如果在项目中必须使用构建器模式,我也遇到了同样的困境。后来经过研究,我意识到这是绝对不必要的,因为Kotlin已经提供了命名参数和默认参数。
如果你真的需要实现,Kirill Rakhman的答案是最有效的实现方法。另一个你可能会发现有用的东西是https://www.baeldung.com/kotlin-builder-pattern,你可以比较Java和Kotlin的实现。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接