Patrón Observer
Un objeto (el sujeto) guarda una lista de interesados y les avisa automáticamente cuando cambia, sin saber quiénes son: los eventos de un botón, las notificaciones o un modelo que avisa a sus vistas.
Visualízalo paso a paso
Cambia los datos, dale a reproducir y sigue cada paso en el dibujo, en la línea de Java que se ejecuta y en sus variables.
Observer
Escribe las órdenes, una por línea o separadas por comas: suscribir/desuscribir pantalla, alarma o registro, y lectura con la temperatura.
Paso 1
Pantalla se suscribe: el sensor lo añade a su lista sin saber qué hará con los avisos.
1class Sensor {
2 private final List<Observador> observadores = new ArrayList<>();
3
4 void suscribir(Observador o) {
5 observadores.add(o); // observadores = pantalla, t = —
6 }
7
8 void desuscribir(Observador o) {
9 observadores.remove(o);
10 }
11
12 void nuevaLectura(double t) {
13 for (Observador o : observadores)
14 o.actualizar(t);
15 }
16}Variables
- observadores
- pantalla
- t
- —
Atajos con el foco dentro del visualizador: ← → paso a paso, Espacio reproducir o pausar, Inicio/Fin ir al principio o al final.
La idea
A menudo, cuando algo cambia, varias partes del programa tienen que enterarse: si cambia la temperatura, la pantalla la muestra, la alarma la vigila y el registro la guarda. Si el sensor llamara directamente a cada una, conocería a todas, y añadir una nueva obligaría a tocar el sensor.
El patrón Observer invierte la relación: los interesados (observadores) se suscriben al objeto que cambia (el sujeto), y el sujeto solo sabe que tiene una lista de observadores que implementan una interfaz con un método actualizar. Cuando cambia, recorre la lista y llama a ese método en cada uno. Cada observador decide qué hacer.
Así, el sujeto y los observadores quedan desacoplados: se pueden añadir, quitar o cambiar observadores en tiempo de ejecución sin tocar el sujeto. Es la base de toda la programación con eventos: los listeners de Swing, JavaFX y Android, los eventos del DOM y los sistemas de mensajería publicador-suscriptor.
Cuándo usarlo
- Un cambio en un objeto tiene que provocar reacciones en otros, y no se sabe de antemano cuántos ni cuáles.
- Interfaces gráficas: un botón avisa a sus listeners al pulsarlo; un modelo avisa a sus vistas al cambiar (MVC).
- Se quiere que quien produce los datos no dependa de quien los consume.
Cuándo no
- Si solo hay un interesado fijo: una llamada directa es más clara.
- Si el orden de las reacciones importa mucho o hay cadenas de avisos que provocan otros avisos: el flujo se vuelve difícil de seguir y de depurar.
Participantes
- Observador. La interfaz con el método que el sujeto llama al cambiar (
actualizar). - Sujeto. Guarda la lista de observadores y ofrece
suscribirydesuscribir. Al cambiar,notificarrecorre la lista y avisa a cada uno. - Observadores concretos. Cada clase interesada implementa la interfaz y decide qué hacer con el aviso (mostrar, comprobar un umbral, guardar).
- Suscripción. Alguien (normalmente el
maino la configuración) conecta los observadores con el sujeto, y los puede quitar cuando ya no interesan.
Diagrama de clases
Ver el diagrama en PlantUML
1@startuml
2interface Observador {
3 +actualizar(valor : double) : void
4}
5class Sensor {
6 -observadores : List<Observador>
7 +suscribir(o : Observador) : void
8 +desuscribir(o : Observador) : void
9 +nuevaLectura(valor : double) : void
10 -notificar(valor : double) : void
11}
12class Pantalla {
13 +actualizar(valor : double) : void
14}
15class Alarma {
16 -umbral : double
17 +actualizar(valor : double) : void
18}
19Observador <|.. Pantalla
20Observador <|.. Alarma
21Sensor o-- "0..*" Observador : observadores
22@endumlPuedes copiarlo en el editor de diagramas UML y modificarlo.
El código
Un sensor con observadores
El sensor no conoce ninguna clase concreta: solo la interfaz Observador. Al ser funcional, un observador puede ser una lambda.
1import java.util.*;
2
3interface Observador {
4 void actualizar(double valor);
5}
6
7/** El sujeto: guarda a sus observadores y les avisa de cada cambio, sin saber qué hacen. */
8class Sensor {
9 private final List<Observador> observadores = new ArrayList<>();
10
11 void suscribir(Observador o) { observadores.add(o); }
12 void desuscribir(Observador o) { observadores.remove(o); }
13
14 void nuevaLectura(double valor) {
15 for (Observador o : observadores) o.actualizar(valor); // notificar a todos
16 }
17}
18
19class Pantalla implements Observador {
20 public void actualizar(double valor) { System.out.println("Pantalla: " + valor + " °C"); }
21}
22
23class Alarma implements Observador {
24 private final double umbral;
25 Alarma(double umbral) { this.umbral = umbral; }
26 public void actualizar(double valor) {
27 if (valor > umbral) System.out.println("¡Alarma! " + valor + " °C");
28 }
29}
30
31public class Main {
32 public static void main(String[] args) {
33 Sensor sensor = new Sensor();
34 Pantalla pantalla = new Pantalla();
35 sensor.suscribir(pantalla);
36 sensor.suscribir(new Alarma(30));
37 sensor.suscribir(v -> System.out.println("Registro: guardada la lectura " + v)); // también una lambda
38 sensor.nuevaLectura(24.5);
39 sensor.desuscribir(pantalla);
40 sensor.nuevaLectura(31.0);
41 }
42}from abc import ABC, abstractmethod
class Observador(ABC):
@abstractmethod
def actualizar(self, valor): ...
class Sensor:
"""El sujeto: guarda a sus observadores y les avisa de cada cambio, sin saber qué hacen."""
def __init__(self):
self._observadores = []
def suscribir(self, o):
self._observadores.append(o)
def desuscribir(self, o):
self._observadores.remove(o)
def nueva_lectura(self, valor):
for o in self._observadores: # notificar a todos
o.actualizar(valor)
class Pantalla(Observador):
def actualizar(self, valor):
print(f"Pantalla: {valor} °C")
class Alarma(Observador):
def __init__(self, umbral):
self.umbral = umbral
def actualizar(self, valor):
if valor > self.umbral:
print(f"¡Alarma! {valor} °C")
class Registro(Observador):
def actualizar(self, valor):
print(f"Registro: guardada la lectura {valor}")
sensor = Sensor()
pantalla = Pantalla()
sensor.suscribir(pantalla)
sensor.suscribir(Alarma(30))
sensor.suscribir(Registro())
sensor.nueva_lectura(24.5)
sensor.desuscribir(pantalla)
sensor.nueva_lectura(31.0)/** El sujeto: guarda a sus observadores y les avisa de cada cambio, sin saber qué hacen. */
class Sensor {
#observadores = [];
suscribir(o) { this.#observadores.push(o); }
desuscribir(o) { this.#observadores = this.#observadores.filter((x) => x !== o); }
nuevaLectura(valor) {
for (const o of this.#observadores) o.actualizar(valor); // notificar a todos
}
}
class Pantalla {
actualizar(valor) { console.log(`Pantalla: ${valor.toFixed(1)} °C`); }
}
class Alarma {
constructor(umbral) { this.umbral = umbral; }
actualizar(valor) {
if (valor > this.umbral) console.log(`¡Alarma! ${valor.toFixed(1)} °C`);
}
}
const sensor = new Sensor();
const pantalla = new Pantalla();
sensor.suscribir(pantalla);
sensor.suscribir(new Alarma(30));
sensor.suscribir({ actualizar: (v) => console.log(`Registro: guardada la lectura ${v.toFixed(1)}`) }); // también un objeto suelto
sensor.nuevaLectura(24.5);
sensor.desuscribir(pantalla);
sensor.nuevaLectura(31.0);using System;
using System.Collections.Generic;
using System.Globalization;
interface IObservador {
void Actualizar(double valor);
}
// El sujeto: guarda a sus observadores y les avisa de cada cambio, sin saber qué hacen.
class Sensor {
private readonly List<IObservador> observadores = new();
public void Suscribir(IObservador o) => observadores.Add(o);
public void Desuscribir(IObservador o) => observadores.Remove(o);
public void NuevaLectura(double valor) {
foreach (var o in observadores) o.Actualizar(valor); // notificar a todos
}
}
static class Formato {
public static string Grados(double v) => v.ToString("0.0", CultureInfo.InvariantCulture);
}
class Pantalla : IObservador {
public void Actualizar(double valor) => Console.WriteLine(quot;Pantalla: {Formato.Grados(valor)} °C");
}
class Alarma : IObservador {
private readonly double umbral;
public Alarma(double umbral) { this.umbral = umbral; }
public void Actualizar(double valor) {
if (valor > umbral) Console.WriteLine(quot;¡Alarma! {Formato.Grados(valor)} °C");
}
}
class Registro : IObservador {
public void Actualizar(double valor) => Console.WriteLine(quot;Registro: guardada la lectura {Formato.Grados(valor)}");
}
class Program {
static void Main() {
var sensor = new Sensor();
var pantalla = new Pantalla();
sensor.Suscribir(pantalla);
sensor.Suscribir(new Alarma(30));
sensor.Suscribir(new Registro());
sensor.NuevaLectura(24.5);
sensor.Desuscribir(pantalla);
sensor.NuevaLectura(31.0);
}
}<?php
interface Observador {
public function actualizar(float $valor): void;
}
/** El sujeto: guarda a sus observadores y les avisa de cada cambio, sin saber qué hacen. */
class Sensor {
private array $observadores = [];
public function suscribir(Observador $o): void { $this->observadores[] = $o; }
public function desuscribir(Observador $o): void {
$this->observadores = array_values(array_filter($this->observadores, fn($x) => $x !== $o));
}
public function nuevaLectura(float $valor): void {
foreach ($this->observadores as $o) $o->actualizar($valor); // notificar a todos
}
}
function grados(float $v): string { return number_format($v, 1, ".", ""); }
class Pantalla implements Observador {
public function actualizar(float $valor): void { echo "Pantalla: " . grados($valor) . " °C\n"; }
}
class Alarma implements Observador {
public function __construct(private float $umbral) { }
public function actualizar(float $valor): void {
if ($valor > $this->umbral) echo "¡Alarma! " . grados($valor) . " °C\n";
}
}
$sensor = new Sensor();
$pantalla = new Pantalla();
$sensor->suscribir($pantalla);
$sensor->suscribir(new Alarma(30));
$sensor->suscribir(new class implements Observador { // también una clase anónima
public function actualizar(float $valor): void { echo "Registro: guardada la lectura " . grados($valor) . "\n"; }
});
$sensor->nuevaLectura(24.5);
$sensor->desuscribir($pantalla);
$sensor->nuevaLectura(31.0);Salida al ejecutarlo (la misma en los 5 lenguajes)
Pantalla: 24.5 °C Registro: guardada la lectura 24.5 ¡Alarma! 31.0 °C Registro: guardada la lectura 31.0
En la práctica
- Los listeners de las interfaces gráficas:
addActionListeneren Swing,setOnActionen JavaFX,setOnClickListeneren Android,addEventListeneren el navegador. PropertyChangeSupportyPropertyChangeListeneren los JavaBeans; las propiedades observables de JavaFX.- En Android,
LiveDatayFlowavisan a la interfaz cuando cambian los datos delViewModel. - A gran escala, los sistemas publicador-suscriptor (Kafka, RabbitMQ, los webhooks) aplican la misma idea entre programas distintos.
Errores típicos
- Olvidar desuscribir los observadores que ya no se usan: el sujeto los sigue referenciando y no se liberan (una fuga de memoria clásica en Android y Swing).
- Modificar la lista mientras se recorre (un observador que se desuscribe al recibir el aviso):
ConcurrentModificationException. Hay que recorrer una copia o usarCopyOnWriteArrayList. - Depender del orden de notificación sin documentarlo.
- Avisos en cascada: un observador que cambia el sujeto, que vuelve a avisar… hasta un bucle infinito.
- Observadores lentos que bloquean al sujeto: si la reacción es costosa, conviene hacerla en otro hilo.
Ejercicios
Cada ejercicio se corrige solo con sus pruebas (algunas ocultas). Escribe tu solución en el editor y pulsa Ejecutar o Comprobar; la solución explicada está debajo, por si te atascas.
1. Un sensor de temperatura con observadores
Programa el sujeto Sensor (con sus observadores identificados por nombre y avisados en el orden en que se suscribieron) y el observador Alarma. La pantalla y las estadísticas ya están escritas. La alarma avisa una vez cuando la temperatura pasa de su umbral y otra vez cuando vuelve a estar por debajo o igual; mientras sigue por encima no repite el aviso.
- Órdenes:
suscribir pantalla,suscribir estadisticas,suscribir alarma 30,desuscribir nombreylectura 25.5. suscribir:Suscrito: nombreoYa estaba suscrito: nombre.desuscribir:Baja: nombreoNo estaba suscrito: nombre.- La alarma:
¡Alarma! 31,0 °C supera 30,0 °CyAlarma desactivada: 28,0 °C. Al final se escriben las estadísticas.
Ejemplo
suscribir pantalla suscribir alarma 30 suscribir estadisticas lectura 24.5 lectura 30 lectura 31.2 lectura 33 lectura 29.5 desuscribir pantalla lectura 31
Suscrito: pantalla Suscrito: alarma Suscrito: estadisticas Pantalla: 24,5 °C Pantalla: 30,0 °C Pantalla: 31,2 °C ¡Alarma! 31,2 °C supera 30,0 °C Pantalla: 33,0 °C Pantalla: 29,5 °C Alarma desactivada: 29,5 °C Baja: pantalla ¡Alarma! 31,0 °C supera 30,0 °C Estadísticas: 6 lecturas · mínima 24,5 °C · máxima 33,0 °C · media 29,9 °C
Ver la solución explicada
1import java.util.*;
2
3interface Observador {
4 void actualizar(double t);
5}
6
7class Pantalla implements Observador {
8 public void actualizar(double t) { System.out.println("Pantalla: " + Main.grados(t)); }
9}
10
11class Estadisticas implements Observador {
12 int n;
13 double min = Double.MAX_VALUE, max = -Double.MAX_VALUE, suma;
14 public void actualizar(double t) {
15 n++;
16 suma += t;
17 min = Math.min(min, t);
18 max = Math.max(max, t);
19 }
20 String resumen() {
21 return n == 0 ? "Estadísticas: sin lecturas" : "Estadísticas: " + n + " lecturas · mínima " + Main.grados(min) + " · máxima " + Main.grados(max) + " · media " + Main.grados(suma / n);
22 }
23}
24
25/** Avisa al cruzar el umbral hacia arriba y otra vez al volver por debajo (o a él). */
26class Alarma implements Observador {
27 private final double umbral;
28 private boolean activa = false;
29
30 Alarma(double umbral) { this.umbral = umbral; }
31
32 public void actualizar(double t) {
33 if (!activa && t > umbral) {
34 activa = true;
35 System.out.println("¡Alarma! " + Main.grados(t) + " supera " + Main.grados(umbral));
36 } else if (activa && t <= umbral) {
37 activa = false;
38 System.out.println("Alarma desactivada: " + Main.grados(t));
39 }
40 }
41}
42
43/** El sujeto: los observadores, por nombre y en orden de suscripción. */
44class Sensor {
45 private final Map<String, Observador> observadores = new LinkedHashMap<>();
46
47 String suscribir(String nombre, Observador o) {
48 if (observadores.containsKey(nombre)) return "Ya estaba suscrito: " + nombre;
49 observadores.put(nombre, o);
50 return "Suscrito: " + nombre;
51 }
52
53 String desuscribir(String nombre) {
54 return observadores.remove(nombre) == null ? "No estaba suscrito: " + nombre : "Baja: " + nombre;
55 }
56
57 void nuevaLectura(double t) {
58 for (Observador o : observadores.values()) o.actualizar(t);
59 }
60}
61
62public class Main {
63 static String grados(double t) {
64 return String.format(Locale.ROOT, "%.1f", t).replace('.', ',') + " °C";
65 }
66
67 public static void main(String[] args) {
68 Scanner sc = new Scanner(System.in);
69 Sensor sensor = new Sensor();
70 Estadisticas estadisticas = new Estadisticas();
71 while (sc.hasNextLine()) {
72 String linea = sc.nextLine().trim();
73 if (linea.isEmpty()) continue;
74 String[] p = linea.split("\\s+");
75 try {
76 switch (p[0]) {
77 case "suscribir" -> {
78 Observador o = switch (p[1]) {
79 case "pantalla" -> new Pantalla();
80 case "estadisticas" -> estadisticas;
81 case "alarma" -> new Alarma(Double.parseDouble(p[2]));
82 default -> throw new IllegalArgumentException();
83 };
84 System.out.println(sensor.suscribir(p[1], o));
85 }
86 case "desuscribir" -> System.out.println(sensor.desuscribir(p[1]));
87 case "lectura" -> sensor.nuevaLectura(Double.parseDouble(p[1]));
88 default -> throw new IllegalArgumentException();
89 }
90 } catch (RuntimeException e) {
91 System.out.println("Orden no válida: " + linea);
92 }
93 }
94 System.out.println(estadisticas.resumen());
95 }
96}El sensor no sabe qué hace cada observador: solo los avisa. Por eso la misma lectura la muestra la pantalla, la vigila la alarma y la cuentan las estadísticas sin que el sensor cambie.
La alarma tiene su propio estado (activa o no): un observador puede ser tan sencillo como una lambda o tan elaborado como haga falta.
2. Órdenes de bolsa que se desuscriben solas
Las órdenes de compra y venta observan el precio de una acción y, en cuanto se cumplen, se ejecutan y se desuscriben ellas mismas. El programa ya funciona… hasta que una orden se ejecuta: entonces se desuscribe mientras el sujeto recorre la lista y Java lanza ConcurrentModificationException. Arregla nuevoPrecio para que avise a todos los que estaban suscritos al empezar la notificación, aunque alguno se desuscriba durante ella.
- Órdenes:
grafico,venta quien precio,compra quien precioyprecio valor, que avisa a los observadores en el orden en que se suscribieron. - El gráfico escribe
Gráfico ACME: ↑ 101,00 €(·la primera vez,↑,↓o=); una orden que se cumple,Orden ejecutada: ana vende ACME a 106,00 €(venta si el precio es mayor o igual que su objetivo; compra si es menor o igual). - Al final:
Órdenes pendientes: luis (compra a 95,00 €), …oninguna.
Ejemplo
grafico venta ana 105 compra luis 95 precio 100 precio 106 precio 94 precio 97
Gráfico ACME: · 100,00 € Gráfico ACME: ↑ 106,00 € Orden ejecutada: ana vende ACME a 106,00 € Gráfico ACME: ↓ 94,00 € Orden ejecutada: luis compra ACME a 94,00 € Gráfico ACME: ↑ 97,00 € Órdenes pendientes: ninguna
Ver la solución explicada
1import java.util.*;
2
3interface Observador {
4 void actualizar(Accion a, double precio);
5}
6
7/** El sujeto: el precio de una acción. */
8class Accion {
9 final String nombre;
10 private final List<Observador> observadores = new ArrayList<>();
11
12 Accion(String nombre) { this.nombre = nombre; }
13
14 void suscribir(Observador o) { observadores.add(o); }
15 void desuscribir(Observador o) { observadores.remove(o); }
16 List<Observador> pendientes() { return observadores; }
17
18 void nuevoPrecio(double precio) {
19 // Se recorre una copia: un observador puede desuscribirse mientras se notifica
20 for (Observador o : new ArrayList<>(observadores)) o.actualizar(this, precio);
21 }
22}
23
24class Grafico implements Observador {
25 private double anterior = Double.NaN;
26 public void actualizar(Accion a, double precio) {
27 String flecha = Double.isNaN(anterior) ? "·" : precio > anterior ? "↑" : precio < anterior ? "↓" : "=";
28 System.out.println("Gráfico " + a.nombre + ": " + flecha + " " + Main.euros(precio));
29 anterior = precio;
30 }
31 public String toString() { return "gráfico"; }
32}
33
34/** Una orden de un solo uso: se ejecuta al llegar a su precio y se desuscribe sola. */
35class Orden implements Observador {
36 final String quien;
37 final boolean venta;
38 final double objetivo;
39
40 Orden(String quien, boolean venta, double objetivo) {
41 this.quien = quien;
42 this.venta = venta;
43 this.objetivo = objetivo;
44 }
45
46 public void actualizar(Accion a, double precio) {
47 if (venta ? precio >= objetivo : precio <= objetivo) {
48 System.out.println("Orden ejecutada: " + quien + (venta ? " vende " : " compra ") + a.nombre + " a " + Main.euros(precio));
49 a.desuscribir(this);
50 }
51 }
52
53 public String toString() { return quien + " (" + (venta ? "venta" : "compra") + " a " + Main.euros(objetivo) + ")"; }
54}
55
56public class Main {
57 static String euros(double x) {
58 return String.format(Locale.ROOT, "%.2f", x).replace('.', ',') + " €";
59 }
60
61 public static void main(String[] args) {
62 Scanner sc = new Scanner(System.in);
63 Accion accion = new Accion("ACME");
64 while (sc.hasNextLine()) {
65 String linea = sc.nextLine().trim();
66 if (linea.isEmpty()) continue;
67 String[] p = linea.split("\\s+");
68 try {
69 switch (p[0]) {
70 case "grafico" -> accion.suscribir(new Grafico());
71 case "venta", "compra" -> accion.suscribir(new Orden(p[1], p[0].equals("venta"), Double.parseDouble(p[2])));
72 case "precio" -> accion.nuevoPrecio(Double.parseDouble(p[1]));
73 default -> throw new IllegalArgumentException();
74 }
75 } catch (ConcurrentModificationException e) {
76 System.out.println("ERROR: la lista de observadores ha cambiado mientras se notificaba");
77 return;
78 } catch (RuntimeException e) {
79 System.out.println("Orden no válida: " + linea);
80 }
81 }
82 List<String> quedan = new ArrayList<>();
83 for (Observador o : accion.pendientes()) if (o instanceof Orden) quedan.add(o.toString());
84 System.out.println("Órdenes pendientes: " + (quedan.isEmpty() ? "ninguna" : String.join(", ", quedan)));
85 }
86}El iterador de un ArrayList detecta que la lista ha cambiado desde que empezó y falla en lugar de saltarse o repetir elementos. Recorrer una copia fija la lista de destinatarios al empezar la notificación.
Es el fallo más típico del patrón Observer: que un observador se desuscriba (o suscriba a otro) dentro de su propio actualizar. Las bibliotecas de eventos lo resuelven igual, con una copia o con listas preparadas para ello.
Test
Test: Observer
0/5 respondidas · 0 aciertosElige una respuesta en cada pregunta: verás al momento si es correcta y por qué. Con un 80 % de aciertos se da por superada.
1.¿Qué conoce el sujeto de sus observadores en el patrón Observer?
2.Un observador se desuscribe dentro de
actualizarmientras el sujeto recorre suArrayListcon for-each. ¿Qué pasa?3.¿Por qué hay que desuscribir los observadores que ya no se usan?
4.¿Cuál de estos es un ejemplo del patrón Observer?
5.¿Qué relación hay entre MVC y Observer?