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 Java la estrategia es una interfaz con un solo método (Descuento), así que cada estrategia puede ser una referencia a método (Estrategia::dosPorUno) o una lambda. Antes de Java 8 había que escribir una clase por estrategia.

import java.util.Comparator;
import java.util.List;

// Estrategia: el algoritmo (cómo se calcula el descuento) se pasa como parámetro y se puede cambiar.
// Como Descuento tiene un solo método, una estrategia puede ser una lambda o una referencia a método.
public class Estrategia {
    @FunctionalInterface
    interface Descuento {
        int aplicar(List<Integer> precios);
    }

    static int sinDescuento(List<Integer> precios) {
        return precios.stream().mapToInt(Integer::intValue).sum();
    }

    static int diezPorCiento(List<Integer> precios) {
        return sinDescuento(precios) * 90 / 100;
    }

    static int dosPorUno(List<Integer> precios) {
        List<Integer> orden = precios.stream().sorted(Comparator.reverseOrder()).toList(); // de más caro a más barato
        int total = 0;
        for (int i = 0; i < orden.size(); i += 2) {
            total += orden.get(i); // de cada pareja se paga la más cara
        }
        return total;
    }

    static final class Caja {
        private final Descuento descuento;

        Caja(Descuento descuento) {
            this.descuento = descuento;
        }

        int cobrar(List<Integer> precios) {
            return descuento.aplicar(precios);
        }
    }

    public static void main(String[] args) {
        List<Integer> precios = List.of(300, 450, 150, 100);
        System.out.println("sin descuento: " + new Caja(Estrategia::sinDescuento).cobrar(precios));
        System.out.println("diez por ciento: " + new Caja(Estrategia::diezPorCiento).cobrar(precios));
        System.out.println("dos por uno: " + new Caja(Estrategia::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 Consumer<String> guardados en una lista, y suscribir devuelve un Runnable que sirve para darse de baja. Java trae una versión clásica en java.beans (PropertyChangeListener), y en los programas con interfaz gráfica los listeners de botones y ventanas son este patrón.

import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;

// Observador: un objeto avisa a todos los que se han suscrito cuando ocurre algo, sin saber quiénes son.
public class Observador {
    static final class Cafetera {
        private final List<Consumer<String>> oyentes = new ArrayList<>();

        // suscribir devuelve un Runnable para darse de baja
        Runnable suscribir(Consumer<String> oyente) {
            oyentes.add(oyente);
            return () -> oyentes.remove(oyente);
        }

        void preparar() {
            for (Consumer<String> oyente : new ArrayList<>(oyentes)) {
                // se recorre una copia por si alguien se da de baja mientras se avisa
                oyente.accept("café listo");
            }
        }
    }

    public static void main(String[] args) {
        Cafetera cafetera = new Cafetera();
        cafetera.suscribir(mensaje -> System.out.println("pantalla: " + mensaje));
        Runnable bajaMovil = cafetera.suscribir(mensaje -> System.out.println("móvil: " + mensaje));

        cafetera.preparar();
        bajaMovil.run();
        System.out.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.