Polimorfismo em Java OOP na prática: substituibilidade e desenho de APIs

Capítulo 8

Tempo estimado de leitura: 9 minutos

+ Exercício

Polimorfismo como ferramenta de desenho de APIs

Na prática, polimorfismo é a capacidade de escrever código que depende de um tipo abstrato (interface ou superclasse) e, ainda assim, executar o comportamento correto do objeto concreto em tempo de execução (despacho dinâmico). O ganho principal não é “usar herança”, e sim desenhar APIs extensíveis: você adiciona novos tipos sem precisar modificar o código cliente.

Quando uma API é bem polimórfica, o código cliente trabalha com List<Pagamento>, Notificador, RegraDeDesconto, etc., e não com if/switch por tipo. Isso reduz acoplamento, diminui bugs e facilita evolução.

Substituibilidade (Liskov aplicada, sem formalismo)

Substituibilidade significa: se um método aceita um tipo abstrato (ex.: Pagamento), então qualquer implementação concreta desse tipo deve poder ser usada sem quebrar expectativas do código que chama.

Na prática, isso se traduz em regras simples ao desenhar implementações:

  • Não enfraqueça o contrato: se a API diz “processa pagamento e retorna um recibo”, uma implementação não deveria “às vezes não processar” sem sinalizar erro de forma consistente.
  • Não surpreenda com efeitos colaterais: se o método promete apenas calcular um valor, não deveria gravar no banco ou disparar e-mails.
  • Respeite pré-condições: não exija mais do que o tipo abstrato exige. Se a interface aceita qualquer valor positivo, uma implementação não deveria aceitar apenas múltiplos de 10 sem documentar/validar no mesmo nível do contrato.
  • Respeite pós-condições: não entregue menos do que o prometido. Se promete retornar uma lista não nula, não retorne null.

Essas regras evitam que o código cliente precise “adivinhar” qual implementação está por trás e começar a fazer checagens com instanceof.

Continue em nosso aplicativo e ...
  • Ouça o áudio com a tela desligada
  • Ganhe Certificado após a conclusão
  • + de 5000 cursos para você explorar!
ou continue lendo abaixo...
Download App

Baixar o aplicativo

Despacho dinâmico e coleções de tipos abstratos

O padrão mais comum de polimorfismo em APIs é: receber uma coleção de um tipo abstrato e iterar chamando um método que cada implementação resolve de forma diferente.

import java.math.BigDecimal;
import java.util.List;

interface RegraDePreco {
    BigDecimal aplicar(BigDecimal precoBase);
}

final class SemDesconto implements RegraDePreco {
    public BigDecimal aplicar(BigDecimal precoBase) {
        return precoBase;
    }
}

final class DescontoPercentual implements RegraDePreco {
    private final BigDecimal percentual; // ex.: 0.10 para 10%

    DescontoPercentual(BigDecimal percentual) {
        this.percentual = percentual;
    }

    public BigDecimal aplicar(BigDecimal precoBase) {
        return precoBase.subtract(precoBase.multiply(percentual));
    }
}

final class PrecoComposto {
    BigDecimal calcular(BigDecimal precoBase, List<RegraDePreco> regras) {
        BigDecimal atual = precoBase;
        for (RegraDePreco regra : regras) {
            atual = regra.aplicar(atual); // despacho dinâmico
        }
        return atual;
    }
}

O método calcular não sabe (nem precisa saber) se a regra é percentual, fixa, progressiva, etc. Para estender, você cria uma nova classe que implementa RegraDePreco e a adiciona à lista.

Evitar instanceof: “pergunte ao objeto”

Checagens com instanceof geralmente aparecem quando o código cliente tenta decidir “o que fazer” baseado no tipo concreto. Em vez disso, mova a decisão para o próprio objeto por meio de um método no tipo abstrato.

Exemplo de cheiro de código (type checks):

interface Notificacao {
}

final class Email implements Notificacao {
    final String endereco;
    Email(String endereco) { this.endereco = endereco; }
}

final class Sms implements Notificacao {
    final String numero;
    Sms(String numero) { this.numero = numero; }
}

final class Enviador {
    void enviar(Notificacao n, String mensagem) {
        if (n instanceof Email e) {
            enviarEmail(e.endereco, mensagem);
        } else if (n instanceof Sms s) {
            enviarSms(s.numero, mensagem);
        } else {
            throw new IllegalArgumentException("Tipo não suportado");
        }
    }

    private void enviarEmail(String endereco, String mensagem) { /* ... */ }
    private void enviarSms(String numero, String mensagem) { /* ... */ }
}

Problemas: toda vez que surgir um novo tipo (ex.: Push), você precisa editar Enviador. Isso quebra extensibilidade e aumenta risco de regressão.

Versão polimórfica (API extensível):

interface Notificacao {
    void enviarCom(Transportes transportes, String mensagem);
}

final class Email implements Notificacao {
    private final String endereco;

