Soluções — 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 Exercícios

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.
Dica(Resposta) Teste 1 — Fusão Estrutural e o Custo da Herança de Estado
  • ✔ Verdadeiro — A alocação de memória para um campo declarado na classe base é incondicional — a JVM reserva espaço para cada atributo herdado em toda instância da linhagem, independentemente de o código da subclasse jamais ler ou escrever naquele campo. “Nunca acessar” é uma decisão de código; “não alocar” exigiria que o campo simplesmente não existisse naquela hierarquia — é exatamente o fenômeno da Subclasse Gorda.
  • ✗ Falso — É a “Ilusão da Produtividade” às avessas: eliminar duplicação de código não é o mesmo que reduzir acoplamento — na verdade, herdar sem relação de identidade real aumenta o acoplamento estrutural (a subclasse passa a depender de toda a superfície pública e das mudanças futuras do construtor/estrutura da base), trocando um problema barato de resolver (duplicação) por um caro (acoplamento estrutural rígido).
  • ✔ Verdadeiro — É exatamente o argumento de “Encapsulamento Preservado” da Era da Composição: por composição, PedidoInternacional só interage com Pedido através de seus métodos públicos, então qualquer refatoração interna de Pedido (como o formato de armazenamento de itens) fica oculta, desde que a interface pública seja mantida. Por herança, o acesso a detalhes via protected cria justamente o tipo de dependência que uma mudança “inocente” na base pode quebrar sem aviso.
  • ✔ Verdadeiro — É a mesma estrutura de erro transportada para outro domínio: herdar por conveniência de método (ganhar calcularHorasTrabalhadas() de graça) sem que exista a necessidade real de tratar FuncionarioTerceirizado como um Funcionario em todo lugar do sistema (por exemplo, na geração da folha de pagamento) é exatamente a “Ilusão da Produtividade” — reuso de código sem relação de identidade genuína.
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.
Dica(Resposta) Teste 2 — O Peso Físico na Heap: a Subclasse Gorda
  • ✔ Verdadeiro — É a mesma estrutura do exemplo Ebook/ProdutoBase transportada para outro domínio: um atributo semanticamente relevante só para uma parte da hierarquia (filmes de cinema), declarado na base, é alocado para toda instância descendente — inclusive as que nunca usarão aquele dado.
  • ✔ Verdadeiro — A Subclasse Gorda é especificamente sobre atributos de estado desperdiçados na Heap — se a base não declara nenhum campo, não há nada para alocar desnecessariamente por causa dela, independentemente da profundidade da hierarquia. O fenômeno pode ainda existir por causa de atributos declarados em outras classes intermediárias, mas não por causa dessa base específica sem estado.
  • ✗ Falso — Confunde dois problemas distintos: visibilidade (quem pode acessar o campo) e alocação de memória (se o campo existe fisicamente na instância). Trocar protected por private restringe o acesso direto pelas subclasses, mas não impede que a JVM aloque espaço para aquele campo em toda instância da linhagem — o campo continua existindo fisicamente, só fica inacessível por nome de fora da própria classe.
  • ✔ Verdadeiro — É a correção por composição sugerida na aula: ao mover os campos logísticos para uma classe separada, só as classes que realmente compõem uma instância de DadosLogisticosFisicos (produtos físicos) pagam o custo de memória associado — um Ebook, que nunca precisaria dessa composição, deixa de carregar esses bytes desperdiçados.
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.
Dica(Resposta) Teste 3 — Explosão Combinatória e Eixos Ortogonais
  • ✔ Verdadeiro — O crescimento é multiplicativo: \(2^3 = 8\) com três eixos, \(2^4 = 16\) com quatro — cada eixo binário adicional dobra o número de combinações possíveis, o mesmo padrão exponencial discutido na aula.
  • ✗ Falso — A explosão combinatória é uma propriedade estrutural da tentativa de modelar múltiplos eixos independentes por herança — não depende da natureza do eixo (técnico ou de negócio). “Categorias Promocionais” é um exemplo de terceiro eixo que dispararia a mesma explosão; qualquer eixo verdadeiramente ortogonal aos já existentes produz o mesmo efeito multiplicativo.
  • ✔ Verdadeiro — Dois eixos binários independentes (Tipo de Veículo × Perfil de Risco) modelados por herança pura geram exatamente \(2 \times 2 = 4\) subclasses-folha, com o mesmo risco de duplicação de regra e mesma inviabilidade de escalar para um terceiro eixo — a estrutura do problema é idêntica à do e-commerce, só o domínio muda.
  • ✔ Verdadeiro — A explosão combinatória nasce especificamente da ortogonalidade dos eixos — cada combinação das opções de um eixo com as do outro sendo um cenário de negócio válido e necessário. Se uma regra de negócio elimina metade das combinações a priori (todo Importado já é Físico), o espaço de casos deixa de crescer multiplicativamente, e uma única subclasse extra pode bastar sem reabrir o problema.
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.
Dica(Resposta) Teste 4 — A Proibição da Herança Múltipla em Java (Diamante)
  • ✔ Verdadeiro — É a forma clássica do Problema do Diamante: se ProdutoFisico e ProdutoImportado herdassem ambos, por caminhos distintos, de uma mesma classe Produto, uma hipotética FisicoImportado herdaria duas “cópias” conceituais do estado de Produto através de cada caminho — ambiguidade que existe independentemente de haver ou não colisão de nomes de método entre ProdutoFisico e ProdutoImportado diretamente.
  • ✗ Falso — Interfaces com métodos default podem gerar conflito: se duas interfaces implementadas pela mesma classe declararem um método default com a mesma assinatura, o compilador exige que a classe resolva explicitamente a ambiguidade (sobrescrevendo o método). Múltiplas interfaces evitam o problema mais grave — ambiguidade de estado (campos duplicados) — mas não eliminam toda possibilidade de conflito de comportamento.
  • ✔ Verdadeiro — É exatamente a ambiguidade de estado que motiva a proibição: sem uma regra de desempate clara (qual desconto, de qual superclasse, um acesso this.desconto dentro de FisicoImportado deveria retornar?), o comportamento do programa se tornaria imprevisível ou exigiria uma sintaxe de desambiguação explícita a cada acesso — complexidade que o Java optou por nunca introduzir para herança de classes.
  • ✔ Verdadeiro — Métodos default permitem que uma interface forneça uma implementação padrão de comportamento, algo que só herança de classes oferecia antes — mas interfaces nunca carregam estado (campos de instância), então a ambiguidade mais perigosa do Diamante (conflito de campos duplicados) simplesmente não tem como ocorrer nesse mecanismo, mesmo quando um conflito de método default precisa ser resolvido explicitamente.
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.
Dica(Resposta) Teste 5 — Composição, Agregação e Associação
  • ✔ Verdadeiro — O critério que distingue Agregação de Composição estrita é justamente a sobrevivência da parte à destruição do todo — se as partes continuam vivas e utilizáveis, a relação não tinha a posse exclusiva de vida característica da Composição estrita, encaixando-se na definição de Agregação (Todo-Parte sem dono absoluto da existência da parte).
  • ✔ Verdadeiro — A definição de Associação usada na aula exige vidas completamente autônomas — nenhum dos dois lados depende da existência do outro. Amarrar a criação e a destruição do cartão ao ciclo de vida específico de um cliente introduz exatamente a dependência que a Associação, por definição, não tem; a relação descrita passaria a se parecer com Composição estrita (a “parte” só existe através do “todo” e morre com ele), não mais com a colaboração livre entre Cliente e CartaoDeCredito vista na aula.
  • ✗ Falso — Guardar uma referência é a mecânica comum a Agregação e Associação — a diferença entre as duas não está em “ter uma referência”, mas no grau de autonomia dos ciclos de vida envolvidos. Classificar toda referência como Agregação ignora exatamente a distinção que a aula estabeleceu (Todo-Parte com posse parcial vs. colaboração totalmente livre).
  • ✔ Verdadeiro — É a mesma estrutura do CarrinhoDeCompras/Produto aplicada a outro domínio: o “Todo” (Equipe) agrupa as partes (Jogador), mas a extinção do todo não implica a destruição das partes — elas seguem existindo e podem, inclusive, ser realocadas, exatamente o comportamento que caracteriza Agregação.
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.
Dica(Resposta) Teste 6 — Quase-Decomponibilidade e Sistemas Modulares
  • ✗ Falso — A “quase-decomponibilidade” de Simon exige ambos os lados da comparação: elos internos fortes (coesão) e dependências externas fracas — não apenas a ausência de dependência externa. Um sistema sem nenhuma coesão interna em suas peças não seria estável mesmo com zero acoplamento entre elas, porque cada peça, isoladamente, já seria frágil e desorganizada; e um sistema com dependência zero entre partes também deixaria de funcionar como um sistema coeso, incapaz de colaborar para um objetivo comum.
  • ✔ Verdadeiro — A quase-decomponibilidade é sobre a força relativa dos elos internos vs. externos de fato, não sobre a aparência superficial da organização do código. Compartilhar variáveis globais e estruturas internas entre módulos cria dependências externas fortes exatamente onde deveriam ser fracas — a “modularidade” nesse caso é só uma fachada de nomenclatura, não uma propriedade estrutural real do sistema.
  • ✔ Verdadeiro — Por herança, a lógica de logística estaria fundida na própria identidade de Pedido — substituir a transportadora exigiria alterar a hierarquia de tipos de Pedido (potencialmente afetando todo código que dependia daquele tipo). Por composição, basta trocar a implementação da interface de logística injetada, mantendo Pedido e seu tipo intocados — exatamente o argumento da analogia do eletrodoméstico modular.
  • ✗ Falso — Simplesmente dividir um sistema em muitas classes não garante resiliência se essas classes permanecerem fortemente acopladas entre si (por exemplo, todas dependendo de detalhes internas umas das outras). A quase-decomponibilidade — e a resiliência que dela decorre — depende da qualidade das interações (elos externos fracos, via contratos estáveis), não apenas da quantidade de divisões.
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.
Dica(Resposta) Teste 7 — Identidade vs. Papel: a Falha da Herança no Checkout
  • ✔ Verdadeiro — O aprisionamento estático é especificamente sobre a impossibilidade de trocar o meio de pagamento depois de instanciado o objeto — se essa troca nunca fosse necessária por regra de negócio, esse argumento específico perderia força (embora o problema de identidade-vs-papel e o risco de explosão da árvore, discutidos na aula, ainda permaneçam como razões independentes para preferir composição).
  • ✗ Falso — “Centralizar em um único método” não é o mesmo que “coeso” — pelo contrário, o método passa a acumular responsabilidades de domínios completamente diferentes (antifraude de cartão, geração de QR Code Pix, código de barras de boleto), o oposto de coesão. É o code smell clássico discutido na aula: Pedido passa a conhecer detalhes de infraestrutura alheios ao seu domínio.
  • ✔ Verdadeiro — O plano de cobrança de um usuário, assim como o meio de pagamento de um pedido, é um papel temporário (pode mudar de mensal para anual), não uma característica que define permanentemente o que o Usuario é. Modelar essa variação por herança gera o mesmo aprisionamento estático: trocar de plano exigiria trocar a classe do objeto, o que Java não permite em tempo de execução.
  • ✔ Verdadeiro — É exatamente a solução que o padrão Strategy oferece: tratando o meio de pagamento como uma referência substituível (Pagavel) injetada via set, a troca de Pix para Cartão se resume a trocar o objeto apontado pela referência — o Pedido permanece o mesmo objeto na Heap durante toda a operação.
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.
Dica(Resposta) Teste 8 — O Padrão Strategy e o Princípio Aberto/Fechado
  • ✔ Verdadeiro — Mesmo com uma única implementação hoje, o Strategy já entrega encapsulamento (a lógica de Pix fica isolada e testável sem instanciar Pedido) e desacoplamento (Pedido não conhece detalhes internos de Pix) — os dois primeiros benefícios da composição discutidos na aula não dependem de já existir mais de uma estratégia em uso; a “intercambiabilidade” é um benefício adicional, não o único.
  • ✗ Falso — Instanciar a classe concreta diretamente dentro de Pedido reintroduz exatamente o acoplamento que a injeção de dependência elimina — qualquer novo meio de pagamento exigiria editar processarPagamento() para decidir quando instanciar a classe nova, violando o Princípio Aberto/Fechado que a interface Pagavel e a injeção via set/construtor foram desenhadas para garantir.
  • ✗ Falso — O OCP é especificamente sobre não precisar modificar Pedido para acomodar uma nova variação de comportamento intercambiável (um novo meio de pagamento) — não uma proibição geral de qualquer edição futura. Corrigir um bug genuíno na lógica de orquestração de Pedido (por exemplo, uma condição mal escrita em finalizarPedido()) é manutenção normal do código, categoricamente diferente de precisar reabrir o arquivo toda vez que a loja aceita um novo meio de pagamento.
  • ✔ Verdadeiro — É a mesma topologia formal do GoF aplicada a um eixo de variação diferente do mesmo domínio: Pedido (Context) mantém uma referência a CalculadoraFrete (Strategy), e cada transportadora (Correios, Transportadora Privada, Retirada em Loja) é uma ConcreteStrategy intercambiável — a estrutura é idêntica à de Pagavel/Pix/Cartao/Boleto, só o algoritmo variável muda (cálculo de frete em vez de processamento de pagamento).