A tomada, não o aparelho
A parede oferece um contrato (tomada). Geladeira, abajur e carregador ‘implementam’ o plug. Você não reescreve a instalação elétrica para cada aparelho novo — só encaixa. Polimorfismo é a tomada; herança malfeita é soldar o cabo na parede.
Herança entra onde?
Herança cria uma família de tipos e permite tratar o filho como o pai. Isso é polimorfismo — desde que o filho possa substituir o pai de verdade (LSP).
Mas polimorfismo não exige extends. Uma interface implementada por classes sem parentesco comum, ou funções que obedecem à mesma assinatura, resolvem o mesmo problema. Strategy, Repository e boa parte do código moderno usam isso.
A trilha liga daqui para Composição sobre herança e para Strategy: quando o comportamento varia, o caminho saudável costuma ser ‘receber uma peça que fala o contrato’, não ‘subclassear e sobrescrever’.
- Cliente
- chamaContrato
- resolve em runtimeImplementação
Como isso aparece na prática
O switch de gateway de pagamento
Cenário
Cada novo meio (cartão, pix, boleto) ganhava um case no service de checkout. O arquivo virava o mapa da empresa de pagamentos.
Como o padrão entra
Um contrato Gateway.cobrar(pedido) e uma implementação por meio; o checkout só recebe o gateway configurado.
Trade-off: Mais arquivos e fiação na composição. Em troca, meio novo = classe nova, sem reabrir o checkout.
Testes sem bater no SMTP real
Cenário
Testar o fluxo de cadastro exigia e-mail de verdade ou um mock global frágil.
Como o padrão entra
O serviço depende de Notificador; no teste entra um fake que grava a mensagem em memória.
Trade-off: Uma interface a mais. Em troca, o teste fica determinístico e rápido.
Onde isto dá errado
Interface com um só implementador eterno
Se nunca houve segunda variante e não há fronteira de teste, a interface é especulação. Polimorfismo prematuro também custa.
Contrato que vaza o tipo concreto
Métodos que só fazem sentido numa implementação empurram
instanceofde volta — o cliente deixou de ser polimórfico.Herança que quebra substituição
Filho que lança ‘não suportado’ nos métodos do pai não é polimorfismo útil: o chamador precisa saber qual filho recebeu (veja LSP).
O jeito errado
Compila, roda, passa no code review. E quebra depois.
O cliente escolhe o comportamento com switch de tipo em vez de falar com um contrato.
type Canal = "email" | "slack";
function avisar(canal: Canal, msg: string) {
if (canal === "email") enviarEmail(msg);
else if (canal === "slack") enviarSlack(msg);
// próximo canal = reabrir esta função
}O que quebra
- nasce um canal novotodo switch de canal no sistema precisa ser reaberto
- o teste quer um fakenão há onde encaixar — o comportamento está hardcoded
A saída: Contrato Notificador + implementações; o cliente chama n.enviar(msg).
Do switch ao contrato
// Um contrato, várias implementações — o chamador não escolhe a classe.
interface Notificador {
enviar(msg: string): void;
}
class Email implements Notificador {
enviar(msg: string) { /* SMTP */ }
}
class Slack implements Notificador {
enviar(msg: string) { /* webhook */ }
}
function avisar(n: Notificador, msg: string) {
n.enviar(msg); // polimorfismo: o mesmo call, comportamentos diferentes
}
avisar(new Email(), "ok");
avisar(new Slack(), "ok");class Notificador(Protocol):
def enviar(self, msg: str) -> None: ...
class Email:
def enviar(self, msg: str) -> None: ...
class Slack:
def enviar(self, msg: str) -> None: ...
def avisar(n: Notificador, msg: str) -> None:
n.enviar(msg) # duck typing + contrato = polimorfismointerface Notificador { void enviar(String msg); }
class Email implements Notificador {
public void enviar(String msg) { /* ... */ }
}
class Slack implements Notificador {
public void enviar(String msg) { /* ... */ }
}
void avisar(Notificador n, String msg) {
n.enviar(msg); // resolve em runtime pela implementação real
}- 1
Nomeie o papel
O que o cliente precisa fazer? ‘Enviar aviso’, não ‘falar SMTP’.
- 2
Escreva o contrato
Interface/type com só o que o cliente usa.
- 3
Implemente as variantes
Uma classe (ou função) por comportamento real.
- 4
Injete na borda
Quem monta o app escolhe a implementação; o domínio só recebe o contrato.
Quando usar × quando evitar
Use quando
3- Várias implementações do mesmo papel.
- Cliente estável quando nasce variante nova.
- Testes com fake atrás do mesmo contrato.
Evite quando
2- Uma variante só, sem fronteira de teste.
- Contrato que força o cliente a conhecer os filhos.
Testar o que ficou
3 armadilhas de Polimorfismo viram perguntas, com o nome do padrão escondido, misturadas com padrões parecidos.
Onde isto aparece de verdade
Você já usa isto, talvez sem saber o nome.
Array.prototype.sort(compareFn)Você passa a estratégia de comparação; o sort não conhece seus tipos — polimorfismo por função.strategies do PassportLocal, JWT, OAuth: o mesmo pipeline de auth, implementações diferentes atrás do contrato.React: componentes como tipo<Botao />e<Link />satisfazem o mesmo lugar na árvore — o pai fala com a interface do filho.
O padrão em uma linha
Sem nome de domínio, sem cerimônia. Só a ideia.
function avisar(n: Notificador, msg: string) { n.enviar(msg); }O que ele cobra
1 salto a mais entre a chamada e o efeito.
- O fluxo real fica menos óbvio no jump-to-definition — a implementação aparece em runtime
- Contrato frouxo demais empurra
instanceofde volta ao cliente
Não vale se: só existem uma ou duas variações estáveis e um if claro é mais honesto que uma hierarquia especulativa.
Onde isto aparece
Continue por aqui
- EncapsulamentoEsconder o estado atrás de um contrato: quem usa o objeto não sabe — nem depende de — como ele guarda dados por dentro. Sem isso, cada mudança de representação vaza para quem consome; com isso, o objeto protege suas invariantes e pode mudar a implementação sem quebrar o resto.
- StrategyEncapsula uma família de algoritmos intercambiáveis por trás de uma interface comum, deixando o cliente trocar a estratégia em tempo de execução.
- LSP — Substituição de LiskovUm subtipo deve poder substituir seu supertipo sem que o código que usa o supertipo perceba a diferença — o que é uma exigência sobre comportamento, não apenas sobre assinatura.
- Composição sobre herançaHerança promete reúso e entrega acoplamento: a subclasse herda tudo do pai, inclusive o que não queria, e a árvore engessa numa dimensão só. Compor comportamento a partir de peças encaixáveis dá o mesmo reúso sem amarrar quem usa a quem fornece — por isso o conselho antigo de preferir composição, e só herdar quando a relação 'é um' é honesta.
- OCP — Aberto/FechadoMódulos devem ser abertos para extensão e fechados para modificação: adicionar comportamento novo deveria significar escrever código novo, não reabrir e alterar o que já funciona.