Exercícios — 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

Aula Soluções

Questões discursivas

  1. (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.

  2. (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.

  3. (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).

NotaTeste 1 — A Explosão Combinatória e o Limite da Herança Estática
  • □ 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.
NotaTeste 2 — Estrutura do Decorator: Wrapping, É-UM e TEM-UM
  • □ 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.
NotaTeste 3 — A Recursão no Decorator: BebidaDecorator e Leite
  • □ 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.
NotaTeste 4 — Decorator no Mundo Real: java.io e a Cadeia de Notificadores
  • □ 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.
NotaTeste 5 — O Acoplamento de Notificação em Cascata
  • □ 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().
NotaTeste 6 — Estrutura do Observer: Subject, Observer e Broadcast
  • □ 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.
NotaTeste 7 — Push vs. Pull: Estratégias de Tráfego de Dados
  • □ 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.
NotaTeste 8 — Do Observer Local à Arquitetura Orientada a Eventos
  • □ 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.