Herança: DNA, Fragilidade e Template Method

Aula 8 — Programação Orientada a Objetos

Autor

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

Data de Publicação

30 de agosto de 2026

Slides Lista de aulas

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:

  1. O que muda quando um filho não apenas assina o contrato do pai, mas incorpora fisicamente sua estrutura?
  2. Por que uma alteração “inocente” na classe base pode quebrar um filho que nunca foi tocado?
  3. Como saber se uma herança é uma especialização legítima, ou apenas reuso de código disfarçado?
  4. 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

DicaPor que “o gerenciador usa uma lista, não é uma lista” é o critério certo para decidir entre herança e composição?

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 ListaDeContatos expõ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 retorna null, gerando NullPointerException longe da causa.
  • O problema do super: esquecer super.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

DicaPor que não existe uma forma de um filho “recusar” um atributo herdado, mas existe @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

DicaSe o 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 fosse final, 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:

  1. 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.
  2. A alteração inocente: invariantes invisíveis (ordem, retorno, super) vivem só na lógica do pai, e o compilador não as protege.
  3. Herdar ou compor: só herde quando há DNA compartilhado e necessidade de tratamento polimórfico — nunca por conveniência de reuso.
  4. 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.

Exercícios Soluções