Saltar a contenido

A5.B Patrones de comportamiento

Los patrones de comportamiento tratan de cómo colaboran los objetos y de cómo se reparten el trabajo.

Estrategia: cambiar el algoritmo

La estrategia saca un algoritmo fuera de una clase para poder cambiarlo sin tocarla. Aquí, Caja no sabe cómo se calcula un descuento: recibe una estrategia, y se le puede dar «sin descuento», «diez por ciento» o «dos por uno».

En Kotlin una estrategia es, simplemente, una función: el tipo Descuento es un typealias de (List<Int>) -> Int, y Caja recibe la que quiera con una referencia (::dosPorUno) o una lambda.

// Estrategia: el algoritmo (cómo se calcula el descuento) se pasa como parámetro y se puede cambiar.
// En Kotlin una estrategia es, simplemente, una función.
typealias Descuento = (List<Int>) -> Int

fun sinDescuento(precios: List<Int>) = precios.sum()

fun diezPorCiento(precios: List<Int>) = sinDescuento(precios) * 90 / 100

fun dosPorUno(precios: List<Int>): Int {
    val orden = precios.sortedDescending() // de más caro a más barato
    var total = 0
    for (i in orden.indices step 2) {
        total += orden[i] // de cada pareja se paga la más cara
    }
    return total
}

class Caja(private val descuento: Descuento) {
    fun cobrar(precios: List<Int>) = descuento(precios)
}

fun main() {
    val precios = listOf(300, 450, 150, 100)
    println("sin descuento: ${Caja(::sinDescuento).cobrar(precios)}")
    println("diez por ciento: ${Caja(::diezPorCiento).cobrar(precios)}")
    println("dos por uno: ${Caja(::dosPorUno).cobrar(precios)}")
}

Salida:

sin descuento: 1000
diez por ciento: 900
dos por uno: 600

Los tres resultados salen de la misma caja con distinta estrategia. Añadir un descuento nuevo es escribir una función nueva: no se toca Caja (es la «O» de SOLID, abierto para ampliar y cerrado para modificar). Es el patrón que más se parece a lo que ya viste en A1: pasar una función como parámetro.

Observador: avisar sin conocer

Un observador deja que un objeto avise a otros cuando le pasa algo, sin saber quiénes son ni cuántos. Quien quiere enterarse se suscribe, y puede darse de baja.

Los oyentes son funciones guardadas en una lista, y suscribir devuelve otra función para darse de baja. Con corrutinas, el equivalente moderno es Flow (kotlinx.coroutines): un emisor que muchos pueden recoger. En Android, también LiveData.

// Observador: un objeto avisa a todos los que se han suscrito cuando ocurre algo, sin saber quiénes son.
class Cafetera {
    private val oyentes = mutableListOf<(String) -> Unit>()

    // suscribir devuelve una función para darse de baja
    fun suscribir(oyente: (String) -> Unit): () -> Unit {
        oyentes.add(oyente)
        return { oyentes.remove(oyente) }
    }

    fun preparar() {
        for (oyente in oyentes.toList()) { // se recorre una copia por si alguien se da de baja mientras se avisa
            oyente("café listo")
        }
    }
}

fun main() {
    val cafetera = Cafetera()
    cafetera.suscribir { mensaje -> println("pantalla: $mensaje") }
    val bajaMovil = cafetera.suscribir { mensaje -> println("móvil: $mensaje") }

    cafetera.preparar()
    bajaMovil()
    println("(el móvil se da de baja)")
    cafetera.preparar()
}

Salida:

pantalla: café listo
móvil: café listo
(el móvil se da de baja)
pantalla: café listo

La cafetera no conoce a la pantalla ni al móvil: solo sabe que tiene una lista de oyentes. Después de la baja, el móvil deja de recibir avisos y la pantalla sigue recibiéndolos.

Darse de baja es obligatorio

Un oyente que no se da de baja sigue vivo, aunque ya no se use: consume memoria y puede ejecutar código en un momento en que no debe. Es una fuente clásica de fallos en interfaces gráficas y en Android. Y fíjate en que el ejemplo recorre una copia de la lista al avisar: si un oyente se diera de baja durante el aviso, modificar la lista que se está recorriendo daría error.

Errores frecuentes

Error Cómo evitarlo
Una cadena de if para elegir el algoritmo Pasa el algoritmo como estrategia
Crear una clase por estrategia cuando basta una función Si la estrategia no tiene estado, usa una función
Suscribirse y no darse nunca de baja Guarda la función de baja y llámala al terminar
Que un oyente lento bloquee a los demás Avisa rápido; el trabajo largo, en otra tarea (A3)
Depender del orden en que se avisa a los oyentes No se garantiza en general: no escribas código que lo necesite

Para practicar

Los ejercicios A5.4 y A5.5 piden ordenar con estrategias y un termómetro observable. Para ver la misma idea en los otros lenguajes: Pasar de uno a otro.