Apuntes DAM
Volver al inicio

Patrón Observer

Patrones de diseñoDe comportamientoNivel intermedioTambién: observador, publicador-suscriptor, listener

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

  1. Observador. La interfaz con el método que el sujeto llama al cambiar (actualizar).
  2. Sujeto. Guarda la lista de observadores y ofrece suscribir y desuscribir. Al cambiar, notificar recorre la lista y avisa a cada uno.
  3. Observadores concretos. Cada clase interesada implementa la interfaz y decide qué hacer con el aviso (mostrar, comprobar un umbral, guardar).
  4. Suscripción. Alguien (normalmente el main o la configuración) conecta los observadores con el sujeto, y los puede quitar cuando ya no interesan.

Diagrama de clases

0..*observadores«interface»Observador+actualizar(valor : double) : voidSensor-observadores : List<Observador>+suscribir(o : Observador) : void+desuscribir(o : Observador) : void+nuevaLectura(valor : double) : void-notificar(valor : double) : voidPantalla+actualizar(valor : double) : voidAlarma-umbral : double+actualizar(valor : double) : void
Arrastra las clases para colocarlas a tu gusto.
Ver el diagrama en PlantUML
text
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@enduml

Puedes 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.

Java
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}

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: addActionListener en Swing, setOnAction en JavaFX, setOnClickListener en Android, addEventListener en el navegador.
  • PropertyChangeSupport y PropertyChangeListener en los JavaBeans; las propiedades observables de JavaFX.
  • En Android, LiveData y Flow avisan a la interfaz cuando cambian los datos del ViewModel.
  • 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 usar CopyOnWriteArrayList.
  • 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 nombre y lectura 25.5.
  • suscribir: Suscrito: nombre o Ya estaba suscrito: nombre. desuscribir: Baja: nombre o No estaba suscrito: nombre.
  • La alarma: ¡Alarma! 31,0 °C supera 30,0 °C y Alarma desactivada: 28,0 °C. Al final se escriben las estadísticas.
☕JavaUn sensor de temperatura con observadoresMedio

Ejemplo

Entrada (lo que se escribe por teclado)
suscribir pantalla
suscribir alarma 30
suscribir estadisticas
lectura 24.5
lectura 30
lectura 31.2
lectura 33
lectura 29.5
desuscribir pantalla
lectura 31
Salida esperada
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
⏳
Test oculto #3
⏳
Test oculto #4
0/4 tests pasados · pulsa un test para ver su entrada y su salida esperada
Ver la solución explicada
java
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 precio y precio 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 €), … o ninguna.
☕JavaÓrdenes de bolsa que se desuscriben solasDifícil

Ejemplo

Entrada (lo que se escribe por teclado)
grafico
venta ana 105
compra luis 95
precio 100
precio 106
precio 94
precio 97
Salida esperada
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
⏳
Test oculto #3
⏳
Test oculto #4
0/4 tests pasados · pulsa un test para ver su entrada y su salida esperada
Ver la solución explicada
java
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 aciertos

Elige 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. 1.¿Qué conoce el sujeto de sus observadores en el patrón Observer?

  2. 2.Un observador se desuscribe dentro de actualizar mientras el sujeto recorre su ArrayList con for-each. ¿Qué pasa?

  3. 3.¿Por qué hay que desuscribir los observadores que ya no se usan?

  4. 4.¿Cuál de estos es un ejemplo del patrón Observer?

  5. 5.¿Qué relación hay entre MVC y Observer?

Relacionado