Exercícios — O Colapso da Herança e a Era da Composição: o Padrão Strategy

Aula 10 — Programação Orientada a Objetos

Autor

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

Aula Soluções

Questões discursivas

  1. (Explosão Combinatória) Considere um sistema modelado por herança pura que precisa combinar dois eixos de variação independentes, cada um com duas opções (por exemplo, Logística: Físico/Digital; Fiscalidade: Nacional/Importado). Demonstre por que o número de subclasses-folha necessárias cresce da forma que cresce, e explique com precisão por que a proibição de herança múltipla de classes em Java não seria suficiente para resolver esse problema mesmo se ela não existisse — ou seja, mesmo que Java permitisse class FisicoImportado extends ProdutoFisico, ProdutoImportado, que outro problema estrutural continuaria existindo?

  2. (Identidade vs. Papel) Explique por que modelar os meios de pagamento de um Pedido através de subclasses (PedidoPix, PedidoCartao) confunde os conceitos de identidade e papel. Descreva, com precisão técnica, a falha concreta que ocorre na memória quando o cliente decide trocar de meio de pagamento no último segundo do fluxo de checkout, e explique por que essa falha não ocorre na solução por composição/Strategy.

  3. (Topologia do Strategy e o OCP) Mapeie a topologia formal do padrão Strategy (Context, Strategy, ConcreteStrategy) para as classes do e-commerce discutidas em aula (Pedido, Pagavel, Pix/Cartao/Boleto). Em seguida, demonstre como a introdução de uma nova estratégia, PagamentoCriptomoeda, comprova que o sistema está “aberto para extensão e fechado para modificação” — identificando exatamente qual arquivo não precisa ser tocado e por quê.

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 — Fusão Estrutural e o Custo da Herança de Estado
  • □ Se uma subclasse não acessa nenhum atributo protected herdado do pai, ainda assim ela carrega esses atributos fisicamente alocados na Heap para cada instância criada.
  • □ Herdar de uma classe unicamente para reutilizar métodos prontos, sem que exista uma relação de identidade real entre as duas classes, reduz o acoplamento do sistema porque elimina duplicação de código.
  • □ Se PedidoInternacional dependesse de Pedido por composição em vez de herança, uma mudança no formato interno de armazenamento de itens dentro de Pedido teria maior chance de permanecer invisível para PedidoInternacional, contanto que a interface pública de Pedido não mudasse.
  • □ Um sistema de RH que cria FuncionarioTerceirizado estendendo Funcionario apenas para reaproveitar o método calcularHorasTrabalhadas(), sem que o terceirizado deva ser tratado polimorficamente como um Funcionario em folha de pagamento, comete o mesmo erro semântico do PedidoInternacional.
NotaTeste 2 — O Peso Físico na Heap: a Subclasse Gorda
  • □ Uma classe ArquivoDeVideo que estende uma classe base ArquivoDeMidia contendo o atributo duracaoDeExibicaoEmSala (relevante só para filmes de cinema) carregaria esse atributo desperdiçado em cada instância, mesmo sendo um vídeo doméstico.
  • □ Se uma classe base abstrata não declarar nenhum atributo próprio, apenas métodos abstratos, suas subclasses não sofrem o fenômeno da Subclasse Gorda relacionado a esses atributos, mesmo herdando de uma hierarquia profunda.
  • □ O desperdício de memória da Subclasse Gorda é resolvido simplesmente tornando os atributos da classe base private em vez de protected, já que isso impede o acesso indevido por parte das subclasses.
  • □ Se pesoFisico e dimensoesPacote fossem movidos de ProdutoBase para uma classe DadosLogisticosFisicos usada por composição apenas nos produtos que realmente precisam de envio físico, um Ebook deixaria de alocar espaço para esses dois atributos.
NotaTeste 3 — Explosão Combinatória e Eixos Ortogonais
  • □ Em uma hierarquia de herança pura que já modela três eixos binários de variação independentes (totalizando 8 subclasses-folha), a adição de um quarto eixo binário eleva esse número para 16.
  • □ A explosão combinatória só ocorre quando os eixos de variação envolvidos são eixos técnicos de infraestrutura (como logística); eixos puramente de regra de negócio, como categorias promocionais, não geram o mesmo efeito.
  • □ Um sistema de seguros que precisa combinar Tipo de Veículo (Carro/Moto) com Perfil de Risco (Baixo/Alto) através de subclasses como CarroBaixoRisco e MotoAltoRisco está sujeito ao mesmo padrão de explosão combinatória visto no e-commerce.
  • □ Se dois eixos de variação de um sistema não forem realmente independentes — por exemplo, se todo produto Importado for necessariamente Físico por regra de negócio —, modelar essa relação por herança direta (ImportadoFisico extends ProdutoFisico) não gera o mesmo problema de explosão combinatória.
