Exercícios — Substituição e Falha: o Princípio de Liskov e a Taxonomia de Exceções

Aula 9 — Programação Orientada a Objetos

Autor

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

Aula Soluções

Questões discursivas

  1. Um método sacar(double valor) em ContaBancaria não impõe restrição de horário. Uma subclasse ContaInvestimento sobrescreve esse método lançando uma RuntimeException caso o saque ocorra fora do horário comercial — uma regra que não existe na base. Explique, em termos das regras de contrato do LSP, exatamente qual regra é violada e por quê.

  2. Explique a diferença entre Early Binding e Late Binding usando o protocolo de despacho da VTable (os quatro passos vistos na aula). Em qual desses passos a JVM realmente decide qual código executar, e por que essa decisão não poderia ser tomada só com a informação disponível ao compilador?

  3. Um desenvolvedor precisa decidir se uma falha de comunicação com um gateway de pagamento externo deve ser modelada como uma exceção Checked ou Unchecked. Argumente a favor de uma das duas escolhas, e explique por que a escolha errada geraria um problema de design (não só um erro de sintaxe).

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 — Late Binding e VTable
  • □ Se um método for marcado como static, a JVM ainda assim consulta a VTable da classe real do objeto para decidir qual código executar.
  • □ Em um sistema que usasse Early Binding em vez de Late Binding, adicionar uma nova subclasse de Pagavel exigiria recompilar o código que já chamava métodos de Pagavel.
  • □ A VTable consultada numa chamada p.pagamentoConfirmado(id) é determinada pela classe declarada da variável p, não pela classe do objeto apontado por ela.
  • □ Se duas classes não relacionadas por herança ou interface comum implementarem um método com a mesma assinatura, a JVM ainda assim pode fazer Late Binding entre elas ao chamar esse método através de uma referência comum.
NotaTeste 2 — Sobrescrita como Contrato (@Override)
  • □ Se um método sobrescrito muda a semântica do método original (por exemplo, de “validar” para “excluir o registro”), o compilador não impede essa mudança, mesmo com @Override presente.
  • □ Se a classe pai remover o método que uma subclasse originalmente sobrescrevia (renomeando-o), o compilador acusa erro na subclasse graças à anotação @Override.
  • □ Um método sobrescrito que devolve um subtipo mais específico do tipo de retorno original (covariância de retorno) ainda é uma sobrescrita válida.
  • @Override transforma automaticamente qualquer alteração de comportamento do método em uma violação de compilação, independente da assinatura.
NotaTeste 3 — LSP: a Definição Formal
  • □ Se \(\phi(x)\) representa que “o método nunca retorna null”, e existe um subtipo \(S\) de \(T\) cujas instâncias podem retornar null nesse mesmo método, a fórmula \(\forall x{:}T,\phi(x) \implies \forall y{:}S,\phi(y)\) é violada por esse subtipo.
  • □ O LSP é satisfeito sempre que o subtipo compila corretamente contra a interface do tipo base, independentemente do comportamento em tempo de execução.
  • □ Um sistema de testes automatizados que roda a mesma bateria de testes contra Pagavel, trocando só a implementação concreta injetada, está aplicando na prática a intuição do “cliente cego” usada para explicar o LSP.
  • □ Se um subtipo \(S\) satisfaz o LSP em relação a \(T\), isso implica necessariamente que \(T\) também satisfaz o LSP em relação a \(S\).
NotaTeste 4 — Pré-condições no LSP
  • □ Se a base aceita valores de 0 a 1000, e uma subclasse aceita valores de -1000 a 2000 (um domínio maior), essa subclasse respeita a regra de pré-condições do LSP.
  • □ Uma subclasse pode enfraquecer uma pré-condição do pai (aceitar mais entradas do que ele aceitava) sem violar o LSP.
  • □ Se um método do pai não documenta explicitamente nenhuma pré-condição, uma subclasse está livre para adicionar qualquer restrição de entrada sem risco de violar o LSP.
  • □ Fortalecer uma pré-condição é equivalente, na prática, a remover funcionalidade que o cliente já esperava ter disponível através do tipo base.
