1. Conociendo el patrón «Observador»
El patrón «Observador» (Observer) es uno de los patrones de diseño más conocidos y fundamentales. Describe una situación en la que un objeto (observado, o subject) informa de sus cambios a otros objetos (observadores, observers) que se han suscrito a dichos cambios.
En pocas palabras: tenemos un canal de Telegram (objeto observado) y unos «suscriptores» (observadores). Cada vez que sale una nueva publicación, el canal notifica a todos los suscriptores, y estos ya deciden qué hacer: leer, ignorar o darse de baja.
En programación, este patrón permite notificar automáticamente a los objetos interesados sobre eventos o cambios de estado sin acoplarlos directamente entre sí. Esto es importante para construir sistemas flexibles, ampliables y fáciles de mantener.
¿Dónde aparece el patrón «Observador»?
- En interfaces gráficas (Swing, AWT, JavaFX): escuchas de eventos.
- En bibliotecas reactivas (RxJava, Project Reactor).
- En la lógica de negocio: reaccionar a cambios en el estado del modelo.
- En motores de juegos (eventos de colisiones, victoria, derrota, etc.).
- En cualquier lugar donde sea necesario separar «qué ha ocurrido» de «qué hacer al respecto».
Relación del patrón con los eventos y los listeners en Java
De hecho, todo el modelo de eventos de Java se basa en el «Observador». Cuando escribes button.addActionListener(listener);, estás aplicando este patrón:
- Observado: el botón (u otro componente).
- Observador: tu listener que implementa el método actionPerformed().
- Evento: el usuario hizo clic, pasó el ratón, etc.
- Notificación: el componente invoca actionPerformed().
¡Todo esto es una implementación clásica de Observer!
2. Implementación clásica del patrón «Observador»
Veamos cómo implementar el patrón en nuestras propias clases — sin Swing ni AWT, para ver que no hay magia.
Elementos básicos del patrón
- Observable (Subject) — el objeto observado. Mantiene la lista de observadores y los notifica de los cambios.
- Observer — la interfaz de observador, normalmente con el método update().
Ejemplo: termómetro y aire acondicionado
Interfaz del observador
public interface TemperatureObserver {
void temperatureChanged(int newTemperature);
}
Clase «Termómetro» (observado)
import java.util.*;
public class Thermometer {
private int temperature;
private final List<TemperatureObserver> observers = new ArrayList<>();
public void addObserver(TemperatureObserver observer) {
observers.add(observer);
}
public void removeObserver(TemperatureObserver observer) {
observers.remove(observer);
}
public void setTemperature(int newTemperature) {
if (this.temperature != newTemperature) {
this.temperature = newTemperature;
notifyObservers();
}
}
private void notifyObservers() {
for (TemperatureObserver observer : observers) {
observer.temperatureChanged(temperature);
}
}
}
Ejemplo de observador: «Aire acondicionado»
public class AirConditioner implements TemperatureObserver {
@Override
public void temperatureChanged(int newTemperature) {
if (newTemperature > 25) {
System.out.println("¡Aire acondicionado encendido! Hace calor: " + newTemperature + "°C");
} else {
System.out.println("Aire acondicionado apagado. Temperatura: " + newTemperature + "°C");
}
}
}
Uso
public class Main {
public static void main(String[] args) {
Thermometer thermometer = new Thermometer();
AirConditioner conditioner = new AirConditioner();
thermometer.addObserver(conditioner);
thermometer.setTemperature(22); // Aire acondicionado apagado. Temperatura: 22°C
thermometer.setTemperature(28); // ¡Aire acondicionado encendido! Hace calor: 28°C
}
}
¡Eso es toda la magia! Puedes añadir cien observadores más si quieres: todos recibirán notificaciones cuando cambie la temperatura.
Esquema gráfico del patrón
flowchart LR
T["Termómetro (Observable)"] -- notifica --> AC["Aire acondicionado (Observer)"]
T -- notifica --> L["Logger (Observer)"]
T -- notifica --> Alarm["Alarma (Observer)"]
Detalles modernos: Observable obsoleto y nuevos enfoques
En la biblioteca estándar de Java existían java.util.Observable y java.util.Observer, pero desde Java 9 están marcados como obsoletos (deprecated). La razón es la falta de flexibilidad (por ejemplo, Observable es una clase y no una interfaz, lo que dificulta heredar de otra clase).
El enfoque moderno es diseñar tus propias interfaces de listeners y la lógica de suscripción/cancelación (como en el ejemplo anterior). Es más flexible, seguro y se ajusta mejor a las necesidades reales.
3. Ejemplo: miniaplicación con suscriptores
Hagamos un «contador de pulsaciones» con posibilidad de suscribirse a los cambios de su valor.
Interfaz del listener
public interface CounterListener {
void counterChanged(int newValue);
}
Clase‑contador
import java.util.*;
public class Counter {
private int value = 0;
private final List<CounterListener> listeners = new ArrayList<>();
public void addCounterListener(CounterListener l) {
listeners.add(l);
}
public void removeCounterListener(CounterListener l) {
listeners.remove(l);
}
public void increment() {
value++;
notifyListeners();
}
private void notifyListeners() {
for (CounterListener l : listeners) {
l.counterChanged(value);
}
}
public int getValue() {
return value;
}
}
Listener: mostramos un mensaje
public class ConsoleCounterListener implements CounterListener {
@Override
public void counterChanged(int newValue) {
System.out.println("El contador ha cambiado: " + newValue);
}
}
Uso
public class Main {
public static void main(String[] args) {
Counter counter = new Counter();
counter.addCounterListener(new ConsoleCounterListener());
counter.increment(); // El contador ha cambiado: 1
counter.increment(); // El contador ha cambiado: 2
}
}
4. Matices útiles
Alternativas y ampliaciones modernas
En proyectos reales se suelen utilizar clases anónimas o expresiones lambda para suscribirse: counter.addCounterListener(newValue -> System.out.println("Nuevo valor: " + newValue));
(Para poder hacerlo, la interfaz debe ser funcional — tener un único método abstracto.)
También son populares las bibliotecas reactivas (RxJava, Project Reactor), donde el «Observador» se implementa con soporte para flujos de eventos, filtrado, asincronía, etc. Para entender la idea basta con el esquema clásico visto más arriba.
Aplicación del patrón «Observador» en la práctica
- Modelos de datos. Un cambio en el modelo (lista de tareas, productos, usuarios) notifica a las vistas para actualizarse.
- Registro (logging). Un suscriptor «logger» reacciona a eventos por todo el sistema.
- Notificaciones. Ante un cambio de estado: envío de email, notificaciones push, mensajes en Telegram.
- Juegos. Cambio de salud, aparición de un enemigo, fin de nivel.
- Multihilo. Un hilo publica eventos, otros reaccionan.
5. Errores típicos al implementar el patrón «Observador»
Error n.º 1: Olvidaste eliminar el listener. Si un listener ya no es necesario pero no se eliminó, seguirá recibiendo notificaciones. En aplicaciones de larga vida esto puede provocar fugas de memoria.
Error n.º 2: Operaciones largas o bloqueantes en los manejadores. Si el manejador realiza trabajo pesado (E/S, BD), la aplicación puede «colgarse», especialmente si las notificaciones llegan desde el hilo de UI. Traslada las tareas pesadas a hilos en segundo plano.
Error n.º 3: Excepciones en los listeners. Una excepción en un listener puede interrumpir el envío al resto. Envuelve las llamadas a los listeners en try-catch y registra los errores.
Error n.º 4: Registro múltiple del mismo listener. Si el mismo listener se añade varias veces, recibirá el evento tantas veces como se haya añadido. Vigila el registro y aplica protección contra duplicados.
Error n.º 5: Acoplamiento fuerte entre observado y observador. Si el observado conoce implementaciones concretas de los observadores, se rompe el bajo acoplamiento. Usa solo interfaces (por ejemplo, TemperatureObserver, CounterListener).
GO TO FULL VERSION