    Email(String endereco) { this.endereco = endereco; }

    public void enviarCom(Transportes transportes, String mensagem) {
        transportes.email().enviar(endereco, mensagem);
    }
}

final class Sms implements Notificacao {
    private final String numero;

    Sms(String numero) { this.numero = numero; }

    public void enviarCom(Transportes transportes, String mensagem) {
        transportes.sms().enviar(numero, mensagem);
    }
}

interface TransporteEmail {
    void enviar(String endereco, String mensagem);
}

interface TransporteSms {
    void enviar(String numero, String mensagem);
}

final class Transportes {
    private final TransporteEmail email;
    private final TransporteSms sms;

    Transportes(TransporteEmail email, TransporteSms sms) {
        this.email = email;
        this.sms = sms;
    }

    TransporteEmail email() { return email; }
    TransporteSms sms() { return sms; }
}

final class Enviador {
    private final Transportes transportes;

    Enviador(Transportes transportes) {
        this.transportes = transportes;
    }

    void enviar(Notificacao n, String mensagem) {
        n.enviarCom(transportes, mensagem); // despacho dinâmico
    }
}

Agora Enviador é estável. Para adicionar Push, você cria uma nova classe que implementa Notificacao e injeta o transporte correspondente (ou amplia Transportes de forma controlada). O código cliente continua usando Notificacao.

Polimorfismo no desenho de APIs: dicas práticas

1) Aceite abstrações, retorne abstrações quando fizer sentido

Se seu método só precisa de um contrato, não exija uma classe concreta. Exemplo: receber List<RegraDePreco> em vez de ArrayList<RegraDePreco>. Isso permite que o chamador use qualquer implementação de lista.

BigDecimal calcularTotal(List<RegraDePreco> regras) { /* ... */ }

2) Mantenha contratos pequenos e coesos

Interfaces muito grandes forçam implementações a “fingir” comportamentos (retornar valores vazios, lançar exceções) e isso viola substituibilidade. Prefira contratos focados.

3) Modele variações como estratégias

Quando você tem “várias formas de fazer a mesma coisa” (calcular frete, aplicar imposto, validar documento), crie um tipo abstrato com um método principal e injete a implementação. O código cliente chama o método e o despacho dinâmico resolve a variação.

4) Use instanceof como último recurso (e isole)

Há casos legítimos (integração com bibliotecas, serialização, adaptação de tipos externos). Se precisar, isole em um ponto (ex.: um adaptador) para não contaminar a base com condicionais por tipo.

Passo a passo prático: coleção polimórfica em um caso de cobrança

Objetivo: processar uma lista de itens cobrando cada um conforme seu tipo, sem if/switch.

Passo 1 — Defina o contrato do item cobravel

import java.math.BigDecimal;

interface ItemCobravel {
    BigDecimal subtotal();
}

Passo 2 — Crie implementações com regras próprias

import java.math.BigDecimal;

final class Produto implements ItemCobravel {
    private final BigDecimal preco;
    private final int quantidade;

    Produto(BigDecimal preco, int quantidade) {
        this.preco = preco;
        this.quantidade = quantidade;
    }

    public BigDecimal subtotal() {
        return preco.multiply(BigDecimal.valueOf(quantidade));
    }
}

final class Servico implements ItemCobravel {
    private final BigDecimal valorHora;
    private final int horas;

    Servico(BigDecimal valorHora, int horas) {
        this.valorHora = valorHora;
        this.horas = horas;
    }

    public BigDecimal subtotal() {
        return valorHora.multiply(BigDecimal.valueOf(horas));
    }
}

Passo 3 — Escreva o cálculo usando apenas o tipo abstrato

import java.math.BigDecimal;
import java.util.List;

final class Fatura {
    BigDecimal total(List<ItemCobravel> itens) {
        BigDecimal total = BigDecimal.ZERO;
        for (ItemCobravel item : itens) {
            total = total.add(item.subtotal());
        }
        return total;
    }
}

Para adicionar um novo tipo (ex.: Assinatura), você implementa ItemCobravel. O cálculo não muda.

Exercício de refatoração: remover condicionais por tipo e introduzir métodos polimórficos

Você recebeu o código abaixo em um projeto. Ele calcula o custo de entrega conforme o tipo de envio e aplica regras diferentes. O objetivo é refatorar para polimorfismo, eliminando instanceof e deixando a API aberta para novos tipos de envio.

Código inicial (com condicionais por tipo)

import java.math.BigDecimal;

interface Envio {
}

final class EnvioNormal implements Envio {
    final BigDecimal distanciaKm;
    EnvioNormal(BigDecimal distanciaKm) { this.distanciaKm = distanciaKm; }
}

final class EnvioExpresso implements Envio {
    final BigDecimal distanciaKm;
    EnvioExpresso(BigDecimal distanciaKm) { this.distanciaKm = distanciaKm; }
}

final class RetiradaNaLoja implements Envio {
}

