O Padrão Decorator e o Padrão Observer: Composição Dinâmica e Desacoplamento de Eventos

Aula 13 — Programação Orientada a Objetos

Autor

Marcos M. Raimundo — Instituto de Computação, UNICAMP

Data de Publicação

18 de setembro de 2026

Slides Lista de aulas

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:

  1. 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?
  2. 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?
  3. Se um Pedido precisa notificar e-mail, log e estoque toda vez que é faturado, por que amarrar essas três chamadas diretamente dentro de Pedido é uma bomba-relógio de manutenção?
  4. 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 independentes

Este 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.

DicaSe a cafeteria decidir oferecer 5 adicionais independentes (Leite, Chocolate, Chantilly, Canela, Mel), quantas classes distintas a estratégia de herança estática exigiria para representar todas as combinações possíveis — e o que esse número revela sobre por que “adicionar mais uma subclasse quando precisar” nunca é uma resposta sustentável nesse cenário?

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.

DicaSe, no exemplo do 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 BebidaDecorator não implementasse a interface Bebida, mas apenas contivesse uma referência interna a um objeto Bebida, ainda seria possível empilhar Leite sobre Chocolate da mesma forma transparente que o padrão Decorator permite hoje.
  • □ Se um Cafe puro (sem nenhum decorador aplicado) tiver seu getCusto() 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 BufferedInputStream decora qualquer InputStream, incluindo outro BufferedInputStream já aplicado, envolver um fluxo já bufferizado com um segundo BufferedInputStream automaticamente 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.

DicaSe um novo tipo de observador precisar, no futuro, de um dado que hoje não está entre os parâmetros enviados pelo modelo Push atual (por exemplo, o valor total do pedido), o que precisa mudar no sistema — e por que essa mesma pergunta, respondida de outro jeito, é exatamente o argumento a favor do modelo Pull?

Escreva sua resposta e compare com um colega antes de avançar (2 min).

  • □ Se o PedidoSubject usasse o modelo Pull (passando apenas a própria referência para update(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 PedidoSubject crescer para um valor muito grande, o Pedido (o Sujeito) continua sem precisar conhecer o tipo concreto de nenhum deles, porque o for de broadcast opera apenas sobre a interface PedidoObserver.
  • □ 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 PedidoSubject e 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:

  1. 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.
  2. 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.
  3. Por que o acoplamento direto do Pedido a EmailService/LogService/InventarioService é uma bomba-relógio: porque cada canal de notificação novo exige editar Pedido, violando o OCP e contaminando o núcleo de negócio com detalhes de infraestrutura voláteis.
  4. 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.

Exercícios Soluções