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()
La gente de hoy en día debería revisar los constructores de tipo seguro de Kotlin.
El uso de dicha forma de creación de objetos se verá así:
html {
head {
title {+"XML encoding with Kotlin"}
}
// ...
}
Un buen ejemplo de uso ''en acción'' es el vaadin-on-kotlin , que utiliza constructores de tipo seguro para ensamblar vistas y componentes .
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
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:
Aquí hay un ejemplo:
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)
}
}
}