How to implement Builder pattern in Kotlin?

2019-01-12 20:52发布

问题:

Hi I am a newbie in the Kotlin world. I like what I see so far and started to think to convert some of our libraries we use in our application from Java to Kotlin.

These libraries are full of Pojos with setters, getters and Builder classes. Now I have googled to find what is the best way to implement Builders in Kotlin but no success.

2nd Update: The question is how to write a Builder design-pattern for a simple pojo with some parameters in Kotlin? The code below is my attempt by writing java code and then using the eclipse-kotlin-plugin to convert to 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:

First and foremost, in most cases you don't need to use builders in Kotlin because we have default and named arguments. This enables you to write

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

and use it like so:

val car = Car(model = "X")

If you absolutely want to use builders, here's how you could do it:

Making the Builder a companion object doesn't make sense because objects are singletons. Instead declare it as an nested class (which is static by default in Kotlin).

Move the properties to the constructor so the object can also be instantiated the regular way (make the constructor private if it shouldn't) and use a secondary constructor that takes a builder and delegates to the primary constructor. The code will look as follow:

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

Usage: val car = Car.Builder().model("X").builder()

This code can be shortened additionally by using a 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)
    }
}

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

If some values are required and don't have default values, you need to put them in the constructor of the builder and also in the build method we just defined:

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

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



回答2:

Because I'm using Jackson library for parsing objects from JSON, I need to have an empty constructor and I can't have optional fields. Also all fields have to be mutable. Then I can use this nice syntax which does the same thing as Builder pattern:

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


回答3:

I personally have never seen a builder in Kotlin, but maybe it is just me.

All validation one needs happens in the init block:

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

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

Here I took a liberty to guess that you don't really wanted model and year to be changeable. Also those default values seems to have no sense, (especially null for name) but I left one for demonstration purposes.

An Opinion: The builder pattern used in Java as a mean to live without named parameters. In languages with named parameters (like Kotlin or Python) it is a good practice to have constructors with long lists of (maybe optional) parameters.



回答4:

I have seen many examples that declare extra funs as builders. I personally like this approach. Save effort to write builders.

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

    }
}

I have not yet found a way that can force some fields to be initialized in DSL like showing errors instead of throwing exceptions. Let me know if anyone knows.



回答5:

I would say the pattern and implementation stays pretty much the same in Kotlin. You can sometimes skip it thanks to default values, but for more complicated object creation, builders are still a useful tool that can't be omitted.



回答6:

you can use optional parameter in kotlin example:

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

then

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


回答7:

For a simple class you don't need a separate builder. You can make use of optional constructor arguments as Kirill Rakhman described.

If you have more complex class then Kotlin provides a way to create Groovy style Builders/DSL:

Type-Safe Builders

Here is an example:

Github Example - Builder / Assembler



回答8:

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

    }

}


回答9:

I implemented a basic Builder pattern in Kotlin with the follow code:

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

And finally

Java:

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

Kotlin:

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


回答10:

People nowdays should check Kotlin's Type-Safe Builders.

Using said way of object creation will look something like this:

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

A nice 'in-action' usage example is the vaadin-on-kotlin framework, which utilizes typesafe builders to assemble views and components.