← Todos los patrones

Observer

Behavioral · ver código fuente en GitHub

Leer en: English · Português · Español

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

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

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");
    }
}

Ver informe completo de cobertura JaCoCo →