Patrón constructor
Parte de la sección Programación orientada a objetos del Journey de Kotlin de Coddy. Lección 48 de 57.
El patrón builder crea un objeto paso a paso y comprueba el conjunto completo al final. El builder recopila las partes en métodos encadenables que devuelven el propio builder, y build() crea el objeto terminado:
data class Email(val to: List<String>, val subject: String)
class EmailBuilder {
private val recipients = mutableListOf<String>()
private var subject = ""
fun to(address: String): EmailBuilder {
recipients.add(address)
return this
}
fun subject(text: String): EmailBuilder {
subject = text
return this
}
fun build(): Email {
check(recipients.isNotEmpty()) { "no recipients" }
return Email(recipients.toList(), subject)
}
}Dentro de main:
val mail = EmailBuilder().to("ada@x.io").to("bo@x.io").subject("Lunch").build()
println(mail)Salida:
Email(to=[ada@x.io, bo@x.io], subject=Lunch)apply acorta cada paso: ejecuta un bloque con el objeto como this y devuelve el objeto, de modo que un paso se convierte en una sola línea. check(condition) { message } lanza IllegalStateException, la contraparte de require para el estado del objeto:
class RequestBuilder {
var url = ""
var method = "GET"
private val headers = mutableListOf<String>()
fun header(name: String, value: String) = apply { headers.add("$name: $value") }
fun build(): String {
check(url.isNotEmpty()) { "no url" }
return "$method $url " + headers
}
}Dentro de main:
val request = RequestBuilder().apply {
url = "/users"
method = "POST"
header("Accept", "json")
}.build()
println(request)Salida:
POST /users [Accept: json]Una función que recibe una lambda con receptor, de tipo Builder.() -> Unit, convierte el builder en un pequeño DSL (lenguaje específico de dominio): dentro de las llaves, el builder es this, por lo que quien llama solo escribe la configuración:
class MenuBuilder(private val title: String) {
private val items = mutableListOf<String>()
fun item(name: String, cents: Int) {
items.add("$name %d.%02d".format(cents / 100, cents % 100))
}
fun build() = "== $title ==\n" + items.joinToString("\n")
}
fun menu(title: String, block: MenuBuilder.() -> Unit): String = MenuBuilder(title).apply(block).build()Dentro de main:
val lunch = menu("Lunch") {
item("soup", 450)
item("salad", 600)
}
println(lunch)Salida:
== Lunch ==
soup 4.50
salad 6.00Los argumentos con nombre y predeterminados de Kotlin ya cubren los casos sencillos: Pizza(size = 30, cheese = true) nombra cada opción en el sitio de la llamada. Un builder resulta útil cuando el objeto se construye en varios pasos, recopila listas o necesita comprobaciones entre campos antes de poder existir:
data class Pizza(val size: Int = 26, val cheese: Boolean = true, val olives: Boolean = false)
val simple = Pizza(size = 30, olives = true) // no se necesita builder
val mail = EmailBuilder() // un builder para ensamblaje paso a paso
.to("ada@x.io")
.to("bo@x.io") // recopila una lista
.subject("Plan")
.build() // comprueba todo al finalDesafío
FácilEscribe un builder para la clase de datos proporcionada Email. EmailBuilder tiene pasos encadenables: to(address) (se puede llamar varias veces y añade destinatarios), subject(text), body(text) y urgent(). build() devuelve un Email o lanza IllegalStateException con no recipients o no subject (en ese orden de comprobación). Un correo urgente obtiene [URGENT] delante de su asunto.
El código proporcionado lee comandos (to ada@x.io, subject Hello, body text, urgent, send), inicia un nuevo builder después de cada línea --- e imprime el correo construido, To: ada@x.io, bo@x.io | [URGENT] Hello | text, o error: no subject.
Tu código va en EmailBuilder.kt y Email.kt. Main.kt contiene el código de entrada/salida proporcionado y no se puede editar.
Pruébalo tú mismo
fun main() {
// Código de entrada/salida suministrado: déjalo tal como está
val input = generateSequence(::readLine).toList()
var builder = EmailBuilder()
for (cmd in input) {
when {
cmd == "send" -> {
try {
println(builder.build())
} catch (e: IllegalStateException) {
println("error: ${e.message}")
}
}
cmd == "---" -> builder = EmailBuilder()
cmd == "urgent" -> builder.urgent()
cmd.startsWith("to ") -> builder.to(cmd.drop(3))
cmd.startsWith("subject ") -> builder.subject(cmd.drop(8))
else -> builder.body(cmd.drop(5))
}
}
}
Esta lección incluye un breve cuestionario. Empieza la lección para responderlo y registrar tu progreso.
Todas las lecciones de Programación orientada a objetos
1Clases y objetos
Trabajar con archivosDefinir clasesConstructores primariosBloques init y validaciónConstructores secundariosRepaso - Tarjeta de biblioteca4Herencia
Clases abiertas y subclasesSobrescritura de miembrossuper y constructoresClases abstractasRepaso: directorio del personal2Propiedades y encapsulación
Accesores personalizadosModificadores de visibilidadSetters privadoslazy y lateinitRepaso: cuenta bancaria5Interfaces
Declaración de interfacesMétodos predeterminadosInterfaces múltiplesInterfaces como tiposRepaso: métodos de pago8Genéricos
Funciones genéricasClases genéricasRestricciones de tiposVariancia con in y outRepaso - Cola de prioridad11Proyecto: Gestión de biblioteca
Libros y miembrosPréstamo de librosPractica por tu cuenta: Playground de Kotlin