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
- Se existe exatamente um consumidor e ele nunca vai ser mais que um, uma chamada de método direta é mais simples e mais fácil de acompanhar do que um mecanismo de inscrição construído pra um caso que ainda não existe.
- Observadores que precisam rodar numa ordem específica, ou cuja falha deveria impedir os outros de rodar, não se encaixam bem nesse padrão — o Observer puro não dá nenhuma garantia de ordem ou isolamento de erro. Isso precisa de um pipeline explícito em vez disso.
- Cuidado com observadores que silenciosamente mantêm uma referência a um sujeito viva por mais tempo do que o pretendido (uma forma clássica de vazamento de memória em sujeitos de vida longa com observadores de vida curta) — um observador que terminou precisa cancelar a inscrição, não só sair de escopo.
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
- Gamma, E., Helm, R., Johnson, R., & Vlissides, J. (1994). Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley. — o Capítulo 5 formaliza o 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. — o Observer é o caso especial, dentro de um único processo e limitado a uma classe, dos sistemas publish/subscribe que essa revisão cobre; o fan-out de webhook/auditoria/push do exemplo aplicado é uma miniatura exatamente do que ela descreve em escala de sistemas distribuídos.
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");
}
}