patterns pattern patron example diseño design-patterns kotlin

design patterns - pattern - ¿Cómo implementar el patrón Builder en Kotlin?



patron de diseño clone (13)

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 para encontrar 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 } } }


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 }


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 Builder sea un companion object no tiene sentido porque los object 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 método de build 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" }


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.


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.


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



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



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

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

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 que el model y el year fueran cambiantes. Además, esos valores predeterminados parecen no tener sentido (especialmente null para el 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).


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


Yo diría que el patrón y la implementación se mantienen más o menos iguales 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.


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

entonces

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


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