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 Dart una estrategia puede ser, simplemente, una función: el tipo Descuento es un typedef de int Function(List<int>), y Caja recibe la que quiera. 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 Dart una estrategia puede ser, simplemente, una función.
typedef Descuento = int Function(List<int> precios);

int sinDescuento(List<int> precios) => precios.fold(0, (a, b) => a + b);

int diezPorCiento(List<int> precios) => sinDescuento(precios) * 90 ~/ 100;

int dosPorUno(List<int> precios) {
  final orden = [...precios]..sort((a, b) => b.compareTo(a)); // de más caro a más barato
  var total = 0;
  for (var i = 0; i < orden.length; i += 2) {
    total += orden[i]; // de cada pareja se paga la más cara
  }
  return total;
}

class Caja {
  final Descuento descuento;
  Caja(this.descuento);

  int cobrar(List<int> precios) => descuento(precios);
}

void main() {
  final precios = [300, 450, 150, 100];
  print('sin descuento: ${Caja(sinDescuento).cobrar(precios)}');
  print('diez por ciento: ${Caja(diezPorCiento).cobrar(precios)}');
  print('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; suscribir devuelve otra función para darse de baja. En Flutter este patrón está en el propio SDK: ChangeNotifier y ValueNotifier avisan a los widgets que los escuchan, y los Stream de Dart son observadores a lo grande.

// Observador: un objeto avisa a todos los que se han suscrito cuando ocurre algo, sin saber quiénes son.
class Cafetera {
  final List<void Function(String)> _oyentes = [];

  // suscribir devuelve una función para darse de baja
  void Function() suscribir(void Function(String) oyente) {
    _oyentes.add(oyente);
    return () => _oyentes.remove(oyente);
  }

  void preparar() {
    for (final oyente in List.of(_oyentes)) {
      // se recorre una copia por si alguien se da de baja mientras se avisa
      oyente('café listo');
    }
  }
}

void main() {
  final cafetera = Cafetera();
  cafetera.suscribir((mensaje) => print('pantalla: $mensaje'));
  final bajaMovil = cafetera.suscribir((mensaje) => print('móvil: $mensaje'));

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