¿Cómo implementar el patrón Builder en Kotlin?

146

Hola, soy un novato en el mundo de Kotlin. Me gusta lo que veo hasta ahora y comencé a pensar en convertir algunas de nuestras bibliotecas que usamos en nuestra aplicación de Java a Kotlin.

Estas bibliotecas están llenas de Pojos con setters, getters y clases de Builder. Ahora busqué en Google cuál es la mejor manera de implementar Builders en Kotlin, pero no tuve éxito.

2ª actualización: La pregunta es cómo escribir un patrón de diseño de Builder para un pojo simple con algunos parámetros en Kotlin. El siguiente código es mi intento escribiendo código java y luego usando el eclipse-kotlin-plugin para convertir a 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
        }
    }
}
Keyhan
fuente
1
necesitas modely yearser mutable? ¿Los cambias después de una Carcreación?
voddan
Supongo que deberían ser inmutables, sí. También debes asegurarte de que estén configurados y no vacíos
Keyhan
1
También puede utilizar este procesador de anotaciones github.com/jffiorillo/jvmbuilder para generar la clase de generador automáticamente para usted.
JoseF
@JoseF Buena idea agregarlo a kotlin estándar. Es útil para bibliotecas escritas en kotlin.
Keyhan

Respuestas:

273

En primer lugar, en la mayoría de los casos no es necesario usar constructores en Kotlin porque tenemos argumentos predeterminados y con nombre. Esto te permite escribir

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

y úsalo así:

val car = Car(model = "X")

Si quieres usar constructores, así es como puedes hacerlo:

Hacer que el constructor a companion objectno tenga sentido porque objects son singletons. En cambio, declararlo como una clase anidada (que es estática por defecto en Kotlin).

Mueva las propiedades al constructor para que el objeto también se pueda instanciar de la manera regular (haga que el constructor sea privado si no debería) y use un constructor secundario que tome un constructor y delegue al constructor primario. El código se verá de la siguiente manera:

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

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

Este código se puede acortar adicionalmente mediante el uso de un generador 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)
    }
}

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

Si se requieren algunos valores y no tienen valores predeterminados, debe colocarlos en el constructor del constructor y también en el buildmétodo que acabamos de definir:

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

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

Kirill Rakhman
fuente
2
Nada, pero el autor de la pregunta preguntó específicamente cómo implementar el patrón de construcción.
Kirill Rakhman
44
Debería corregirme, el patrón del generador tiene algunas ventajas, por ejemplo, podría pasar un generador parcialmente construido a otro método. Pero tienes razón, agregaré un comentario.
Kirill Rakhman
3
@KirillRakhman ¿qué tal si llamas al constructor desde Java? ¿Hay alguna manera fácil de hacer que el constructor esté disponible para Java?
Keyhan
66
Las tres versiones se pueden llamar desde Java, así: Car.Builder builder = new Car.Builder();. Sin embargo, solo la primera versión tiene una interfaz fluida, por lo que las llamadas a la segunda y tercera versión no pueden encadenarse.
Kirill Rakhman el
10
Creo que el ejemplo de Kotlin en la parte superior solo explica un posible caso de uso. La razón principal por la que uso constructores es para convertir un objeto mutable en uno inmutable. Es decir, necesito mutarlo con el tiempo mientras estoy "construyendo" y luego encontrar un objeto inmutable. Al menos en mi código solo hay uno o 2 ejemplos de código que tiene tantas variaciones de parámetros que usaría un constructor en lugar de varios constructores diferentes. Pero para hacer un objeto inmutable, tengo algunos casos en los que un constructor es definitivamente la forma más limpia en la que puedo pensar.
ycomp
21

Un enfoque es hacer algo como lo siguiente:

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

Muestra de uso:

val car = Car.Builder()
  .model("Ford Focus")
  .color("Black")
  .type("Type")
  .build()
Dmitrii Bychkov
fuente
¡Muchas gracias! ¡Me has alegrado el día! Su respuesta debe estar marcada como SOLUCIÓN.
sVd
9

Como estoy usando la biblioteca Jackson para analizar objetos desde JSON, necesito tener un constructor vacío y no puedo tener campos opcionales. Además, todos los campos deben ser mutables. Entonces puedo usar esta buena sintaxis que hace lo mismo que el patrón Builder:

val car = Car().apply{ model = "Ford"; year = 2000 }
David Vávra
fuente
8
En Jackson no es necesario tener un constructor vacío, y los campos no necesitan ser mutables. Solo tiene que anotar los parámetros de su constructor con@JsonProperty
Bastian Voigt
2
Ya ni siquiera tiene que anotar @JsonProperty, si compila con el -parametersinterruptor.
Amir Abiri
2
Jackson puede configurarse para usar un generador.
Keyhan
1
Si agrega el módulo jackson-module-kotlin a su proyecto, puede usar clases de datos y funcionará.
Nils Breunese
2
¿Cómo está haciendo esto lo mismo que un patrón de generador? Está creando instancias del producto final y luego intercambiando / agregando información. El objetivo del patrón Builder es no poder obtener el producto final hasta que esté presente toda la información necesaria. Eliminar el .apply () te deja con un auto indefinido. Eliminar todos los argumentos de constructor de Builder te deja con un Car Builder, y si intentas convertirlo en un auto, es probable que encuentres una excepción por no haber especificado el modelo y el año todavía. No són la misma cosa.
ZeroStatic
7

