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 Python una estrategia es, simplemente, una función: Caja recibe la que quiera, porque las funciones son valores. No hace falta una clase por estrategia.

# Estrategia: el algoritmo (cómo se calcula el descuento) se pasa como parámetro y se puede cambiar.
# En Python una estrategia es, simplemente, una función.
def sin_descuento(precios):
    return sum(precios)


def diez_por_ciento(precios):
    return sin_descuento(precios) * 90 // 100


def dos_por_uno(precios):
    orden = sorted(precios, reverse=True)  # de más caro a más barato
    return sum(orden[::2])  # de cada pareja se paga la más cara


class Caja:
    def __init__(self, descuento):
        self._descuento = descuento

    def cobrar(self, precios):
        return self._descuento(precios)


precios = [300, 450, 150, 100]
print(f"sin descuento: {Caja(sin_descuento).cobrar(precios)}")
print(f"diez por ciento: {Caja(diez_por_ciento).cobrar(precios)}")
print(f"dos por uno: {Caja(dos_por_uno).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. Python no trae una clase Observable en su biblioteca estándar: se escribe como arriba, que son pocas líneas.

# Observador: un objeto avisa a todos los que se han suscrito cuando ocurre algo, sin saber quiénes son.
class Cafetera:
    def __init__(self):
        self._oyentes = []

    def suscribir(self, oyente):
        """Devuelve una función para darse de baja."""
        self._oyentes.append(oyente)
        return lambda: self._oyentes.remove(oyente)

    def preparar(self):
        for oyente in list(self._oyentes):  # se recorre una copia por si alguien se da de baja mientras se avisa
            oyente("café listo")


cafetera = Cafetera()
cafetera.suscribir(lambda mensaje: print(f"pantalla: {mensaje}"))
baja_movil = cafetera.suscribir(lambda mensaje: print(f"móvil: {mensaje}"))

cafetera.preparar()
baja_movil()
print("(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.