NotaTeste 4 — A Proibição da Herança Múltipla em Java (Diamante)
  • □ Mesmo que duas superclasses candidatas a uma herança múltipla não compartilhem nenhum método com o mesmo nome, ainda pode haver ambiguidade se ambas herdarem, por caminhos diferentes, de uma mesma superclasse comum mais alta na árvore.
  • □ Java resolve completamente o Problema do Diamante ao permitir que uma classe implemente múltiplas interfaces, porque interfaces nunca podem gerar nenhum tipo de conflito entre implementações.
  • □ Se Java permitisse class FisicoImportado extends ProdutoFisico, ProdutoImportado, e as duas superclasses declarassem um atributo protected com o mesmo nome mas valores diferentes, o compilador precisaria de alguma regra de desempate para decidir qual valor prevalece em cada acesso.
  • □ A ausência de herança múltipla de classes em Java é a razão fundamental pela qual a linguagem introduziu métodos default em interfaces, permitindo simular parte do reaproveitamento de comportamento sem os riscos de ambiguidade de estado.
NotaTeste 5 — Composição, Agregação e Associação
  • □ Se o objeto “Todo” de uma relação de Agregação for destruído, mas os objetos “Parte” continuarem acessíveis e utilizáveis por outras partes do sistema, essa é uma evidência de que a relação realmente era uma Agregação, e não uma Composição estrita.
  • □ Se um CartaoDeCredito só pudesse ser instanciado a partir de um Cliente já existente e fosse automaticamente destruído quando esse Cliente fosse removido do sistema, a relação entre eles deixaria de se encaixar na definição de Associação apresentada na aula.
  • □ Toda relação de composição de objetos em que um objeto guarda uma referência a outro deve, por definição, ser classificada como Agregação, nunca como Associação.
  • □ Uma classe Equipe que mantém uma lista de Jogador, onde os jogadores continuam existindo no sistema (podem ser transferidos para outra equipe) mesmo se a Equipe for extinta, é um exemplo de Agregação e não de Composição estrita.
NotaTeste 6 — Quase-Decomponibilidade e Sistemas Modulares
  • □ Segundo a teoria de Herbert Simon, um sistema perfeitamente decomponível — onde não existe absolutamente nenhuma dependência entre os subsistemas — ainda seria considerado estável e evolutivo, mesmo sem nenhuma coesão interna em cada peça.
  • □ Um monólito de software onde módulos de autenticação, pagamento e notificação compartilham as mesmas variáveis globais e estruturas de dados internas viola o princípio de quase-decomponibilidade, mesmo que a interface externa do sistema pareça organizada em módulos.
  • □ Se a API dos Correios mudasse e o sistema estivesse estruturado por herança (Pedido estendendo uma classe LogisticaCorreios), a substituição da lógica de logística exigiria uma intervenção estruturalmente mais invasiva do que se o sistema estivesse estruturado por composição.
  • □ A resiliência de um sistema modular decorre exclusivamente do número de classes em que ele foi dividido, independentemente de como essas classes se comunicam entre si.
NotaTeste 7 — Identidade vs. Papel: a Falha da Herança no Checkout
  • □ Se um sistema garantisse que o meio de pagamento de um Pedido nunca muda depois de criado (é definido uma única vez, na instanciação, e nunca mais alterado), o problema do aprisionamento estático deixaria de ser um argumento decisivo contra modelar PedidoPix como subclasse de Pedido.
  • □ Colocar toda a lógica de processamento de Pix, Cartão e Boleto dentro de um único método processarPagamento() do Pedido, usando if/else, melhora a coesão do sistema porque centraliza toda a responsabilidade de pagamento em um único lugar.
  • □ Um sistema de streaming que cria classes AssinaturaMensal extends Usuario e AssinaturaAnual extends Usuario para representar o plano de cobrança atual do usuário comete o mesmo erro de confundir identidade com papel visto no PedidoPix.
  • □ Se o meio de pagamento de um Pedido fosse tratado como um papel (uma referência substituível) em vez de uma identidade fixada por herança, um cliente poderia trocar de Pix para Cartão no último segundo do checkout sem que o objeto Pedido precisasse ser recriado.
NotaTeste 8 — O Padrão Strategy e o Princípio Aberto/Fechado
  • □ Se uma única implementação concreta de Pagavel existisse no sistema (por exemplo, apenas Pix), o padrão Strategy ainda traria benefício arquitetural relevante, mesmo sem nenhuma estratégia alternativa em uso.
  • □ Se o Pedido chamasse diretamente new Pix() dentro do método processarPagamento(), em vez de receber uma instância de Pagavel injetada, adicionar um novo meio de pagamento ainda seria possível sem modificar a classe Pedido.
  • □ O Princípio Aberto/Fechado, quando aplicado ao Strategy, significa que a classe Pedido nunca mais poderá ser modificada por nenhum motivo, incluindo correção de bugs em sua própria lógica de orquestração.
  • □ Um sistema de cálculo de frete que aceita diferentes transportadoras (Correios, Transportadora Privada, Retirada em Loja) através de uma interface comum CalculadoraFrete, injetada no objeto Pedido, aplica a mesma topologia Context/Strategy/ConcreteStrategy vista no processamento de pagamento.