如何在 Kotlin 中实现 Builder 模式?

Posted

技术标签:

【中文标题】如何在 Kotlin 中实现 Builder 模式?【英文标题】:How to implement Builder pattern in Kotlin? 【发布时间】: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 编写的库很有用。 大多数答案都忽略了构建器的一个基本但重要的用途,即增量构建一个不可变对象。这有无数种用途,例如,在解析输入时。在这种情况下,为每个事件创建一个新的数据类将是完全浪费的。 【参考方案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")

【讨论】:

以上是关于如何在 Kotlin 中实现 Builder 模式?的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Kotlin 中实现 OnClickListener 接口? [复制]

如何在 Kotlin 中实现 switch-case 语句

如何通过 Interface Builder 在 Objective-C 中实现搜索栏

如何在 KOTLIN 中实现 buttonX.setOnClickListener(this)? [复制]

如何在 android studio 中实现 Admob 插页式广告 - Kotlin

如何使用 Kotlin 在 RecyclerView Adapter 中实现 onClick 并进行数据绑定