Soluções — O Padrão Decorator e o Padrão Observer: Composição Dinâmica e Desacoplamento de Eventos
Aula 13 — Programação Orientada a Objetos
Dica(Resposta) Teste 1 — A Explosão Combinatória e o Limite da Herança Estática
- ✔ Verdadeiro — Usar atributos booleanos dentro de
Cafe(temLeite,temChocolate, …) de fato evita a explosão de subclasses — o número de classes deixa de depender do número de adicionais. Mas o conjunto de adicionais possíveis passa a estar fixado dentro do código-fonte deCafe: adicionar um novo adicional exige editar essa classe e recompilá-la, uma forma diferente (mas real) de rigidez em tempo de compilação, ainda que sem explosão combinatória de classes. - ✔ Verdadeiro — Com \(N=1\), a herança estática exige \(2^1=2\) classes (
CafeeCafeComLeite), e o Decorator também usaria 2 classes (CafeeLeite, mais a classe abstrataBebidaDecorator, que é infraestrutura reutilizável, não específica de um adicional). No caso trivial de um único adicional, as duas estratégias empatam em número de classes por adicional — é só quando o número de adicionais cresce que a herança diverge exponencialmente enquanto o Decorator continua crescendo linearmente (uma classe nova por adicional, não por combinação). - ✔ Verdadeiro — A assinatura estrutural do problema é idêntica à da cafeteria: filtros opcionais, cumulativos e combináveis livremente, modelados como subclasses, geram \(2^N\) combinações para \(N\) filtros — exatamente o mesmo colapso, em outro domínio.
- ✗ Falso — O problema da explosão combinatória não nasce da distinção classe/interface, nasce de modelar cada combinação de adicionais como um tipo à parte. Trocar
extendsporimplementsnão muda o fato de que ainda seria preciso um tipo (classe ou interface) para cada uma das \(2^N\) combinações possíveis — o número de tipos necessários continuaria exponencial.
Dica(Resposta) Teste 2 — Estrutura do Decorator: Wrapping, É-UM e TEM-UM
- ✔ Verdadeiro — Calcular e guardar o custo no construtor “congelaria” o valor no momento da criação; qualquer mutação posterior no objeto decorado (por exemplo, uma promoção aplicada depois) não seria refletida, porque o
getCusto()deLeiteretornaria o valor armazenado, não recalcularia via delegação. É por isso que o padrão delega a cada chamada, dentro do próprio método, e não uma única vez no construtor. - ✔ Verdadeiro — Cada camada de
Leitesoma seu próprio acréscimo de forma independente — a recursão simplesmente atravessa dez camadas em vez de uma. Nenhuma classe nova é necessária para esse caso extremo, o que evidencia a vantagem central do Decorator sobre a herança estática: a quantidade de camadas é uma decisão de runtime, não de projeto de classes. - ✔ Verdadeiro — A mesma dualidade estrutural se aplica: cada decorador de permissão implementaria a interface do
Usuario(É-UM) e conteria uma referência ao usuário decorado (TEM-UM), permitindo empilhar checagens de acesso de forma cumulativa e transparente ao código cliente. - ✗ Falso — É exatamente o oposto: o repasse puro é o comportamento padrão de
BebidaDecorator(a classe abstrata), mas cada decorador concreto (Leite,Chocolate) precisa sobrescrever os métodos para acrescentar seu próprio comportamento sobre o repasse (viasuper.getCusto() + acréscimo) — sem essa sobrescrita, o decorador não teria nenhum efeito observável.
Dica(Resposta) Teste 3 — A Recursão no Decorator:
BebidaDecorator e Leite
- ✔ Verdadeiro — O mecanismo de delegação recursiva não depende de
Cafe.getCusto()ser determinístico — cada chamada degetCusto()na cadeia dispara uma nova descida atéCafe, e o valor que ele retornar naquele instante é o que sobe acumulando os acréscimos deLeiteeChocolate. A consistência está no mecanismo (soma sobre o valor retornado naquela chamada), não na estabilidade do valor deCafeentre chamadas diferentes. - ✔ Verdadeiro —
super.getCusto()é exatamente o elo que conecta a camada atual com o valor acumulado por todas as camadas internas. Removê-lo quebra a cadeia recursiva no ponto deChocolate: o método deixa de descer paraLeite/Cafee retorna apenas o valor queChocolateadicionaria, um caso-limite que expõe a dependência estrutural do padrão em relação a essa chamada. - ✔ Verdadeiro — A pilha de chamadas de função é a mesma estrutura recursiva geral que o Decorator usa deliberadamente para acumular valores: descer delegando, subir acumulando — o Decorator apenas dá nome e reuso a essa estrutura no contexto de composição dinâmica de objetos.
- ✗ Falso — Para operações puramente aditivas sobre um número (soma de acréscimos fixos), a ordem das camadas não altera o resultado final — a soma é comutativa.
getCusto()produziria R$ 8,50 em qualquer uma das duas ordens (isso contrasta deliberadamente com o exemplo doNotificadorComRetry/NotificadorComLog, onde a ordem das camadas muda o comportamento observável, porque a operação ali não é aditiva/comutativa).
Dica(Resposta) Teste 4 — Decorator no Mundo Real:
java.io e a Cadeia de Notificadores
- ✔ Verdadeiro — Herdar diretamente de
FileInputStreamprenderiaBufferedInputStreama essa única classe concreta em tempo de compilação; para bufferizar umSocketInputStream, seria preciso criar uma segunda classe (BufferedSocketInputStream), duplicando a lógica de buffer. Decorar a interfaceInputStreamé o que permite ao mesmoBufferedInputStreamenvolver qualquer implementação concreta. - ✔ Verdadeiro — Como
NotificadorComLogestá na camada logo abaixo deNotificadorComRetry, cada iteração do laço de retry chamasuper.enviar(), que é oenviar()deNotificadorComLog— portanto o log é registrado uma vez por tentativa, não uma única vez ao final da série de tentativas. - ✔ Verdadeiro — Mesma estrutura de composição recursiva em camadas, aplicada a um domínio de pipeline de dados em vez de streams de I/O ou notificações — cada camada adiciona uma responsabilidade e delega para a camada interna.
- ✗ Falso — A ordem muda o comportamento observável: com
NotificadorComRetrypor fora (como no exemplo da aula), o log dispara a cada tentativa de reenvio; comNotificadorComLogpor fora, o log dispararia uma única vez, antes de todas as tentativas de retry ocorrerem internamente. Diferente do custo aditivo do exemplo da bebida, esta composição não é comutativa.
Dica(Resposta) Teste 5 — O Acoplamento de Notificação em Cascata
- ✗ Falso — Injeção de dependência resolve o problema de acoplamento a implementações concretas (permite trocar
EmailServicepor um mock, por exemplo), mas não resolve o problema estrutural do OCP aqui em jogo:Pedidoainda precisaria de um novo campo e uma nova linha de chamada dentro defecharPedido()para cada canal novo, mesmo que os serviços existentes sejam injetados. Só uma lista de abstrações percorrida em broadcast (Observer) elimina essa necessidade de editarPedido. - ✗ Falso — A violação do OCP se manifesta como a necessidade de modificar
Pedidoquando um requisito novo aparece. Sem nenhuma notificação a fazer, não há ainda nenhuma dependência concreta codificada emPedido, logo não há violação a apontar nesse estado — o problema só se manifesta no momento em que o primeiro (ou um novo) canal de notificação precisa ser adicionado. - ✔ Verdadeiro — Mesma assinatura estrutural: um método de negócio central que conhece e invoca diretamente múltiplos subsistemas periféricos, tornando-se refém das assinaturas e da disponibilidade de cada um deles.
- ✗ Falso — Extrair as chamadas para um método privado melhora a organização/legibilidade local, mas não remove o acoplamento:
Pedidoainda conhece e depende diretamente das três classes concretas, e ainda precisaria ser editado (mesmo que só nesse método privado) para adicionar um canal novo — a violação do OCP permanece intacta.
Dica(Resposta) Teste 6 — Estrutura do Observer: Subject, Observer e Broadcast
- ✗ Falso — O sistema de tipos do Java não permite: uma
List<EmailService>só aceita instâncias deEmailService(ou subclasses dela).SmsServiceimplementarPedidoObservernão o torna umEmailService; ele só poderia ser adicionado a uma lista tipada comoList<PedidoObserver>— exatamente por isso o Sujeito deve depender da interface, não de uma classe concreta. - ✔ Verdadeiro — O laço
forsobre uma lista vazia simplesmente não itera nenhuma vez; a lógica de negócio (processar o faturamento) roda normalmente antes do laço e é completamente independente de haver ou não observadores registrados — mais uma evidência do desacoplamento entre regra de negócio e notificação. - ✔ Verdadeiro — Mesma topologia um-para-muitos com anonimato de identidade concreta do lado dos assinantes — o publicador (fonte de notícias) só conhece a interface de notificação comum, exatamente como
PedidoSubjectsó conhecePedidoObserver. - ✗ Falso — É exatamente o oposto — o parâmetro do método
registraré tipado comoPedidoObserver(a interface), entãoPedidoSubjectnunca referencia nem depende da classe concreta de quem é registrado; essa é precisamente a diferença estrutural que elimina a dependência direta presente na versão original comEmailServicefixo.
Dica(Resposta) Teste 7 — Push vs. Pull: Estratégias de Tráfego de Dados
- ✔ Verdadeiro — Uma vez que a assinatura já inclui os dois parâmetros, qualquer observador (existente ou novo) pode simplesmente ignorar o parâmetro que não usa — o problema do Push só aparece quando um observador precisa de um dado que a assinatura ainda não inclui, forçando uma mudança que quebraria os observadores já existentes.
- ✔ Verdadeiro — O modelo Pull depende inteiramente da API pública do Sujeito para que o observador consiga extrair dados; sem nenhum getter público exposto, não há como o observador obter informação alguma a partir da referência recebida, evidenciando o caso-limite em que o Pull falha por falta de superfície pública.
- ✔ Verdadeiro — O painel precisa conhecer a API pública do sensor para consultar o valor — exatamente o acoplamento bidirecional sutil descrito para o modelo Pull: o observador passa a depender da interface pública do sujeito.
- ✗ Falso — A escolha entre Push e Pull é um trade-off, não uma hierarquia absoluta: quando as necessidades de dados dos observadores são homogêneas e bem conhecidas, o Push é mais simples e direto, sem o custo do acoplamento bidirecional que o Pull introduz. Preferir sempre um dos dois ignora o contexto que motiva a escolha.
Dica(Resposta) Teste 8 — Do Observer Local à Arquitetura Orientada a Eventos
- ✔ Verdadeiro — A topologia permanece conceitualmente idêntica ao migrar de escala — o desacoplamento entre observadores (ou consumidores) é uma propriedade da estrutura publicador/canal/assinante, preservada independentemente de os assinantes estarem na mesma JVM ou em processos distintos.
- ✔ Verdadeiro — A relação “um-para-muitos” do Observer degenera, no caso limite de um único assinante, em “um-para-um” sem deixar de ser uma aplicação válida do padrão — o mecanismo de desacoplamento (publicador não conhece assinante concreto) continua presente independentemente da cardinalidade.
- ✔ Verdadeiro — Mesma lógica de publicador que dispara um evento (devolução) para uma lista de interessados sem conhecê-los individualmente — mapeável diretamente para Sujeito→Produtor de Eventos, Observadores→Consumidores.
- ✗ Falso — O barramento de mensagens transporta os eventos, mas não elimina a necessidade de um contrato compartilhado sobre o formato/esquema dessas mensagens — produtor e consumidores ainda precisam concordar sobre a estrutura dos dados publicados, o análogo distribuído da interface
PedidoObserverdo Observer local.