Aula Soluções
Questões discursivas
(Da Herança à Composição em Tempo de Execução) Compare precisamente a explosão combinatória por herança estática (Aula 10, cenário Físico/Digital × Nacional/Importado) com a explosão combinatória de adicionais de bebida desta aula, e explique por que o Decorator resolve esta última sem recair no mesmo problema, mesmo permitindo empilhar múltiplas camadas por instância.
(A Dualidade É-UM/TEM-UM) Explique com precisão técnica por que BebidaDecorator precisa, ao mesmo tempo, implementar a interface Bebida (É-UM) e conter uma referência a um objeto Bebida (TEM-UM), e o que quebraria na transparência do padrão se apenas uma das duas relações existisse. Use o exemplo do java.io (BufferedInputStream/InputStream) para ilustrar sua resposta.
(Do Acoplamento em Cascata à Inversão de Controle) Considerando o Pedido original (com EmailService, LogService e InventarioService como dependências diretas), explique por que essa arquitetura viola o OCP, e demonstre como o Observer resolve o problema sem que PedidoSubject precise conhecer o tipo concreto de nenhum observador. Em seguida, compare os modelos Push e Pull quanto ao trade-off entre simplicidade imediata e flexibilidade a longo prazo.
Questões de Verdadeiro/Falso
Cada bloco de 4 itens trata do mesmo tema. A questão só é considerada correta se todos os 4 itens forem julgados corretamente (deixar em branco tem penalidade de 20% da nota da questão).
- □ Se a cafeteria decidisse tratar cada adicional como um atributo booleano dentro da própria classe
Cafe, em vez de como uma subclasse, o número de classes necessárias deixaria de crescer exponencialmente com o número de adicionais, mas a decisão de quais adicionais existem passaria a ser fixada em tempo de compilação dentro de Cafe.
- □ Se a cafeteria oferecesse apenas 1 adicional opcional, o número de subclasses exigido pela herança estática seria idêntico ao que a mesma situação exigiria sob o padrão Decorator (uma classe para representar o objeto simples, outra para o objeto com o adicional).
- □ Um editor de imagens que aplica filtros (preto-e-branco, sépia, desfoque) de forma independente e cumulativa sobre uma foto sofreria do mesmo tipo de explosão combinatória se cada combinação de filtros fosse modelada como uma subclasse de
Imagem.
- □ Bastaria usar interfaces em vez de classes abstratas para representar
CafeComLeite, CafeComChocolate etc. para eliminar a explosão combinatória, já que interfaces não sofrem do mesmo problema de herança múltipla que classes sofrem em Java.
- □ Se
Leite chamasse bebidaDecorada.getCusto() diretamente no seu construtor, em vez de dentro do método getCusto() sobrescrito, o custo acumulado refletiria apenas o valor do objeto decorado no momento da criação de Leite, não permitindo que mudanças posteriores no objeto interno fossem refletidas em chamadas futuras.
- □ Se um cliente empilhasse dez decoradores
Leite seguidos sobre o mesmo Cafe (new Leite(new Leite(... new Cafe() ...))), o custo final refletiria dez incrementos de R$ 1,50 aplicados sucessivamente, mesmo sem existir nenhuma subclasse dedicada a “café com dez leites”.
- □ Um sistema de permissões que envolve um
Usuario base com decoradores como PermissaoAdmin e PermissaoAuditor, cada um acrescentando checagens de acesso ao redor do usuário original, estaria aplicando a mesma estrutura É-UM/TEM-UM vista em BebidaDecorator.
- □ Como o Decorator implementa a mesma interface do objeto que envolve, um decorador concreto nunca deveria sobrescrever um método herdado de
BebidaDecorator, pois isso quebraria o repasse puro que define o padrão.
- □ Se
Cafe.getCusto() retornasse um valor diferente a cada chamada (por exemplo, por causa de uma promoção aleatória), uma cadeia new Chocolate(new Leite(new Cafe())) ainda produziria um resultado consistente com a soma dos acréscimos de cada camada sobre o valor retornado por Cafe naquela chamada específica.
- □ Na cadeia
new Chocolate(new Leite(new Cafe())), se removêssemos a chamada super.getCusto() de dentro do método getCusto() de Chocolate, o valor final retornado ignoraria completamente o custo acumulado por Leite e por Cafe, refletindo apenas o acréscimo do próprio Chocolate.
- □ Uma pilha (
Stack) de chamadas de função em qualquer linguagem de programação, na qual cada função retorna depois de invocar a próxima, compartilha a mesma lógica estrutural de “delegar para dentro e acumular ao retornar” que a cadeia de decoradores usa para calcular getCusto().
- □ Como cada decorador concreto soma um valor fixo ao custo do objeto que envolve, a ordem em que os decoradores são empilhados (
new Chocolate(new Leite(new Cafe())) vs. new Leite(new Chocolate(new Cafe()))) sempre produz um custo final diferente.
- □ Se
BufferedInputStream estendesse diretamente FileInputStream em vez de decorar a interface InputStream, o mesmo BufferedInputStream não poderia ser usado para adicionar buffer a um SocketInputStream (leitura de rede) sem duplicar código.
- □ Se um
NotificadorComRetry envolvesse um NotificadorComLog, que por sua vez envolvesse um NotificadorEmail, e a tentativa de envio falhasse repetidamente até esgotar as tentativas de retry, o log registrado pelo NotificadorComLog ainda seria disparado a cada tentativa individual, não apenas uma vez ao final.
- □ Um pipeline de processamento de dados que envolve uma fonte de dados bruta com camadas sucessivas de validação, transformação e cache, cada uma implementando a mesma interface da anterior, está aplicando a mesma lógica de composição em camadas vista em
BufferedInputStream e na cadeia de notificadores do Pedido.
- □ Como
NotificadorComLog e NotificadorComRetry decoram a mesma interface Notificador, a ordem em que são aplicados (log por fora do retry, ou retry por fora do log) é irrelevante para o comportamento observável do sistema.
- □ Se
Pedido.fecharPedido() recebesse EmailService, LogService e InventarioService por injeção de dependência via construtor, em vez de instanciá-los internamente, isso sozinho já eliminaria a necessidade de editar Pedido sempre que um canal de notificação novo (como SMS) for adicionado.
- □ Se
Pedido precisasse notificar zero subsistemas periféricos (nenhum e-mail, log ou atualização de estoque), a arquitetura de dependências diretas do exemplo original ainda seria uma violação do OCP, mesmo sem nenhuma chamada real no corpo de fecharPedido().
- □ Uma função de checkout de e-commerce que, após confirmar um pagamento, chama diretamente APIs de frete, de fidelidade e de recomendação de produtos dentro do mesmo método, sofre do mesmo tipo de acoplamento de notificação em cascata descrito para o
Pedido.
- □ O problema do
Pedido com EmailService/LogService/InventarioService é resolvido simplesmente movendo as três chamadas para um método privado separado dentro da própria classe Pedido, já que isso reduz o tamanho do método fecharPedido().
- □ Se
PedidoSubject.observadores fosse declarado como List<EmailService> em vez de List<PedidoObserver>, ainda seria possível registrar um SmsService nessa lista, desde que SmsService implementasse a interface PedidoObserver.
- □ Se a lista
observadores de PedidoSubject estiver vazia no momento em que faturar() é chamado, o método ainda executa corretamente a lógica de negócio principal, apenas sem disparar nenhuma notificação.
- □ Um sistema de notícias que mantém uma lista de assinantes de uma newsletter, notificando todos eles sempre que um novo artigo é publicado, sem saber se o assinante é uma pessoa física ou um outro sistema automatizado, está aplicando a mesma estrutura Subject/Observer vista no
PedidoSubject.
- □ Registrar um observador em
PedidoSubject usando registrar(PedidoObserver o) cria necessariamente uma dependência do PedidoSubject em relação à classe concreta do observador registrado, da mesma forma que ocorria na versão original com EmailService fixo.
- □ Se o modelo Push do
PedidoSubject já enviasse tanto o id quanto o valor total do pedido como parâmetros de aoFaturar(String id, double valor), adicionar um observador que só precisa do id não exigiria nenhuma mudança na assinatura da interface PedidoObserver.
- □ No modelo Pull, se o objeto
Subject passado para update(Subject s) não expuser nenhum método público além de construtores, o observador fica impossibilitado de extrair qualquer dado relevante, tornando a notificação praticamente inútil.
- □ Um sistema de sensores que envia, a cada leitura, apenas um identificador do sensor que mudou (modelo Pull), deixando o painel de monitoramento responsável por consultar o valor atual da temperatura, sofre do mesmo tipo de acoplamento bidirecional descrito para o modelo Pull do Observer.
- □ Como o modelo Pull é mais flexível a longo prazo do que o Push, ele deveria ser sempre preferido, independentemente de quão homogêneas e estáveis sejam as necessidades de dados dos observadores.
- □ Se um sistema trocasse seu
PedidoSubject local por um Produtor de Eventos publicando em um tópico Kafka, os Consumidores de Eventos ainda poderiam, em princípio, permanecer completamente alheios à identidade física uns dos outros, da mesma forma que os observadores locais não conheciam a identidade concreta uns dos outros.
- □ Se um sistema distribuído tiver um único Consumidor de Eventos assinando um único tópico, a topologia ainda é conceitualmente uma aplicação do padrão Observer, mesmo com apenas um assinante em vez de muitos.
- □ Um sistema de biblioteca que notifica automaticamente todos os leitores inscritos em uma fila de espera assim que um livro é devolvido, sem que o sistema de devolução saiba quantos leitores estão na fila, está aplicando a mesma lógica de mapeamento Sujeito→Produtor de Eventos vista na evolução do Observer para EDA.
- □ Migrar do Observer local para uma Arquitetura Orientada a Eventos distribuída elimina a necessidade de definir contratos (interfaces/esquemas de mensagem) entre publicador e consumidores, já que o barramento de mensagens absorve toda a responsabilidade de comunicação.