final class CalculadoraDeFrete {
    BigDecimal calcular(Envio envio) {
        if (envio instanceof RetiradaNaLoja) {
            return BigDecimal.ZERO;
        }
        if (envio instanceof EnvioNormal n) {
            return n.distanciaKm.multiply(new BigDecimal("1.50"));
        }
        if (envio instanceof EnvioExpresso e) {
            return e.distanciaKm.multiply(new BigDecimal("3.00")).add(new BigDecimal("10.00"));
        }
        throw new IllegalArgumentException("Tipo de envio não suportado");
    }
}

Meta da refatoração

  • Transformar Envio em um contrato com um método polimórfico, por exemplo BigDecimal frete() (ou frete(PoliticaDeFrete) se quiser separar dependências).
  • Fazer cada tipo de envio saber calcular seu próprio frete (respeitando substituibilidade).
  • Deixar CalculadoraDeFrete simples ou até desnecessária.

Passo a passo sugerido

Passo 1 — Introduza o método no tipo abstrato

import java.math.BigDecimal;

interface Envio {
    BigDecimal frete();
}

Passo 2 — Implemente o método em cada classe concreta

import java.math.BigDecimal;

final class RetiradaNaLoja implements Envio {
    public BigDecimal frete() {
        return BigDecimal.ZERO;
    }
}

final class EnvioNormal implements Envio {
    private final BigDecimal distanciaKm;

    EnvioNormal(BigDecimal distanciaKm) { this.distanciaKm = distanciaKm; }

    public BigDecimal frete() {
        return distanciaKm.multiply(new BigDecimal("1.50"));
    }
}

final class EnvioExpresso implements Envio {
    private final BigDecimal distanciaKm;

    EnvioExpresso(BigDecimal distanciaKm) { this.distanciaKm = distanciaKm; }

    public BigDecimal frete() {
        return distanciaKm.multiply(new BigDecimal("3.00")).add(new BigDecimal("10.00"));
    }
}

Passo 3 — Simplifique o código cliente

import java.math.BigDecimal;

final class CalculadoraDeFrete {
    BigDecimal calcular(Envio envio) {
        return envio.frete();
    }
}

Passo 4 — Teste a extensibilidade

Adicione um novo tipo sem tocar na calculadora:

import java.math.BigDecimal;

final class EnvioInternacional implements Envio {
    private final BigDecimal distanciaKm;
    private final BigDecimal taxaAlfandega;

    EnvioInternacional(BigDecimal distanciaKm, BigDecimal taxaAlfandega) {
        this.distanciaKm = distanciaKm;
        this.taxaAlfandega = taxaAlfandega;
    }

    public BigDecimal frete() {
        return distanciaKm.multiply(new BigDecimal("5.00")).add(taxaAlfandega);
    }
}

Checklist de substituibilidade para o exercício

  • frete() sempre retorna um BigDecimal não nulo.
  • Se houver validações (ex.: distância negativa), elas devem ser consistentes para todos os tipos ou estar claramente no contrato.
  • Nenhuma implementação deve exigir que o chamador faça instanceof para “usar direito”.

Variação (opcional): quando o cálculo depende de serviços externos

Se o frete depende de um serviço (ex.: tabela de preços, API externa), evite colocar dependências dentro das entidades. Uma alternativa é receber um colaborador no método:

import java.math.BigDecimal;

interface PoliticaDeFrete {
    BigDecimal normal(BigDecimal distanciaKm);
    BigDecimal expresso(BigDecimal distanciaKm);
    BigDecimal internacional(BigDecimal distanciaKm, BigDecimal taxaAlfandega);
    BigDecimal retirada();
}

interface Envio {
    BigDecimal frete(PoliticaDeFrete politica);
}

Assim, você mantém polimorfismo e ainda centraliza integrações em um componente próprio, sem voltar para instanceof no código cliente.

Agora responda o exercício sobre o conteúdo:

Ao refatorar uma calculadora de frete que usa instanceof para decidir o tipo de Envio, qual mudança deixa a API mais extensível e reduz a necessidade de alterar o código cliente quando surgir um novo tipo de envio?

Você acertou! Parabéns, agora siga para a próxima página

Você errou! Tente novamente.

A ideia é substituir condicionais por tipo por despacho dinâmico: o cliente fala com o tipo abstrato e cada implementação resolve o comportamento. Assim, novos tipos podem ser adicionados sem modificar a calculadora e sem espalhar instanceof.

Próximo capitúlo

Generics em Java OOP: tipos parametrizados, segurança de tipos e coleções

Arrow Right Icon
Capa do Ebook gratuito Java Orientado a Objetos: Do Conceito ao Código com Padrões de Projeto Básicos
47%

Java Orientado a Objetos: Do Conceito ao Código com Padrões de Projeto Básicos

Novo curso

17 páginas

Baixe o app para ganhar Certificação grátis e ouvir os cursos em background, mesmo com a tela desligada.