Category: Behavioral
El problema
El cambio de estado de un objeto necesita reflejarse en varios otros, pero esos otros no deberían estar cableados directamente al objeto que cambió. Llamar a cada dependiente directamente desde dentro del sujeto lo acopla al tipo concreto de cada consumidor, y agregar un nuevo consumidor significa editar el código del sujeto de nuevo. Lo que se necesita es una forma de que las partes interesadas se registren y sean notificadas, sin que el sujeto sepa nada de ellas más allá de una interfaz común.
La solución
El sujeto mantiene una lista de observadores detrás de una interfaz común y notifica a todos ellos cada vez que su estado cambia; cada observador decide independientemente qué hacer con esa notificación. Suscribirse y cancelar la suscripción no requieren tocar la lógica del propio sujeto.
classDiagram
class Subject {
-observers
+subscribe(o)
+unsubscribe(o)
+notifyObservers()
}
class Observer {
<<interface>>
+update(state)
}
class ConcreteObserverA
class ConcreteObserverB
Subject o-- Observer
Observer <|.. ConcreteObserverA
Observer <|.. ConcreteObserverB
Ejemplo clásico
classic/WeatherStation
es el ejemplo canónico: un sujeto que envía lecturas de temperature/humidity a cada
WeatherObserver
suscrito. CurrentConditionsDisplay
solo almacena la lectura más reciente; HeatAlertObserver
deriva un booleano de alerta a partir de ella — dos observadores haciendo cosas genuinamente
distintas con la misma notificación exacta, ninguno consciente de la existencia del otro.
WeatherStationTest
cubre a los dos observadores reaccionando independientemente a una medición, un observador con
suscripción cancelada que ya no recibe actualizaciones, y la alerta de calor limpiándose cuando
la temperatura vuelve a bajar.
Ejemplo aplicado: fan-out de estado de transacción
applied/TransactionStatusPublisher
notifica a tres observadores independientes cada vez que cambia el estado de una transacción:
WebhookNotifierObserver
(registra una llamada de webhook saliente), AuditLogObserver
(registra cada transición para compliance), y PushNotificationObserver
(solo reacciona a los estados terminales, SETTLED/FAILED — un cliente no necesita un push
por cada estado intermedio). Esta es exactamente la forma que necesita un gateway de pagos real
cuando el ciclo de vida de una transacción tiene que llegar a varios sistemas independientes: el
publisher no sabe ni le importa cuántos consumidores existen, ni qué hace cada uno realmente con
la notificación.
TransactionStatusPublisherTest
cubre a los tres observadores reaccionando a una secuencia completa PENDING→PROCESSING→SETTLED,
al observador de push disparando también en FAILED, y a un observador con suscripción cancelada
que ya no recibe actualizaciones.
Cuándo no usarlo
- Si existe exactamente un consumidor y nunca va a ser más de uno, una llamada directa al método es más simple y más fácil de seguir que un mecanismo de suscripción construido para un caso que todavía no existe.
- Observadores que deben ejecutarse en un orden específico, o cuya falla debería detener a los demás, no encajan bien en este patrón — el Observer puro no da ninguna garantía de orden ni de aislamiento de errores. Eso necesita un pipeline explícito en su lugar.
- Cuidado con observadores que silenciosamente mantienen viva una referencia a un sujeto por más tiempo del previsto (una forma clásica de fuga de memoria en sujetos de vida larga con observadores de vida corta) — un observador que terminó necesita cancelar su suscripción, no solo salir de alcance.
Cobertura de pruebas
100% de cobertura de instrucciones, 100% de cobertura de ramas (JaCoCo). Reprodúzcalo usted mismo:
./gradlew :behavioral:observer:jacocoTestReport
Informe en behavioral/observer/build/reports/jacoco/test/html/index.html.
Lecturas adicionales
- Gamma, E., Helm, R., Johnson, R., & Vlissides, J. (1994). Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley. — el Capítulo 5 formaliza Observer.
- Eugster, P. T., Felber, P. A., Guerraoui, R., & Kermarrec, A.-M. (2003). "The Many Faces of Publish/Subscribe." ACM Computing Surveys, 35(2), 114–131. — Observer es el caso especial, dentro de un único proceso y limitado a una clase, de los sistemas publish/subscribe que cubre esta revisión; el fan-out de webhook/auditoría/push del ejemplo aplicado es una miniatura exactamente de lo que describe a escala de sistemas distribuidos.
Pruebas unitarias
src/test/java/com/designpatterns/behavioral/observer/classic/WeatherStationTest.java
package com.designpatterns.behavioral.observer.classic;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class WeatherStationTest {
@Test
void everySubscribedObserverReactsIndependentlyToTheSameMeasurement() {
WeatherStation station = new WeatherStation();
CurrentConditionsDisplay display = new CurrentConditionsDisplay();
HeatAlertObserver alert = new HeatAlertObserver();
station.subscribe(display);
station.subscribe(alert);
station.setMeasurements(36.5, 40.0);
assertThat(display.currentConditions()).isEqualTo("Temp: 36.5C, Humidity: 40.0%");
assertThat(alert.isAlertActive()).isTrue();
}
@Test
void anUnsubscribedObserverStopsReceivingUpdates() {
WeatherStation station = new WeatherStation();
CurrentConditionsDisplay display = new CurrentConditionsDisplay();
station.subscribe(display);
station.setMeasurements(20.0, 50.0);
station.unsubscribe(display);
station.setMeasurements(30.0, 60.0);
assertThat(display.currentConditions()).isEqualTo("Temp: 20.0C, Humidity: 50.0%");
}
@Test
void theHeatAlertClearsWhenTheTemperatureDropsBackDown() {
WeatherStation station = new WeatherStation();
HeatAlertObserver alert = new HeatAlertObserver();
station.subscribe(alert);
station.setMeasurements(40.0, 30.0);
assertThat(alert.isAlertActive()).isTrue();
station.setMeasurements(22.0, 30.0);
assertThat(alert.isAlertActive()).isFalse();
}
}
src/test/java/com/designpatterns/behavioral/observer/applied/TransactionStatusPublisherTest.java
package com.designpatterns.behavioral.observer.applied;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class TransactionStatusPublisherTest {
@Test
void everyObserverReceivesEveryStatusChangeButOnlyPushNotifiesOnTerminalStates() {
TransactionStatusPublisher publisher = new TransactionStatusPublisher();
WebhookNotifierObserver webhook = new WebhookNotifierObserver();
AuditLogObserver audit = new AuditLogObserver();
PushNotificationObserver push = new PushNotificationObserver();
publisher.subscribe(webhook);
publisher.subscribe(audit);
publisher.subscribe(push);
publisher.publish("tx-1", TransactionStatus.PENDING);
publisher.publish("tx-1", TransactionStatus.PROCESSING);
publisher.publish("tx-1", TransactionStatus.SETTLED);
assertThat(webhook.deliveredWebhooks()).containsExactly(
"tx-1:PENDING", "tx-1:PROCESSING", "tx-1:SETTLED"
);
assertThat(audit.entries()).containsExactly(
"transaction tx-1 moved to PENDING",
"transaction tx-1 moved to PROCESSING",
"transaction tx-1 moved to SETTLED"
);
assertThat(push.pushedMessages()).containsExactly("Your transaction tx-1 is settled");
}
@Test
void pushNotifiesOnFailedToo() {
TransactionStatusPublisher publisher = new TransactionStatusPublisher();
PushNotificationObserver push = new PushNotificationObserver();
publisher.subscribe(push);
publisher.publish("tx-2", TransactionStatus.FAILED);
assertThat(push.pushedMessages()).containsExactly("Your transaction tx-2 is failed");
}
@Test
void anUnsubscribedObserverStopsReceivingStatusChanges() {
TransactionStatusPublisher publisher = new TransactionStatusPublisher();
AuditLogObserver audit = new AuditLogObserver();
publisher.subscribe(audit);
publisher.publish("tx-3", TransactionStatus.PENDING);
publisher.unsubscribe(audit);
publisher.publish("tx-3", TransactionStatus.SETTLED);
assertThat(audit.entries()).containsExactly("transaction tx-3 moved to PENDING");
}
}