← Todos os padrões

Observer

Behavioral · ver código-fonte no GitHub

Leia em: English · Português · Español

Category: Behavioral

O problema

A mudança de estado de um objeto precisa se refletir em vários outros, mas esses outros não deveriam estar amarrados diretamente ao objeto que mudou. Chamar cada dependente diretamente de dentro do sujeito o acopla ao tipo concreto de cada consumidor, e adicionar um novo consumidor significa editar o código do sujeito de novo. O que se precisa é de uma forma de partes interessadas se registrarem e serem notificadas, sem que o sujeito saiba nada sobre elas além de uma interface comum.

A solução

O sujeito mantém uma lista de observadores atrás de uma interface comum e notifica todos eles sempre que seu estado muda; cada observador decide independentemente o que fazer com essa notificação. Inscrever e cancelar inscrição não exigem tocar na lógica do próprio sujeito.

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

Exemplo clássico

classic/WeatherStation é o exemplo canônico: um sujeito que empurra leituras de temperature/humidity pra cada WeatherObserver inscrito. CurrentConditionsDisplay só armazena a leitura mais recente; HeatAlertObserver deriva um booleano de alerta a partir dela — dois observadores fazendo coisas genuinamente diferentes com a mesma notificação exata, nenhum ciente da existência do outro. WeatherStationTest cobre os dois observadores reagindo independentemente a uma medição, um observador com inscrição cancelada não recebendo mais atualizações, e o alerta de calor limpando quando a temperatura volta a cair.

Exemplo aplicado: fan-out de status de transação

applied/TransactionStatusPublisher notifica três observadores independentes sempre que o status de uma transação muda: WebhookNotifierObserver (registra uma chamada de webhook de saída), AuditLogObserver (registra cada transição pra compliance), e PushNotificationObserver (só reage aos estados terminais, SETTLED/FAILED — um cliente não precisa de push pra cada estado intermediário). Essa é exatamente a estrutura que um gateway de pagamento real precisa quando o ciclo de vida de uma transação tem que alcançar vários sistemas independentes: o publisher não sabe nem se importa quantos consumidores existem, ou o que cada um faz de fato com a notificação. TransactionStatusPublisherTest cobre os três observadores reagindo a uma sequência completa PENDING→PROCESSING→SETTLED, o observador de push também disparando em FAILED, e um observador com inscrição cancelada não recebendo mais atualizações.

Quando não usar

Cobertura de testes

100% de cobertura de instrução, 100% de cobertura de branch (JaCoCo). Reproduza você mesmo:

./gradlew :behavioral:observer:jacocoTestReport

Relatório em behavioral/observer/build/reports/jacoco/test/html/index.html.

Leitura complementar

Testes unitários

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 relatório completo de cobertura JaCoCo →