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.

8 Exercícios

8.1 Questões discursivas

  1. 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?

  2. Um desenvolvedor faz GerenciadorDeCobrancas extends ListaDeContatos só 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.

  3. Explique o padrão Template Method usando o exemplo de realizarPagamento. Por que o método principal precisa ser final, 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).

NotaHerança como Identidade (“É-UM”)
  • □ 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.
  • □ Num sistema onde Cartao herda de MeioPagamentoBase, e Cartao também guarda uma referência a um objeto Endereco como atributo, o acoplamento entre Cartao e Endereco é tão forte quanto o acoplamento entre Cartao e MeioPagamentoBase.
  • □ Herança de atributos significa que o filho fisicamente possui, na memória, os campos definidos no pai.
NotaO Modificador protected
  • protected permite que subclasses manipulem o estado herdado sem expô-lo ao resto do sistema.
  • protected é equivalente, em termos de encapsulamento, a private.
  • □ Se MeioPagamentoBase alterar de protected para private o atributo idCobranca, todo Cartao, Pix e Boleto que acessava esse atributo diretamente (sem usar um método) para de compilar.
  • □ Sem final no método de transição, uma subclasse pode ignorar a validação do pai livremente.
NotaO Problema da Classe Base Frágil
  • □ Se MeioPagamentoBase.processarLote() for reescrito para não chamar mais processarPagamento() internamente (processando os valores diretamente, sem delegar), o Cartao que sobrescreve ambos os métodos passará a contar corretamente, sem nenhuma mudança no código do Cartao.
  • □ O bug de contagem dupla em processarLote surge 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.
NotaHerança por Conveniência vs. Especialização Legítima
  • □ Uma classe RelatorioFinanceiro que precisa apenas do método formatarMoeda() de uma classe utilitária FormatadorNumerico deveria herdar de FormatadorNumerico para ganhar acesso a esse método.
  • GerenciadorDeCobrancas extends ListaDeContatos é um exemplo de especialização legítima.
  • □ Uma classe RegistroDeAuditoria que estende MeioPagamentoBase só para reaproveitar o atributo idCobranca, mas nunca é armazenada numa List<Pagavel> nem passada onde se espera um Pagavel, ainda é uma especialização legítima, desde que implemente corretamente criarCobranca().
  • □ A composição é a alternativa correta quando a relação real é “usa um”, não “é um”.
NotaInvariantes Invisíveis
  • □ Se MeioPagamentoBase documentar explicitamente, em um comentário Javadoc, que validar() deve sempre rodar antes de processar(), essa dependência de ordem deixa de ser uma invariante invisível.
  • □ Se o pai espera que um método nunca retorne null, uma subclasse pode alterar essa semântica livremente sem risco.
  • □ Se Cartao.alterarStatus() sobrescrever o método do pai e esquecer de chamar super.alterarStatus(), mas ainda assim atualizar corretamente o campo status com sua própria lógica, o objeto nunca fica num estado inconsistente.
  • □ Adicionar a anotação @Deprecated a um método do pai é suficiente para transformar uma invariante antes invisível (como um pressuposto de ordem) numa restrição verificada pelo compilador.
NotaA Base como Guardiã
  • □ Atributos que regem invariantes críticos devem ser private na classe base, não protected.
  • □ Métodos final impedem 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 torna a integridade do sistema dependente do acerto individual de cada subclasse.
NotaClasses Abstratas
  • □ 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 abstract de uma classe abstrata representa a parte “sem motor” da máquina semi-acabada.
NotaHerança de Estado vs. Comportamento
  • □ Uma classe Pix que implementa Pagavel (mas não estende nenhuma classe) e nunca armazena idCobranca como atributo, gerando-o sob demanda dentro do próprio método getIdCobranca(), ainda cumpre corretamente o contrato da interface.
  • □ Se MeioPagamentoBase tiver um atributo logsInternos que o Pix nunca usa, é possível, em Java, fazer com que instâncias de Pix simplesmente não aloquem espaço para esse campo na Heap.
  • □ 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 String para UUID, por exemplo) pode exigir revisar todas as subclasses.
NotaTemplate Method: o Esqueleto
  • □ O método principal do Template Method costuma ser marcado como final para travar a ordem dos passos.
  • □ Se MeioPagamentoBase tivesse dois métodos-gancho abstratos (criarCobrancaEspecifica() e validarRegrasEspecificas()), uma subclasse poderia implementar apenas um dos dois e ainda assim compilar normalmente.
  • □ 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.
NotaInversão de Controle e o Princípio de Hollywood
  • □ Se um framework de testes chama automaticamente o método setUp() de uma classe de teste antes de cada teste, sem que o desenvolvedor precise chamá-lo manualmente, esse framework está aplicando o mesmo Princípio de Hollywood do Template Method.
  • □ Um script procedural que lê um arquivo, chama uma função de parsing de uma biblioteca, e depois decide o que fazer com o resultado, já está aplicando Inversão de Controle, porque delega parte do trabalho para código de terceiros (a biblioteca).
  • □ No Template Method, é o filho quem decide quando delegar a execução de volta para o pai.
  • □ Um Template Method com um único método-gancho, mas que impõe quinze passos obrigatórios e inalteráveis entre a validação e a chamada do gancho, seria um exemplo do risco de “camisa de força” mencionado na aula, mesmo respeitando a sintaxe do padrão corretamente.
NotaVantagens e Riscos do Template Method
  • □ Se uma nova subclasse Cripto for adicionada ao sistema um ano depois da aula, sem que o desenvolvedor releia a documentação do Template Method, o passo de registrarLog() ainda vai rodar corretamente para ela.
  • □ Se Pix, Boleto e Cartao cada um sobrescrevesse seu próprio método realizarPagamento() completo (em vez de usar o esqueleto da base), mudar a regra de log exigiria editar três lugares em vez de um.
  • □ 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.
NotaSíntese: Herança como Ferramenta de Especialização
  • □ Herança bem usada exige DNA compartilhado real, não apenas o desejo de reaproveitar código.
  • □ Uma subclasse PixPremium que estende Pix apenas para renomear o método criarCobranca() para criarCobrancaPremium(), sem adicionar nenhum campo ou comportamento novo, e mantendo o mesmo contrato, é uma especialização legítima porque ainda compartilha o DNA do pai.
  • □ Blindar a base (atributos private, métodos final, ganchos abstract) 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.