Herança: DNA, Fragilidade e Template Method
Aula 8 — Programação Orientada a Objetos
1 Proposta da Aula
Até aqui, tratamos de como objetos se comunicam através de interfaces — um papel social, o que o objeto faz. Existe outro mecanismo de compartilhamento em Orientação a Objetos, mais antigo e mais poderoso: a Herança, que define o que um objeto é. É o acoplamento mais forte da OO — e, por isso, também o mais perigoso quando mal utilizado.
O roteiro, em quatro perguntas:
- O que muda quando um filho não apenas assina o contrato do pai, mas incorpora fisicamente sua estrutura?
- Por que uma alteração “inocente” na classe base pode quebrar um filho que nunca foi tocado?
- Como saber se uma herança é uma especialização legítima, ou apenas reuso de código disfarçado?
- Como uma classe pode garantir que todos os seus filhos sigam a mesma sequência de passos, sem abrir mão da especialização?
2 Herança: do Contrato à Identidade
Se dizemos que Cartao estende MeioPagamentoBase, afirmamos para o compilador e para a arquitetura que qualquer operação válida para a base é válida para o cartão — a relação “É-UM”. É o acoplamento vitalício: o filho não apenas assina o contrato do pai, ele assume sua história e estrutura.
public abstract class MeioPagamentoBase implements Pagavel {
protected String idCobranca; // Estado compartilhado
public boolean pagamentoConfirmado(String id) {
return id.equals(this.idCobranca);
}
}
// O Cartao agora "E UM" MeioPagamentoBase
public class Cartao extends MeioPagamentoBase {
private double limite;
@Override
public String criarCobranca(double valor) {
this.idCobranca = "CARD-" + UUID.randomUUID();
return this.idCobranca;
}
}O protected permite que os filhos manipulem o estado herdado sem expô-lo ao resto do sistema — mas é apenas um “encapsulamento de linhagem”: ainda é um vazamento de detalhes de implementação, só que restrito à família. Herança de atributos é incorporação física: quando um Cartao é instanciado, a Heap contém tanto os atributos definidos localmente (limite) quanto os herdados (idCobranca). O Cartao não “usa” o pai por associação externa — ele o incorporou.
A fragilidade sem final. Se o pai não fechar as portas de escape, sua invariante vira ilusão:
public abstract class MeioPagamentoBase {
protected String status = "CRIADO";
// Sem 'final', este metodo e uma mera "sugestao"
public void alterarStatus(String novoStatus) {
if (this.status.equals("CONFIRMADO") && novoStatus.equals("CRIADO")) {
throw new IllegalStateException("Violacao de invariante!");
}
this.status = novoStatus;
}
}
public class Cartao extends MeioPagamentoBase {
@Override
public void alterarStatus(String novoStatus) {
this.status = novoStatus; // ignora a validacao do pai
}
}O Cartao tira vantagem do design aberto e burla a regra. Para blindar a linhagem, o estado deve ser private no pai, e o método de transição, final.
O Problema da Classe Base Frágil. O perigo mais sutil: a subclasse quebra sem violar nada explicitamente, só confiando demais na mecânica interna do pai.
public abstract class MeioPagamentoBase {
public void processarPagamento(double valor) { /* captura do pagamento */ }
public void processarLote(List<Double> valores) {
for (double valor : valores) processarPagamento(valor); // pai chama o proprio metodo
}
}
public class Cartao extends MeioPagamentoBase {
private int totalTransacoes = 0;
@Override
public void processarPagamento(double valor) {
super.processarPagamento(valor);
this.totalTransacoes++;
}
@Override
public void processarLote(List<Double> valores) {
super.processarLote(valores);
this.totalTransacoes += valores.size();
}
}Uma lista de 3 valores deveria resultar em totalTransacoes = 3. Mas super.processarLote() chama processarPagamento() internamente — e, por polimorfismo, o fluxo desvia para a versão sobrescrita do filho, que incrementa de novo. Resultado: 6, contagem duplicada. Se o pai um dia remover essa chamada interna para otimizar, o filho passará a contar 3 de repente — sem que nenhuma linha do próprio código tenha mudado.
3 Quando Herdar, e Quando Não
O erro da herança por conveniência. Se GerenciadorDeCobrancas estende ListaDeContatos só para herdar métodos de array já prontos, isso é um erro grave — o gerenciador não é uma lista, ele apenas usa uma lista. Dois problemas nascem daí: métodos públicos do pai (limpar, remover) vazam automaticamente para o filho; e o compilador aceita o gerenciador em qualquer lugar que espere uma lista, uma incoerência conceitual. A correção é composição: o gerenciador guarda uma referência privada à lista.
Especialização legítima exige dois requisitos simultâneos: compartilhar o DNA essencial do pai, e precisar ser tratada polimorficamente como o tipo base. Pix, Cartao e Boleto são especializações legítimas de MeioPagamentoBase — herdam a maquinaria de auditoria e controle, e estendem com conhecimento próprio (gerar chaves Pix, calcular multa de boleto). Se uma subclasse não adiciona nada — nem comportamento, nem refinamento de invariante —, ela é um “objeto zumbi”: polui a taxonomia sem gerar valor.
Escreva sua resposta e compare com um colega antes de avançar (2 min).
4 O Vínculo de Sangue: Acoplamento e Invariantes Invisíveis
A herança é o acoplamento mais íntimo da OO: o filho conhece e depende das entranhas do pai. Se a base treme, toda a árvore balança — o efeito cascata.
A alteração “inocente”. Imagine reforçar a segurança do idCobranca na base:
public abstract class MeioPagamentoBase {
protected String idCobranca;
// Novo requisito: o ID deve sempre ter prefixo "PUB-"
public void setId(String id) {
if (!id.startsWith("PUB-")) throw new IllegalArgumentException();
this.idCobranca = id;
}
}
public class Pix extends MeioPagamentoBase {
@Override
public String criarCobranca(double valor) {
this.idCobranca = "PIX-" + UUID.randomUUID(); // agora quebra em runtime!
return this.idCobranca;
}
}Pix já existia acessando idCobranca diretamente (via protected), driblando o novo setId(). Ele violou uma regra que nem sabia que existia — a mudança parecia retrocompatível, mas não foi. Este é o Problema da Classe Base Frágil: a base se torna quase impossível de evoluir com segurança.
Três armadilhas de invariante invisível — regras que vivem só na lógica do pai, nunca na assinatura:
- Pressupostos de ordem: o pai assume que o método \(A\) roda antes do \(B\); o filho sobrescreve \(B\) sem saber disso.
- Semântica de retorno: o pai espera que um método nunca retorne
null; o filho sobrescreve e retornanull, gerandoNullPointerExceptionlonge da causa. - O problema do
super: esquecersuper.metodo()deixa o objeto numa “máquina de estado zumbi” — viva na memória, mas com o ciclo de vida lógico quebrado.
5 A Base como Guardiã, e a Classe Abstrata
Para mitigar a fragilidade, o pai assume o controle total: atributos críticos private (não protected), métodos de transição final, e “ganchos” (hooks) protected abstract para os detalhes que só o filho sabe.
public abstract class MeioPagamentoBase implements Pagavel {
private double valor; // privado: o filho nao pode corromper
public final void setValor(double v) { // final: filho nao pode subverter
if (v <= 0) throw new ValorInvalidoException("Invariante violada");
this.valor = v;
}
protected abstract String executarProcessamento(); // o gancho
}Classes Abstratas como máquina semi-acabada. Uma classe abstrata não é só “uma classe que não pode ser instanciada” — é um compromisso arquitetural. Pense num chassi de carro: tem rodas, bancos, suspensão, mas não tem motor. Você não dirige um chassi; a fábrica decide se ele vira CarroEletrico ou Combustao.
public abstract class MeioPagamentoBase implements Pagavel {
protected double valorOriginal;
public MeioPagamentoBase(double valor) { this.valorOriginal = valor; }
public boolean validarValor() { return this.valorOriginal > 0; } // metodo concreto
public abstract String processar(); // o "motor faltante"
}
public class Pix extends MeioPagamentoBase {
public Pix(double valor) { super(valor); }
@Override
public String processar() { return "PIX-QRCODE-" + this.valorOriginal; }
}O compilador age como “inspetor de fábrica”: new MeioPagamentoBase(100.0) nem compila — a máquina está incompleta, seria perigoso operá-la.
Herança de estado vs. comportamento. Herdar comportamento (via interface) é fluido — o filho decide livremente como representar o dado:
public interface Pagavel { String getIdCobranca(); }
public class Pix implements Pagavel {
public String getIdCobranca() { return "PIX-" + UUID.randomUUID(); } // nem precisa guardar!
}Herdar estado (atributos) é uma alocação física compulsória — o filho carrega os campos do pai, precise ou não:
public abstract class MeioPagamentoBase {
protected String idCobranca;
protected LocalDateTime dataCriacao;
protected String logsInternos; // e se o Pix nao precisar de logs?
}Não existe sintaxe para “deserdar” um campo em Java. Se a base carrega logsInternos, todo Pix, Boleto e Cartao carrega esse peso, use ou não. E se um dia descobrirmos que o tipo de idCobranca deveria ser UUID, não String, cada subclasse que o acessa diretamente (via protected) precisa ser revista.
@Override para recusar/substituir um método herdado?
Escreva sua resposta e compare com um colega antes de avançar (2 min).
6 Template Method
Como garantir que todo pagamento siga a mesma sequência de segurança, deixando cada tipo variar só o detalhe técnico?
public abstract class MeioPagamentoBase implements Pagavel {
// O ESQUELETO: metodo final, ninguem muda esta ordem
public final String realizarPagamento(double valor) {
if (!validar(valor)) throw new RuntimeException("Valor invalido");
String id = criarCobrancaEspecifica(valor); // o "buraco" do especialista
registrarLog(id); // passo comum a todos
return id;
}
private boolean validar(double v) { return v > 0; }
private void registrarLog(String id) { System.out.println("Pagamento registrado: " + id); }
protected abstract String criarCobrancaEspecifica(double v); // especializacao obrigatoria
}O pai dita a ordem imutável dos passos; o filho preenche só a lacuna que exige conhecimento especializado. Se cada subclasse implementasse seu próprio realizarPagamento público, nada impediria o Pix de esquecer o log, ou o Boleto de pular a validação — o Template Method centraliza essa responsabilidade no chassi.
Este é o conceito de Inversão de Controle (IoC), resumido no Princípio de Hollywood: “Não nos ligue, nós ligamos para você.” No procedural tradicional, o código do desenvolvedor retém o fluxo e chama bibliotecas quando precisa. No Template Method, é o contrário: o fluxo pertence à superclasse, que decide o momento exato de desviar para o detalhe do filho, retomando o controle logo depois.
Vantagens: previsibilidade (log e validação sempre acontecem, seja Pix ou Cartão); manutenibilidade (mudar a regra de log é mexer num único lugar); limitação do erro (o desenvolvedor da subclasse só precisa saber sua especialidade). O risco: se o esqueleto for mal projetado, vira uma “camisa de força” para os filhos.
Pix pudesse sobrescrever realizarPagamento() inteiro (em vez de só criarCobrancaEspecifica()), o que exatamente o sistema perderia?
Escreva sua resposta e compare com um colega antes de avançar (2 min).
7 Conclusão
Voltando às quatro perguntas da abertura:
- Incorporar em vez de assinar: herança de estado é alocação física compulsória — o filho carrega o pai na memória, não só na promessa.
- A alteração inocente: invariantes invisíveis (ordem, retorno,
super) vivem só na lógica do pai, e o compilador não as protege. - Herdar ou compor: só herde quando há DNA compartilhado e necessidade de tratamento polimórfico — nunca por conveniência de reuso.
- Mesma sequência, filhos diferentes: Template Method, com o fluxo travado (
final) na base e ganchos (abstract) para o filho.
Ponte para a Aula 9
Herança nos dá reuso de estrutura, mas levanta uma pergunta que o “É-UM” sozinho não responde: quando é realmente seguro substituir o pai por um filho em qualquer lugar do sistema? A Aula 9 formaliza essa pergunta com o Princípio da Substituição de Liskov, e trata do outro lado da herança: como lidar, de forma estruturada, com as falhas que a especialização pode introduzir.
8 Exercícios
8.1 Questões discursivas
Explique o Problema da Classe Base Frágil usando o exemplo de
processarLote/processarPagamento. Por que o bug de contagem dupla surge sem que nenhum código tenha sido escrito “errado” isoladamente?Um desenvolvedor faz
GerenciadorDeCobrancas extends ListaDeContatossó para reaproveitar métodos de manipulação de array. Explique por que isso é um erro de design, e como a composição resolveria o mesmo problema sem os riscos.Explique o padrão Template Method usando o exemplo de
realizarPagamento. Por que o método principal precisa serfinal, e o que aconteceria se cada subclasse pudesse sobrescrevê-lo livremente?
8.2 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).
- ( ) A herança estabelece uma relação de identidade mais profunda que a associação — a relação “É-UM”.
- ( ) O filho, ao herdar, assina apenas o contrato do pai, sem incorporar sua estrutura interna.
- ( ) A herança é considerada o acoplamento mais forte disponível na Orientação a Objetos.
- ( ) Herança de atributos significa que o filho fisicamente possui, na memória, os campos definidos no pai.
protected
- ( )
protectedpermite que subclasses manipulem o estado herdado sem expô-lo ao resto do sistema. - ( )
protectedé equivalente, em termos de encapsulamento, aprivate. - ( )
protectedcria um “encapsulamento de linhagem” — ainda vaza detalhes de implementação, só que para a família. - ( ) Sem
finalno método de transição, uma subclasse pode ignorar a validação do pai livremente.
- ( ) Ocorre quando uma alteração aparentemente segura na base quebra silenciosamente subclasses existentes.
- ( ) O bug de contagem dupla em
processarLotesurge porque o polimorfismo desvia a chamada interna para o método sobrescrito do filho. - ( ) Esse tipo de erro costuma ser detectado pelo compilador antes da execução.
- ( ) Invariantes que vivem apenas na lógica implícita do pai são as mais perigosas de se violar sem perceber.
- ( ) Herdar apenas para reaproveitar métodos, sem relação “É-UM” real, é um sinal de design frágil.
- ( )
GerenciadorDeCobrancas extends ListaDeContatosé um exemplo de especialização legítima. - ( ) Uma especialização legítima precisa compartilhar o DNA do pai e ser tratada polimorficamente como ele.
- ( ) A composição é a alternativa correta quando a relação real é “usa um”, não “é um”.
- ( ) Um pressuposto de ordem entre métodos do pai pode ser quebrado silenciosamente por uma subclasse.
- ( ) Se o pai espera que um método nunca retorne
null, uma subclasse pode alterar essa semântica livremente sem risco. - ( ) Esquecer de chamar
super.metodo()pode deixar o objeto numa “máquina de estado zumbi”. - ( ) Invariantes invisíveis não aparecem na assinatura dos métodos, só na lógica interna do pai.
- ( ) Atributos que regem invariantes críticos devem ser
privatena classe base, nãoprotected. - ( ) Métodos
finalimpedem que subclasses sobrescrevam e subvertam a validação da base. - ( ) Métodos-gancho (
protected abstract) delegam ao filho só o detalhe técnico, sem expor o fluxo inteiro. - ( ) Blindar a base dessa forma tona a integridade do sistema dependente do acerto individual de cada subclasse.
- ( ) Uma classe abstrata pode ter métodos concretos, resolvendo parte do comportamento do objeto.
- ( ) O compilador impede a instanciação direta de uma classe abstrata via
new. - ( ) Uma classe abstrata é uma limitação técnica sem nenhum propósito arquitetural além de “não poder instanciar”.
- ( ) O método
abstractde uma classe abstrata representa a parte “sem motor” da máquina semi-acabada.
- ( ) Herdar apenas comportamento (via interface) preserva a liberdade do filho sobre como representar seus dados.
- ( ) Herdar atributos é uma alocação de memória compulsória — o filho carrega o campo, precise ou não.
- ( ) Existe uma sintaxe padrão em Java para um filho “recusar” um atributo herdado que ele não usa.
- ( ) Mudar o tipo de um atributo herdado (de
StringparaUUID, por exemplo) pode exigir revisar todas as subclasses.
- ( ) O método principal do Template Method costuma ser marcado como
finalpara travar a ordem dos passos. - ( ) Os métodos-gancho abstratos são preenchidos individualmente por cada subclasse especializada.
- ( ) Se cada subclasse pudesse sobrescrever o método principal livremente, a ordem de validação e log deixaria de ser garantida.
- ( ) O Template Method reduz a previsibilidade do sistema, pois cada filho decide sua própria sequência de passos.
- ( ) “Não nos ligue, nós ligamos para você” resume a ideia de que o fluxo principal pertence à superclasse.
- ( ) No paradigma procedural clássico, o código do desenvolvedor tipicamente retém o fluxo principal.
- ( ) No Template Method, é o filho quem decide quando delegar a execução de volta para o pai.
- ( ) Um esqueleto de Template Method mal projetado pode se tornar uma “camisa de força” para as subclasses.
- ( ) Garante previsibilidade: passos como validação e log sempre acontecem, independente da subclasse.
- ( ) Facilita a manutenção: mudar a regra de log exige alterar um único lugar (a base).
- ( ) Exige que o desenvolvedor de cada subclasse conheça o fluxo inteiro do algoritmo, não só seu gancho.
- ( ) É um mecanismo de Inversão de Controle: a base chama o filho, não o contrário.
- ( ) Herança bem usada exige DNA compartilhado real, não apenas o desejo de reaproveitar código.
- ( ) Uma subclasse que não adiciona comportamento nem refina invariantes é um “objeto zumbi” na taxonomia.
- ( ) Blindar a base (atributos
private, métodosfinal, ganchosabstract) reduz o risco de Classe Base Frágil. - ( ) O acoplamento gerado pela herança é, em geral, mais fraco do que o gerado por composição simples.