O Padrão Decorator e o Padrão Observer: Composição Dinâmica e Desacoplamento de Eventos
Aula 13 — Programação Orientada a Objetos
1 Proposta da Aula
Na Aula 12, o Pedido fechou o “tríptico da estabilidade” por dentro (dado, gênese, fluxo) e abriu a primeira fronteira externa do sistema com o Adapter — uma camada de tradução entre o domínio e um sistema legado incompatível. Esta aula completa o conjunto de padrões estruturais e comportamentais do curso com mais duas peças, e as duas compartilham um mesmo espírito: trocar uma decisão tomada em tempo de compilação por uma decisão tomada em tempo de execução — só que aplicada a dois problemas diferentes.
O primeiro problema é sobre um único objeto: como adicionar responsabilidades opcionais e cumulativas a ele — leite, chocolate, chantilly num café; log, retry, cache num serviço — sem recair na mesma explosão combinatória de subclasses que a herança estática já havia produzido lá na Aula 10. A resposta é o padrão Decorator: envolver o objeto em camadas, uma a uma, decididas no momento em que o programa está rodando, não quando o código é compilado.
O segundo problema é sobre vários objetos: como um objeto central do domínio (de novo, o nosso Pedido) avisa um número variável de subsistemas periféricos — e-mail, log, estoque — de que algo aconteceu, sem que o Pedido precise conhecer cada um deles por nome e editar seu próprio código toda vez que um canal novo de notificação aparecer. A resposta é o padrão Observer: o Pedido passa a “gritar” que um evento ocorreu, e quem quiser ouvir se inscreve, sem que o gritante saiba quem — ou quantos — estão ouvindo.
O roteiro, em quatro perguntas:
- Por que a herança estática entra em colapso diante de responsabilidades opcionais e cumulativas — e por que esse colapso é o mesmo, em espírito, que já vimos na Aula 10 com axos de variação independentes?
- Como o Decorator resolve isso “embrulhando” objetos em camadas, e por que essa estrutura depende de uma dualidade de tipos pouco comum — o Decorator é, ao mesmo tempo, o mesmo tipo do que envolve e um contêiner desse tipo?
- Se um
Pedidoprecisa notificar e-mail, log e estoque toda vez que é faturado, por que amarrar essas três chamadas diretamente dentro dePedidoé uma bomba-relógio de manutenção? - Como o Observer inverte esse controle — e o que muda (e o que não muda) quando a mesma ideia sai da memória de um único processo e vai parar numa arquitetura de microsserviços distribuídos?
Considere a cafeteria do exemplo que abre esta aula: ela vende Cafe e quer oferecer Leite, Chocolate e Chantilly como opcionais independentes. Se o engenheiro tentar resolver isso com herança, vai precisar de uma subclasse para cada combinação possível — e é exatamente esse colapso que o próximo bloco expõe em detalhe, antes de o Decorator entrar em cena para resolvê-lo.
2 O Limite da Herança Estática e a Explosão Combinatória
O problema da herança sazonal. Considere um sistema de ponto de venda de uma cafeteria. Temos uma interface Bebida e uma implementação concreta Cafe. O departamento de marketing exige a inclusão de adicionais que modificam o preço e a descrição do produto: leite, chocolate e chantilly. Se o engenheiro tentar resolver isso com herança estática, ele criará uma subclasse para cada combinação possível:
// Explosao combinatoria por heranca estatica
public class Cafe implements Bebida { /* ... */ }
public class CafeComLeite extends Cafe { /* ... */ }
public class CafeComChocolate extends Cafe { /* ... */ }
public class CafeComChantilly extends Cafe { /* ... */ }
public class CafeComLeiteEChocolate extends Cafe { /* ... */ }
public class CafeComLeiteEChantilly extends Cafe { /* ... */ }
public class CafeComChocolateEChantilly extends Cafe { /* ... */ }
public class CafeComLeiteChocolateEChantilly extends Cafe { /* ... */ }
// 2^3 = 8 classes so para 3 adicionais independentesEste design colapsa rapidamente devido à explosão combinatória. À medida que novos adicionais são introduzidos, o número de subclasses cresce exponencialmente (\(2^N\)). Se a cafeteria passar a oferecer 10 adicionais diferentes, o sistema precisará de mais de mil classes apenas para representar as combinações possíveis. A herança falha em cenários onde os comportamentos são opcionais, cumulativos e decididos pelo utilizador final em tempo de execução — exatamente a mesma assinatura de problema que já vimos na Aula 10 com o cenário Físico/Digital × Nacional/Importado, só que ali os eixos eram atributos de um produto, e aqui são adicionais aplicados sobre a mesma instância.
A diferença crucial em relação à Aula 10 é esta: lá, a solução (Strategy) trocava um algoritmo por outro, de uma vez, dentro de um Contexto fixo. Aqui, o cliente quer acumular várias responsabilidades sobre o mesmo objeto, numa ordem e quantidade que só se sabe em tempo de execução — um problema que nem Strategy nem herança estática resolvem bem.
Escreva sua resposta e compare com um colega antes de avançar (2 min).
- □ Se a cafeteria removesse a opção de combinar adicionais entre si (isto é, o cliente só pudesse escolher exatamente um adicional ou nenhum), o número de subclasses necessárias para representar todas as combinações cresceria linearmente com o número de adicionais, em vez de exponencialmente.
- □ No limite em que o número de adicionais independentes tende a infinito, a razão entre o número de subclasses exigidas pela herança estática e o número de adicionais oferecidos também tende a infinito.
- □ Um sistema de seguros que precisa combinar de forma independente e cumulativa cobertura contra roubo, incêndio e desastres naturais em uma única apólice sofreria da mesma explosão combinatória se cada combinação de coberturas fosse modelada como uma subclasse de
Apolice. - □ Para 5 adicionais independentes, a estratégia de herança estática exigiria exatamente 5 subclasses novas, uma para cada adicional individual, sem necessidade de subclasses para as combinações entre eles.
3 O Padrão Decorator: Estrutura, Wrapping e Transparência
O conceito de envelopamento e transparência arquitetural. A solução do Decorator é tratar os adicionais não como tipos filiados à classe mãe, mas como “envelopes” (ou wrappers) que envolvem o objeto original. A grande sacada arquitetural do Decorator está na sua estrutura de tipos dupla: ele É-UM (implementa a interface do componente) ao mesmo tempo em que ele TEM-UM (contém uma instância da mesma interface).
Esta característica única cria uma estrutura semelhante a uma cebola. Imagine o objeto central Cafe. Se o cliente pede leite, nós o “embrulhamos” com um Leite. Se ele também quiser chocolate, pegamos o conjunto anterior e o embrulhamos com um Chocolate. Como todos os componentes — tanto o núcleo quanto os envelopes — assinam a mesmíssima interface, o código cliente que invoca os métodos permanece completamente alheio a quantas camadas de decoração foram empilhadas. O cliente chama o método getCusto() na camada mais externa, e a execução navega recursivamente para dentro de cada envelope, acumulando os valores até atingir o objeto central.
Definição do GoF: “Anexar responsabilidades adicionais a um objeto de maneira dinâmica. O Decorator fornece uma alternativa flexível à herança para extensão de funcionalidades.”
public interface Bebida {
double getCusto();
String getDescricao();
}
public class Cafe implements Bebida {
public double getCusto() { return 5.0; }
public String getDescricao() { return "Cafe"; }
}
// O DECORATOR BASE: repassa chamadas para o decorado
public abstract class BebidaDecorator implements Bebida {
protected Bebida bebidaDecorada; // Composicao da fronteira
public BebidaDecorator(Bebida b) { this.bebidaDecorada = b; }
public double getCusto() { return bebidaDecorada.getCusto(); }
public String getDescricao() { return bebidaDecorada.getDescricao(); }
}
// Decorador Concreto
public class Leite extends BebidaDecorator {
public Leite(Bebida b) { super(b); }
@Override
public double getCusto() { return super.getCusto() + 1.50; } // Adiciona custo
@Override
public String getDescricao() { return super.getDescricao() + ", Leite"; }
}
// Segundo Decorador Concreto, mesma estrutura
public class Chocolate extends BebidaDecorator {
public Chocolate(Bebida b) { super(b); }
@Override
public double getCusto() { return super.getCusto() + 2.00; }
@Override
public String getDescricao() { return super.getDescricao() + ", Chocolate"; }
}A classe BebidaDecorator é o coração técnico do padrão. Sendo abstrata, ela implementa a interface Bebida e obriga a passagem de um objeto Bebida em seu construtor. Seus métodos padrão fazem apenas o repasse puro das chamadas para o objeto interno (bebidaDecorada.getCusto()). Quando criamos o decorador concreto Leite, nós sobrescrevemos esses métodos usando uma estratégia recursiva: getCusto() executa super.getCusto(), que delega a pergunta para quem quer que esteja dentro dele (pode ser o Cafe puro ou outro decorador como Chocolate), e adiciona o valor de 1.50. Isso destrói a necessidade de herança estática combinatória: os opcionais agora são empilhados dinamicamente sob demanda.
O diagrama a seguir mostra a cadeia new Chocolate(new Leite(new Cafe())): o cliente chama getCusto() na camada mais externa (Chocolate), a chamada desce recursivamente até Cafe, e o valor sobe de volta acumulando cada acréscimo no caminho.
A seta tracejada superior mostra a descida da chamada (cada camada repassa para dentro via super.getCusto()); as setas inferiores coloridas mostram a subida do valor acumulado, cada camada somando seu próprio acréscimo antes de devolver o resultado para quem a envolve.
4 Análise da Recursão, o Mundo Real e a Cadeia de Notificadores do Pedido
Uso prático: o ecossistema Java I/O. A aplicação mais famosa do Decorator no mundo real é o próprio pacote java.io. Ler um arquivo do disco rígido byte a byte é uma operação computacionalmente cara. A classe FileInputStream faz exatamente isso: a leitura crua de arquivos. Para evitar ler o disco a todo momento, podemos querer usar um buffer em memória:
// Montando nosso cafe com leite em runtime
Bebida meuPedido = new Cafe(); // R$ 5,00
meuPedido = new Leite(meuPedido); // R$ 5,00 + R$ 1,50 = R$ 6,50
// O exemplo mais famoso do mundo real: Java IOStreams
InputStream stream = new BufferedInputStream(
new FileInputStream("dados.txt")
);Em vez de criar uma classe rígida chamada BufferedFileInputStream via herança, os engenheiros da Sun Microsystems criaram a classe genérica BufferedInputStream, que estende e decora qualquer tipo de InputStream. Desse modo, o desenvolvedor pode adicionar capacidade de buffer não apenas a arquivos físicos, mas também a conexões de rede (SocketInputStream) ou fluxos de memória, de forma totalmente intercambiável — evidenciando o poder de extensão limpa proporcionado pelo Decorator. FileInputStream é o componente concreto (lê bytes crus do disco, de forma lenta); BufferedInputStream é o decorador (adiciona um buffer de memória sem alterar a interface InputStream).
Replicando o raciocínio no domínio do curso: uma cadeia de notificadores. O mesmo mecanismo se aplica perfeitamente ao Pedido que atravessa esta disciplina. Suponha que, ao confirmar um pedido, o sistema precise enviar uma notificação — mas o requisito evolui: primeiro pedem para registrar um log de cada tentativa de envio; depois pedem para tentar reenviar automaticamente em caso de falha. Resolver isso por herança geraria a mesma explosão da cafeteria (NotificadorComLog, NotificadorComRetry, NotificadorComLogEComRetry, …). O Decorator resolve isso com a mesma estrutura de Bebida/BebidaDecorator:
public interface Notificador {
void enviar(String mensagem);
}
public class NotificadorEmail implements Notificador {
@Override
public void enviar(String mensagem) {
System.out.println("E-mail enviado: " + mensagem);
}
}
// O DECORATOR BASE: repassa a chamada para o notificador decorado
public abstract class NotificadorDecorator implements Notificador {
protected Notificador notificadorDecorado;
public NotificadorDecorator(Notificador n) { this.notificadorDecorado = n; }
@Override
public void enviar(String mensagem) {
notificadorDecorado.enviar(mensagem);
}
}
// Decorador Concreto: registra um log antes de repassar
public class NotificadorComLog extends NotificadorDecorator {
public NotificadorComLog(Notificador n) { super(n); }
@Override
public void enviar(String mensagem) {
System.out.println("[LOG] Tentando enviar: " + mensagem);
super.enviar(mensagem); // repassa para o notificador interno
}
}
// Decorador Concreto: tenta reenviar em caso de falha
public class NotificadorComRetry extends NotificadorDecorator {
private int maxTentativas;
public NotificadorComRetry(Notificador n, int maxTentativas) {
super(n);
this.maxTentativas = maxTentativas;
}
@Override
public void enviar(String mensagem) {
for (int tentativa = 1; tentativa <= maxTentativas; tentativa++) {
try {
super.enviar(mensagem); // cada tentativa reexecuta toda a cadeia interna
return;
} catch (RuntimeException e) {
System.out.println("Falha na tentativa " + tentativa + ", tentando novamente...");
}
}
}
}Montando a cadeia — NotificadorComRetry envolvendo NotificadorComLog, que envolve NotificadorEmail:
Notificador notificador = new NotificadorComRetry(
new NotificadorComLog(
new NotificadorEmail()), 3);
notificador.enviar("Pedido #123 confirmado.");Repare num detalhe importante da recursão aqui: como NotificadorComRetry está na camada mais externa, cada tentativa de reenvio executa super.enviar(mensagem) — que é o enviar() de NotificadorComLog — de modo que o log é registrado a cada tentativa, não apenas uma vez. Se a ordem fosse invertida (NotificadorComLog envolvendo NotificadorComRetry), o log seria registrado uma única vez, antes de todas as tentativas de retry ocorrerem lá dentro. Diferente do custo aditivo do exemplo da bebida (onde a ordem de empilhamento não muda a soma final), aqui a ordem das camadas muda o comportamento observável — uma lição importante sobre os limites da metáfora “cebola”: nem toda composição de decoradores é comutativa.
NotificadorComRetry envolvendo um NotificadorComLog que por sua vez envolve um NotificadorEmail, o método enviar() for chamado na camada mais externa, em que ordem exatamente a lógica de cada camada é executada — e o que essa ordem revela sobre por que o cliente nunca precisa saber quantas camadas existem?
Escreva sua resposta e compare com um colega antes de avançar (2 min).
- □ Se
BebidaDecoratornão implementasse a interfaceBebida, mas apenas contivesse uma referência interna a um objetoBebida, ainda seria possível empilharLeitesobreChocolateda mesma forma transparente que o padrão Decorator permite hoje. - □ Se um
Cafepuro (sem nenhum decorador aplicado) tiver seugetCusto()chamado diretamente, o resultado é idêntico ao que se obteria encadeando zero decoradores em torno dele — o caso de zero camadas de wrapping é apenas o caso degenerado da mesma recursão. - □ Um sistema de processamento de texto que permite aplicar, de forma independente e em qualquer ordem, negrito, itálico e sublinhado sobre um texto-base poderia usar a mesma estrutura de Decorator vista para
Bebida, com cada estilo implementado como uma classe que envolve o texto anterior. - □ Como o
BufferedInputStreamdecora qualquerInputStream, incluindo outroBufferedInputStreamjá aplicado, envolver um fluxo já bufferizado com um segundoBufferedInputStreamautomaticamente dobra a velocidade de leitura, já que dois buffers armazenam mais dados que um.
5 O Perigo do Acoplamento de Notificação em Cascata
Até agora, exploramos um padrão que isola responsabilidades acumuladas dentro de um único objeto. Mas há cenários em que a cooperação entre objetos deve ser estritamente indireta — quando uma ação realizada no núcleo do sistema dispara um efeito cascata que exige a atualização de múltiplos subsistemas periféricos. Considere o fluxo de faturamento de um Pedido:
public class Pedido {
private EmailService email;
private LogService log;
private InventarioService inventario; // Dependencias rigidas
public void fecharPedido() {
// Core Business
System.out.println("Pedido faturado.");
// EFEITO CASCATA: acoplamento com subsistemas perifericos
email.enviarConfirmacao();
log.registrarEvento();
inventario.baixarEstoque();
}
}Essa arquitetura falha gravemente em termos de modularidade. A classe Pedido, que devia conter apenas lógica pura de domínio, torna-se refém das APIs de e-mail, logging e gestão de estoque. Se o serviço de inventário falhar ou mudar sua assinatura de método, a classe Pedido quebra. Se o negócio decidir incluir o envio de uma mensagem SMS ou uma notificação Push sempre que um pedido for fechado, o engenheiro será forçado a abrir o arquivo Pedido.java para injetar um novo serviço e adicionar mais uma linha ao método fecharPedido(). Essa ligação direta viola o OCP (Aula 10) e polui o núcleo estável com a volatilidade da infraestrutura — o núcleo de negócio (Pedido) conhece detalhes de infraestrutura que não deveriam dizer respeito a ele.
Note a semelhança com a cadeia de notificadores do bloco anterior: ali, o Notificador era um único objeto acumulando responsabilidades. Aqui, o problema é diferente — são múltiplos objetos independentes (EmailService, LogService, InventarioService) que o Pedido precisa conhecer e invocar diretamente. O Decorator não resolve este problema; o padrão que resolve é o Observer.
6 O Padrão Observer: Publicador, Assinantes e Desacoplamento
A estrutura do Observer e o modelo publicador-assinante. A solução para quebrar o vínculo rígido de notificações é a aplicação do padrão Observer. Ele estabelece uma relação de dependência do tipo um-para-muitos baseada no anonimato de quem recebe. O objeto principal, conhecido como Sujeito (Subject), passa a gerir de forma genérica uma lista de referências para a interface Observer.
Definição do GoF: “Definir uma dependência um-para-muitos entre objetos, de modo que quando um objeto muda de estado, todos os seus dependentes são notificados e atualizados automaticamente.”
Esta topologia remove o conhecimento explícito entre as partes. O Sujeito não faz ideia de quais classes concretas estão na sua lista de monitorização — ele sabe apenas que todos os itens registrados implementam um contrato comum de atualização. Quando ocorre uma mutação interna no estado do Sujeito, ele varre sua lista interna e dispara uma notificação genérica para todos os inscritos. O fluxo de controle é invertido: em vez de o Pedido controlar ativamente o comportamento do EmailService, os serviços secundários passam a subscrever voluntariamente o ciclo de vida do Pedido.
// 1. A fronteira comum
public interface PedidoObserver {
void aoFaturar(String pedidoId);
}
// 2. Subsistemas independentes (plugins)
public class EmailService implements PedidoObserver {
public void aoFaturar(String id) { System.out.println("E-mail enviado para " + id); }
}
public class LogService implements PedidoObserver {
public void aoFaturar(String id) { System.out.println("Log registrado para " + id); }
}
// 3. O Sujeito (isolado de infraestrutura)
public class PedidoSubject {
private List<PedidoObserver> observadores = new ArrayList<>();
public void registrar(PedidoObserver o) { observadores.add(o); }
public void faturar(String id) {
System.out.println("Processando regras de negocio do pedido...");
for (PedidoObserver o : observadores) {
o.aoFaturar(id); // Notificacao abstrata em broadcast
}
}
}Ao analisarmos a classe PedidoSubject, notamos que o array interno armazena referências para a interface PedidoObserver, e não para as classes de infraestrutura diretamente. O método faturar() concentra-se apenas na execução do fluxo de negócio principal. Assim que o faturamento é bem-sucedido, a classe inicia uma iteração simples (broadcast) sobre sua lista de assinantes. A chamada o.aoFaturar(id) transfere a execução para os observadores sem que o sujeito saiba o que eles farão. O EmailService enviará um e-mail, enquanto um eventual SmsService enviará uma mensagem de texto. Se precisarmos remover o envio de e-mails do sistema, basta deixar de registrar o objeto correspondente na inicialização. A classe PedidoSubject permanece intacta e imutável, respeitando plenamente o Open/Closed Principle.
O Sujeito conhece só a interface PedidoObserver (seta sólida); cada observador concreto implementa essa interface (setas tracejadas) sem que o Sujeito precise conhecer sua classe real — a mesma “cegueira deliberada” já vista no State e no Adapter (Aula 12), agora aplicada à comunicação entre objetos independentes.
7 Estratégias de Dados (Push vs. Pull) e do Observer Local à Arquitetura Distribuída
Estratégias de tráfego de dados e acoplamento mútuo. Ao projetar o padrão Observer, o arquiteto de software deve decidir como os dados modificados serão transferidos do Sujeito para os Observadores. O GoF categoriza essa decisão em duas abordagens: o modelo Push (Empurrar) e o modelo Pull (Puxar).
No modelo Push, o Sujeito assume o papel ativo de empacotar as informações relevantes e passá-las como parâmetros diretamente na assinatura do método de notificação (aoFaturar(String id, double valor)). Esta abordagem é ideal quando as necessidades dos observadores são homogêneas e bem conhecidas — sua desvantagem aparece quando novos observadores exigem mais informações: mudar a assinatura da interface para adicionar parâmetros quebrará todas as classes de observadores já existentes, gerando um custo indesejado de refatoração.
Como alternativa, o modelo Pull propõe um fluxo invertido: o método de notificação passa apenas uma referência genérica do próprio Sujeito (update(PedidoSubject pedido)). Ao receber a notificação, o Observador assume a responsabilidade de interrogar ativamente o objeto recebido, chamando os métodos getter públicos necessários. O modelo Pull oferece maior resiliência a longo prazo, pois novos observadores podem extrair dados diferentes sem alterar a assinatura do método de callback — mas introduz um acoplamento bidirecional sutil: o observador agora precisa conhecer a interface pública do sujeito para extrair os dados, exigindo cautela para evitar violações de encapsulamento.
Do Observer local à arquitetura orientada a eventos. É fundamental expandir a visão além dos limites de um único processo em execução na JVM. O princípio de publicar e assinar eventos, introduzido pelo Observer local, atua como o fundamento teórico e prático para o modelo arquitetural mais utilizado em sistemas de grande escala modernos: a Arquitetura Orientada a Eventos (EDA — Event-Driven Architecture).
Em uma aplicação monolítica, o Observer funciona via referências de memória locais. Quando migramos para sistemas distribuídos de alta disponibilidade e microsserviços, a topologia permanece conceitualmente idêntica, mas as ferramentas mudam de escala:
| Observer local | Arquitetura Orientada a Eventos |
|---|---|
| Sujeito local | Produtor de Eventos (Publisher) |
| Interface de callback | Tópicos / Filas de eventos |
| Observador local | Consumidor de Eventos / Microsserviço |
O Sujeito local transforma-se no Produtor de Eventos; a lista interna de observadores é substituída por um barramento de mensagens (Message Broker, como Apache Kafka ou RabbitMQ); as interfaces de callback ganham a forma de Tópicos ou Filas distribuídas; os Observadores locais evoluem para Consumidores de Eventos independentes, executados em servidores distintos. “O Observer é o átomo fundamental que viabiliza sistemas reativos distribuídos.” Dominar o desacoplamento de controle do Observer em nível de código capacita o engenheiro a projetar ecossistemas distribuídos altamente resilientes, onde os microsserviços reagem a eventos assíncronos sem nunca conhecerem a identidade física uns dos outros.
As duas colunas do diagrama compartilham a mesma topologia — um emissor, um canal de anúncio, e consumidores que se inscrevem nesse canal — mostrando que a mudança de escala (memória local para rede distribuída) não altera o princípio arquitetural, só as ferramentas.
Escreva sua resposta e compare com um colega antes de avançar (2 min).
- □ Se o
PedidoSubjectusasse o modelo Pull (passando apenas a própria referência paraupdate(PedidoSubject pedido)) em vez do modelo Push, adicionar um observador que precisa do valor total do pedido não exigiria alterar a assinatura do método de notificação já usado pelos observadores existentes. - □ Se o número de observadores registrados em um
PedidoSubjectcrescer para um valor muito grande, oPedido(o Sujeito) continua sem precisar conhecer o tipo concreto de nenhum deles, porque oforde broadcast opera apenas sobre a interfacePedidoObserver. - □ Um sistema de sensores IoT que publica leituras de temperatura para múltiplos painéis de monitoramento, sem que o sensor conheça quantos ou quais painéis existem, está aplicando a mesma inversão de controle que o Observer aplica entre
PedidoSubjecte seus observadores. - □ Migrar de um Observer local (dentro da mesma JVM) para uma Arquitetura Orientada a Eventos distribuída (com Kafka ou RabbitMQ) exige abandonar completamente o princípio de desacoplamento do Observer, já que agora os componentes rodam em processos e máquinas diferentes.
8 Conclusão
Voltando às quatro perguntas da abertura:
- Por que a herança estática entra em colapso: porque responsabilidades opcionais, cumulativas e decididas em tempo de execução exigem uma classe por combinação, e o número de combinações cresce exponencialmente (\(2^N\)) — a mesma assinatura de problema já vista na Aula 10, agora aplicada ao acúmulo de responsabilidades sobre um único objeto, não à variação de um único algoritmo.
- Como o Decorator resolve isso: envolvendo o objeto em camadas que compartilham a mesma interface — a dualidade É-UM (para ser tratado como o objeto que envolve) e TEM-UM (para poder repassar e acrescentar comportamento) é o que sustenta a transparência: o cliente nunca sabe quantas camadas existem.
- Por que o acoplamento direto do
PedidoaEmailService/LogService/InventarioServiceé uma bomba-relógio: porque cada canal de notificação novo exige editarPedido, violando o OCP e contaminando o núcleo de negócio com detalhes de infraestrutura voláteis. - Como o Observer inverte esse controle, e o que muda na escala distribuída: o Sujeito passa a conhecer só uma interface abstrata de observadores, disparando um broadcast genérico; ao migrar para uma arquitetura distribuída, a topologia continua a mesma — só o Sujeito vira Produtor de Eventos, a lista de observadores vira um barramento de mensagens, e os observadores viram microsserviços consumidores.
O Pedido que atravessa esta disciplina agora tem, além do tríptico interno (dado, gênese, fluxo) e da fronteira externa do Adapter, duas ferramentas a mais: pode acumular responsabilidades sobre um único objeto sem explodir em subclasses (Decorator), e pode notificar um número variável de subsistemas sem conhecê-los por nome (Observer). Faltam quatro padrões no repertório do curso — Strategy, Adapter, Decorator e Observer — todos vistos agora; o que falta é consolidar essas quatro ferramentas numa visão arquitetural única, de fronteiras entre o que é estável e o que é volátil no sistema.
Ponte para a Aula 14
Os quatro padrões vistos até aqui — Strategy, Adapter, Decorator e Observer — não são truques isolados de sintaxe: cada um ataca um tipo específico de acoplamento nocivo (condicional, sintático, taxonômico, temporal). A Aula 14 fecha o curso consolidando essa visão: a distinção entre o núcleo estável (contratos, interfaces, regras de negócio que mudam raramente) e a periferia volátil (implementações concretas, SDKs de terceiros, detalhes de infraestrutura que mudam por pressão de mercado); uma matriz comparativa de decisão entre os quatro padrões; e o fechamento da metáfora do software como um sistema modular, plugável, onde componentes periféricos se encaixam em barramentos estáveis sem que o núcleo precise saber quem são.