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.
- Ouça o áudio com a tela desligada
- Ganhe Certificado após a conclusão
- + de 5000 cursos para você explorar!
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
Envioem um contrato com um método polimórfico, por exemploBigDecimal frete()(oufrete(PoliticaDeFrete)se quiser separar dependências). - Fazer cada tipo de envio saber calcular seu próprio frete (respeitando substituibilidade).
- Deixar
CalculadoraDeFretesimples 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 umBigDecimalnã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
instanceofpara “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.