O que é e por que ele aparece no seu dia a dia
Você provavelmente já escreveu código onde uma subclasse mudou o comportamento esperado da classe pai e passou horas caçando bugs sem entender o motivo. O conceito de lagartixa como escorpião descreve exatamente isso: quando um objeto de uma classe derivada se comporta de forma incompatível com o contrato implícito da classe base. Não é um padrão intencional. É uma armadilha que acontece quando a herança é usada sem considerar as pré-condições e pós-condições de cada método.
Entendendo lagartixa como escorpião na prática
O problema nasce da herança mal planejada. Você cria uma classe Animal com um método mover(). Herda para criar Pássaro, implementa voar(). Depois decide que tem outro tipo de animal que também herda de Animal, mas seu "mover" na verdade paralisa o movimento ou faz algo completamente diferente. O código compila. O runtime quebra. E a culpa é do contrato não respeitado. Minha experiência prática mostra que o erro mais comum é confundir reutilização de código com subtipagem real. Eu trabalhei num sistema de pagamentos onde tínhamos uma classe base Transacao com um método processar(). Criamos a subclasse TransacaoEstorno, que deveria simplesmente reverter uma cobrança. Mas alguém reescreveu processar() de forma que ela também enviava notificações, aplicava taxas e registrava logs diferentes. O código funcionava isoladamente. Quando o sistema chamava processar() polimorficamente, as transações normais passavam a ter comportamento de estorno. Levou três dias úteis para identificar a raiz.
A correção que usei foi simples, mas exigiu refatoração: em vez de forçar o estorno a ser um tipo de transação, criei uma interface separada chamada Reversivel e separei as responsabilidades. O método processar() voltou a ter um único contrato. O tempo de resolução foi de cerca de 6 horas para quem conhece o problema, mas o dano colateral eram transações duplicadas que já estavam em produção.
Quando isso acontece e como detectar cedo
Existem sinais claros antes do bug aparecer em produção. O primeiro é quando você percebe que testar a subclasse individualmente passa, mas usar a subclasse no lugar da classe base quebra fluxos existentes. O segundo sinal é quando o polimorfismo exige verificações de tipo com instanceof ou switch type em múltiplos pontos do código — isso é um sintoma de que o contrato não foi respeitado. Outro indicativo: métodos que lançam exceções diferentes da classe pai. Se Transacao processar() normalmente lança uma exceção de serviço, mas TransacaoEstorno processar() lança uma exceção específica, qualquer caller que trate Transacao genérica vai falhar. Testes unitários isolados não pegam isso. Você precisa de testes de integração que substituam a classe base por cada subclasse em todos os contextsos onde ela é utilizada.
No meu caso, depois do incidente de pagamentos, passei a escrever contratos explícitos em docstrings para cada método que seria sobrescrito. Cada subclasse nova passou por um checklist: pré-condições mantidas? Pós-condições mantidas? Exceções compatíveis? Comportamento observable igual ou estritamente mais restritivo? Leva 10 minutos a mais por método, mas evita semanas de debugging.
👉 Clique no botão abaixo para saber mais sobre o assunto!
Pitfalls avançados que iniciantes não veem
Um equívoco comum é acreditar que o problema só existe com herança de classes concretas. Ela aparece também com interfaces mal desenhadas, com composição mal aplicada e até com protocolos em linguagens que usam duck typing. Se você tem uma interface Paginavel com um metodo pagina(int), e uma implementação que ignora o parâmetro e sempre retorna a primeira página, isso é lagartixa como escorpião — a assinatura promete algo que a implementação não entrega. Outro ponto que muita gente perde de vista: o princípio em si não proíbe subclasses com comportamento diferente. Ele proíbe subclasses que quebram expectativas legítimas dos consumidores da classe base. A diferença é sutil mas crucial. Você pode ter uma subclasse mais restritiva se o contrato original permitir flexibilidade. O problema é quando a restrição quebra callers existentes.
Também é importante notar que nem toda violação desse princípio é ruim em todo contexto. Em frameworks de extensibilidade, às vezes você quer que o usuário sobrescreva métodos e o framework lida com variantes. Mas aí o framework precisa estar preparado para comportamentos desconhecidos. O problema surge quando há suposições implícitas sobre comportamento que não estão documentadas.
O que fazer quando já está no meio do código problemático
Se você encontrou um sistema onde lagartixa como escorpião já está instalado, a correção depende da escala. Para pequenos projetos, substituir herança por composição costuma resolver rápido. Você remove a subclasse problemática, extrai a funcionalidade desejada para um componente separado e injeta onde necessário. Isso geralmente reduz a complexidade de acoplamento em 40% a 60%. Para sistemas maiores, o caminho é mais trabalhoso. Você identifica todas as subclasses que violam o contrato, mapeia os callers afetados, e planeja uma migração gradual usando wrappers ou adaptadores que normalizam o comportamento antes de chamar a implementação original. Isso permite deploy por partes sem quebrar o sistema inteiro de uma vez.
Uma técnica útil que uso é adicionar testes regressivos polimórficos: para cada classe base relevante, escrevo um teste que instancia todas as subclasses conhecidas e roda o mesmo fluxo. Se algumasubclassa falhar num cenário que funciona na classe base, o teste trava a build. Isso custa cerca de 15 a 30 minutos para configurar por hierarquia, mas paga-se semanas depois.
Limitações e quando evitar esse enfoque
O rigor do contrato pode ser excessivo em alguns cenários. Se você está construindo um protótipo rápido, com prazos apertados e onde a hierarquia pode mudar frequentemente, impor verificações estritas de subtipagem pode travar o desenvolvimento. Nesses casos, prefira composição pura e evite herança até que o domínio esteja estabilizado. Também vale saber que o problema não se resolve apenas com boas práticas de código. Ferramentas estáticas de análise ajudam, mas nenhuma abstração substitui o entendimento do domínio. Um analisador pode achar instanceof, mas não sabe se o comportamento da subclassa faz sentido para os consumers daquele método. A verificação humana continua sendo indispensável.