NotaTeste 5 — Pós-condições e Invariantes no LSP
  • □ Se o pai promete retornar um objeto nunca nulo, e o filho retorna um objeto “vazio” (um objeto real, mas sem dado útil) em vez de null, isso ainda pode violar a regra de pós-condições dependendo do que o cliente faz com o resultado.
  • □ Uma subclasse que sempre retorna um resultado mais específico e mais completo do que o pai prometia está fortalecendo a pós-condição, o que é permitido pelo LSP.
  • □ Uma subclasse que herda um atributo protected responsável por uma invariante crítica pode alterar esse atributo livremente, desde que não sobrescreva nenhum método do pai.
  • □ Preservar uma invariante herdada significa apenas que o valor do atributo nunca muda depois da construção do objeto.
NotaTeste 6 — O Paradoxo Círculo-Elipse
  • □ O paradoxo Círculo-Elipse só aparece porque Elipse expõe um método mutável (setAxes) que permite alterar os eixos de forma independente depois da criação do objeto.
  • □ Se Circulo e Elipse fossem ambos imutáveis (sem nenhum setAxes, só construtores), a herança entre eles deixaria de gerar a mesma violação de LSP.
  • □ O paradoxo prova que herança geométrica sempre deve ser evitada em qualquer sistema orientado a objetos, mesmo quando os objetos são imutáveis.
  • □ Substituir a herança Circulo extends Elipse por uma interface comum FormaGeometrica, sem o método setAxes, é uma forma válida de evitar o paradoxo.
NotaTeste 7 — Contratos de Exceção e o LSP
  • □ Uma subclasse que lança uma exceção verificada (checked) não declarada pelo método do pai obriga o código cliente já existente a ser recompilado ou reescrito para tratar essa nova exceção.
  • □ Lançar UnsupportedOperationException para “pular” um método herdado que a subclasse não quis implementar é uma prática recomendada quando a subclasse é uma especialização legítima.
  • □ Se Pagavel.criarCobranca() não declara nenhuma exceção, uma subclasse que lança uma RuntimeException inesperada em condições normais de uso ainda pode violar o espírito do LSP, mesmo sem quebrar a compilação.
  • □ Um subtipo que lança um subtipo mais específico de uma exceção já esperada pelo pai (por exemplo, SaldoInsuficienteException extends PagamentoException, quando o pai já declarava lançar PagamentoException) respeita o contrato de exceção do LSP.
NotaTeste 8 — Taxonomia do Erro e Captura Polimórfica
  • □ Organizar exceções como uma hierarquia de tipos (em vez de uma lista de mensagens de texto) permite que o mesmo bloco catch trate corretamente subtipos de exceção criados depois que aquele código já foi escrito.
  • □ Se um bloco catch (PagamentoException e) vier antes de um bloco catch (CartaoExpiradoException e) no mesmo try, sendo CartaoExpiradoException subtipo de PagamentoException, o código não compila.
  • □ Capturar Exception de forma genérica no nível mais alto do sistema é sempre uma boa prática, pois garante que nenhum erro, de nenhum tipo, escape sem tratamento.
  • □ Tratar o erro como uma transição de estado prevista (por exemplo, de “Processando” para “Falha de Pagamento”) é incompatível com o uso de uma hierarquia de exceções para organizar essas falhas.
NotaTeste 9 — Checked vs. Unchecked e Wrapping
  • □ Se um método interno de baixo nível lança SQLException e essa exceção se propaga sem tradução até um controlador de alto nível, o CheckoutController passa a depender de um detalhe de implementação (o uso de SQL) que deveria estar oculto.
  • □ Reembalar uma exceção técnica numa exceção de domínio, descartando a exceção original em vez de passá-la como causa, preserva completamente a capacidade de depuração técnica do erro.
  • □ Um valor de entrada inválido detectado logo no início de um método é um exemplo típico de erro que deve ser tratado com uma exceção Unchecked, seguindo o princípio Fail-Fast.
  • □ Capturar uma IllegalArgumentException lançada por uma validação de contrato, só para permitir que a execução continue silenciosamente com um valor padrão, resolve o problema de origem que gerou a exceção.