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.
3.1 Quando Herdar
Escreva sua resposta e compare com um colega antes de avançar (2 min).
- □ Herdar apenas para reaproveitar métodos, sem uma relação “É-UM” real, é um design frágil.
- □
GerenciadorDeCobrancas extends ListaDeContatosexpõe métodos do pai que talvez nunca devessem ser públicos no filho. - □ Uma especialização legítima precisa compartilhar o DNA do pai e ser tratada polimorficamente como ele.
- □ Uma subclasse que não adiciona comportamento nem refina invariantes ainda agrega valor arquitetural por existir.
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.
5.1 Classes Abstratas e Herança de Estado
@Override para recusar/substituir um método herdado?
Escreva sua resposta e compare com um colega antes de avançar (2 min).
- □ Uma classe abstrata pode ter métodos concretos e métodos abstratos ao mesmo tempo.
- □ O compilador permite instanciar diretamente uma classe abstrata com
new, desde que todos os métodos existam. - □ Herdar um atributo é uma alocação de memória física compulsória em cada instância do filho.
- □ Não existe, em Java, uma sintaxe para um filho “recusar” um campo herdado do pai.
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.
6.1 Template Method e Inversão de Controle
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).
- □ Se
realizarPagamento()não fossefinal, uma subclasse poderia pular a validação ou o registro de log. - □ O gancho
criarCobrancaEspecifica()é o único ponto que cada subclasse precisa conhecer para se especializar. - □ No Template Method, é o filho quem decide a ordem em que validação, especialização e log acontecem.
- □ A Inversão de Controle aqui significa que a base chama o filho, e não o contrário.
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).