Personalmente, nunca he visto un constructor en Kotlin, pero tal vez solo soy yo.

Toda la validación que uno necesita ocurre en el initbloque:

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

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

Aquí me tomé la libertad de adivinar que realmente no querías modely yearser cambiante. Además, esos valores predeterminados parecen no tener sentido (especialmente nullpara name), pero dejé uno para fines de demostración.

Una opinión: El patrón de construcción utilizado en Java como un medio para vivir sin parámetros con nombre. En lenguajes con parámetros con nombre (como Kotlin o Python), es una buena práctica tener constructores con largas listas de parámetros (quizás opcionales).

voddan
fuente
2
Muchas gracias por la respuesta. Me gusta su enfoque, pero la desventaja es que para una clase con muchos parámetros no es tan fácil usar el constructor y también probar la clase.
Keyhan
1
+ Keyhan de otras dos formas en que puede hacer la validación, suponiendo que la validación no ocurra entre los campos: 1) use delegados de propiedades donde el configurador realiza la validación; esto es casi lo mismo que tener un configurador normal que realiza la validación 2) Evitar obsesión primitiva y crear nuevos tipos para pasar que se validen a sí mismos.
Jacob Zimmerman
1
@Keyhan este es un enfoque clásico en Python, funciona muy bien incluso para funciones con decenas de argumentos. El truco aquí es usar argumentos con nombre (¡no disponible en Java!)
voddan
1
Sí, también es una solución que vale la pena usar, parece diferente a Java, donde la clase de generador tiene algunas ventajas claras, en Kotlin no es tan obvio, hablé con los desarrolladores de C #, C # también tiene características similares a Kotlin (valor predeterminado y podría nombrar parámetros cuando llamando al constructor) tampoco utilizaron el patrón constructor.
Keyhan
1
@ vxh.viet muchos de estos casos se pueden resolver con @JvmOverloads kotlinlang.org/docs/reference/…
voddan el
4

He visto muchos ejemplos que declaran diversiones adicionales como constructores. Personalmente me gusta este enfoque. Ahorre esfuerzo para escribir constructores.

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

    }
}

Todavía no he encontrado una manera de forzar que algunos campos se inicialicen en DSL, como mostrar errores en lugar de lanzar excepciones. Avísame si alguien lo sabe.

Primero
fuente
2

Para una clase simple no necesitas un constructor separado. Puede hacer uso de argumentos de constructor opcionales como describió Kirill Rakhman.

Si tiene una clase más compleja, Kotlin proporciona una forma de crear constructores / DSL de estilo Groovy:

Constructores de tipo seguro

Aquí hay un ejemplo:

Ejemplo de Github: constructor / ensamblador

Dariusz Bacinski
fuente
Gracias, pero también estaba pensando en usarlo desde Java. Hasta donde yo sé, los argumentos opcionales no funcionarían desde Java.
Keyhan
1

Llego tarde a la fiesta. También me encontré con el mismo dilema si tuviera que usar el patrón Builder en el proyecto. Más tarde, después de la investigación, me di cuenta de que es absolutamente innecesario ya que Kotlin ya proporciona los argumentos nombrados y los argumentos predeterminados.

Si realmente necesita implementar, la respuesta de Kirill Rakhman es una respuesta sólida sobre cómo implementar de la manera más efectiva. Otra cosa que puede resultarle útil es https://www.baeldung.com/kotlin-builder-pattern , puede comparar y contrastar con Java y Kotlin en su implementación

Farruh Habibullaev
fuente
0

Yo diría que el patrón y la implementación se mantienen más o menos igual en Kotlin. A veces puede omitirlo gracias a los valores predeterminados, pero para la creación de objetos más complicados, los constructores siguen siendo una herramienta útil que no se puede omitir.

Ritave
fuente
En cuanto a los constructores con valores predeterminados, incluso puede hacer la validación de la entrada utilizando bloques de inicializador . Sin embargo, si necesita algo con estado (para que no tenga que especificar todo por adelantado), entonces el patrón de construcción sigue siendo el camino a seguir.
mfulton26
¿Podría darme un ejemplo simple con código? Digamos una clase de usuario simple con nombre y campo de correo electrónico con validación para correo electrónico.
Keyhan
0

puede usar el parámetro opcional en el ejemplo de 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)
}

luego

myFunc("a")
myFunc("a", 1)
myFunc("a", 1, 2)
myFunc("a", 1, 2, "b")
vuhung3990
fuente
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)
        }

    }

}
Brandon Rude
fuente
0

Implementé un patrón básico de Builder en Kotlin con el siguiente código:

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

Y finalmente

Java:

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

Kotlin:

DialogMessage.Builder( context )
       .title("Title")
       .message("")
       .build()
       .show()
Moisés Portillo
fuente
0

Estaba trabajando en un proyecto de Kotlin que expuso una API consumida por clientes Java (que no puede aprovechar las construcciones del lenguaje Kotlin). Tuvimos que agregar constructores para hacerlos utilizables en Java, así que creé una anotación @Builder: https://github.com/ThinkingLogic/kotlin-builder-annotation : es básicamente un reemplazo de la anotación Lombok @Builder para Kotlin.

YetAototherMatt
fuente