Saltar a contenido

A5.A Patrones de creación

Para quién es esto

Es material avanzado: da por sabidas las unidades 1 a 6 (clases, herencia, interfaces y SOLID) y A1 (funciones como valores).

Un patrón de diseño es una solución conocida a un problema que se repite al diseñar programas. No es una librería ni código que copiar: es una forma de organizar clases que otros programadores ya reconocen. Saber su nombre sirve para algo muy práctico: decir «esto es una fábrica» comunica en tres palabras lo que sin él costaría un párrafo.

Los patrones de creación tratan de cómo se crean los objetos. Todos los ejemplos de esta unidad usan una cafetería.

Singleton: una sola instancia

Es una clase de la que solo puede existir un objeto, y todos reciben el mismo. Sirve para algo realmente único: la configuración de la aplicación, un registro de mensajes.

En Kotlin no hace falta escribir el patrón: una declaración object ya es un singleton, creado de forma segura la primera vez que se usa. Con a === b se comprueba que son el mismo objeto.

// Singleton: una clase de la que solo puede existir UNA instancia (aquí, la configuración de la cafetería).
// En Kotlin no hace falta escribir el patrón: una declaración «object» ya es un singleton garantizado por el lenguaje.
object Configuracion {
    val local = "Café Central"
    val iva = 10
}

fun main() {
    val a = Configuracion
    val b = Configuracion
    println("misma instancia: ${if (a === b) "sí" else "no"}")
    println("local: ${a.local}, IVA ${a.iva}%")
}

Salida:

misma instancia: sí
local: Café Central, IVA 10%

El patrón más discutido

Un singleton es estado global con buena presentación: cualquier parte del programa puede cambiarlo, es difícil de sustituir en las pruebas (A4) y esconde dependencias. Úsalo solo si de verdad tiene que haber uno, y si puedes, pásalo como parámetro en lugar de pedírselo a la clase desde cualquier sitio.

Fábrica: decidir qué clase crear

Una fábrica es una función que, a partir de un dato (aquí un nombre), decide qué clase concreta crear. Quien la usa solo conoce el tipo común (Bebida), no las clases concretas: añadir un tipo nuevo no obliga a cambiar el resto del programa.

El when devuelve la clase que toca. Al ser Bebida una sealed interface, el compilador conoce todas las variantes.

// Fábrica: una función decide QUÉ clase concreta crear, y quien la usa solo conoce el tipo común (Bebida).
sealed interface Bebida {
    val nombre: String
    val precio: Int // en céntimos
}

object Cafe : Bebida {
    override val nombre = "café"
    override val precio = 150
}

object Te : Bebida {
    override val nombre = "té"
    override val precio = 120
}

object Zumo : Bebida {
    override val nombre = "zumo"
    override val precio = 200
}

fun fabricar(nombre: String): Bebida = when (nombre) {
    "café" -> Cafe
    "té" -> Te
    "zumo" -> Zumo
    else -> throw IllegalArgumentException("$nombre: no está en la carta")
}

fun main() {
    for (nombre in listOf("café", "té", "zumo", "chocolate")) {
        try {
            val bebida = fabricar(nombre)
            println("${bebida.nombre}: ${bebida.precio} céntimos")
        } catch (e: IllegalArgumentException) {
            println(e.message)
        }
    }
}

Salida:

café: 150 céntimos
té: 120 céntimos
zumo: 200 céntimos
chocolate: no está en la carta

Es la «D» de SOLID (unidad 6) en acción: el código depende de una abstracción (Bebida), no de las clases concretas. Y los datos desconocidos se rechazan en un solo lugar, con un mensaje claro.

Builder: construir paso a paso

Un builder construye un objeto con muchos datos opcionales, con llamadas encadenadas que dicen qué es cada cosa. Lo que no se indica toma su valor por defecto.

En Kotlin los parámetros con nombre y valores por defecto (Pedido(bebida = "capuchino", leche = "avena")) resuelven casi siempre el mismo problema sin escribir un builder. Úsalo cuando construir tenga pasos o reglas. apply { ... } devuelve el propio objeto, y por eso sirve para encadenar.

// Builder: construir un objeto con muchos datos opcionales paso a paso, con llamadas encadenadas.
// (En Kotlin lo habitual es usar parámetros con nombre y valores por defecto: Pedido(bebida = "capuchino", leche = "avena").
//  El builder sigue siendo útil cuando la construcción tiene pasos o reglas.)
class Pedido private constructor(
    private val bebida: String,
    private val tamano: String,
    private val leche: String,
    private val azucar: Boolean,
) {
    override fun toString() = "$bebida ($tamano), leche: $leche, ${if (azucar) "con" else "sin"} azúcar"

    class Builder {
        private var bebida = "café"
        private var tamano = "pequeño"
        private var leche = "ninguna"
        private var azucar = false

        fun bebida(b: String) = apply { bebida = b } // apply devuelve el propio objeto: permite encadenar
        fun tamano(t: String) = apply { tamano = t }
        fun leche(l: String) = apply { leche = l }
        fun conAzucar() = apply { azucar = true }

        fun construir() = Pedido(bebida, tamano, leche, azucar)
    }
}

fun main() {
    println(Pedido.Builder().bebida("capuchino").tamano("grande").leche("avena").construir())
    println(Pedido.Builder().conAzucar().construir()) // lo que no se indica toma el valor por defecto
}

Salida:

capuchino (grande), leche: avena, sin azúcar
café (pequeño), leche: ninguna, con azúcar

Errores frecuentes

Error Cómo evitarlo
Usar un singleton «porque es cómodo» Pregúntate si de verdad tiene que haber una sola instancia; si no, pásala como parámetro
Una fábrica con un switch enorme que crece sin parar Cuando crezca, usa un diccionario o un registro de clases
Un builder para una clase de dos campos Si bastan parámetros con nombre o un constructor, no lo uses
Olvidar validar al final de la construcción La validación va en construir(): un objeto a medias no debería existir

Para practicar

Los ejercicios A5.1, A5.2 y A5.3 piden una fábrica de figuras, un registro único y un correo con builder. Para ver la misma idea en los otros lenguajes: Pasar de uno a otro.