A cópia da chave
Uma cópia da chave da sua casa é aceitável se abrir a mesma porta, do mesmo jeito. Se ela abre mas exige girar três vezes, ou só funciona antes das seis da tarde, ela tecnicamente 'é uma chave' e mesmo assim quebra o que você esperava. A forma encaixa na fechadura; o comportamento é que decide se a substituição é válida.
O problema
Linguagens verificam assinatura, não comportamento. Um subtipo pode ter todos os métodos certos, compilar sem aviso e ainda assim violar o que o código chamador assumia.
O resultado é um polimorfismo que não se sustenta: quem usa a abstração precisa saber qual implementação recebeu, e o benefício de programar contra o tipo desaparece.
Barbara Liskov formulou isso em 1987 como uma condição sobre propriedades: se toda propriedade demonstrável sobre objetos do tipo T vale também para objetos do subtipo S, então S é subtipo de T. Traduzido para o dia a dia, são três regras práticas: pré-condições não podem ser fortalecidas, pós-condições não podem ser enfraquecidas e invariantes do supertipo precisam ser preservadas.
Fortalecer pré-condição é exigir mais: o pai aceitava qualquer inteiro e o filho só aceita positivos. Enfraquecer pós-condição é entregar menos: o pai garantia lista ordenada e o filho devolve em qualquer ordem. Ambos quebram código que já funcionava, sem nenhum erro de compilação.
O exemplo do quadrado e do retângulo é famoso porque expõe a raiz do problema: a taxonomia do mundo real não é a mesma coisa que a relação de subtipo em programação. Matematicamente um quadrado é um retângulo; em código, Quadrado só é subtipo de Retangulo se o retângulo for imutável. É a mutabilidade — poder mudar largura sem mudar altura — que cria a promessa impossível de cumprir.
Como detectar a violação
As violações se anunciam de formas bem reconhecíveis, quase todas envolvendo o chamador precisando saber demais sobre a implementação.
O sinal mais direto é o método que lança 'não suportado'. Se adicionar() existe na interface e uma implementação apenas lança exceção, o contrato está sendo quebrado por construção — e todo chamador precisa saber com qual implementação está falando.
O segundo é o `instanceof` no código polimórfico. Se quem recebe o supertipo precisa perguntar qual é o tipo real para decidir o que fazer, a substituição não é possível na prática.
O terceiro é a documentação com exceções: 'este método funciona para todos os tipos, exceto para X'. Cada exceção assim é uma violação declarada.
O teste que fecha a questão é escrever a suíte contra o supertipo e rodá-la contra todos os subtipos. Se algum precisa de um teste diferente ou de um caso especial, ele não é substituível — e a suíte compartilhada vira um detector permanente de regressão.
Modelagem pelo comportamento
- Ret e Quad implementam a mesma interface Forma
- não há relação de herança entre eles
- objetos imutáveis: redimensionar devolve um novo
- nenhum chamador precisa saber qual recebeu
O custo é abrir mão de uma hierarquia intuitiva e, muitas vezes, adotar imutabilidade — que resolve boa parte das violações de Liskov pela raiz.
Contrato de Conta.sacar(v)a promessa que vale para todo subtipo
Pré-condiçãoaceita v > 0 — o filho não pode exigir mais
Pós-condiçãosaldo diminui em v — o filho não pode entregar menos
Invariantesaldo nunca fica indefinido
Navegue pelas camadas
Camada 1/3Pré-condições
use ← →Camada 1
Pré-condições
o subtipo não exige mais
O que o método precisa para funcionar. Um subtipo pode aceitar mais casos que o pai, nunca menos — quem chamava com um argumento válido antes continua chamando depois.
No código
// pai aceita qualquer valor > 0; filho NÃO pode exigir valor > 100Se violar: código que funcionava com o supertipo passa a lançar exceção ao receber o subtipo, sem nenhum aviso em tempo de compilação.
Estrutura
Código
// VIOLACAO CLASSICA: o subtipo quebra a promessa do supertipo
class Retangulo {
constructor(protected larg: number, protected alt: number) {}
setLargura(l: number): void { this.larg = l; }
setAltura(a: number): void { this.alt = a; }
area(): number { return this.larg * this.alt; }
}
class Quadrado extends Retangulo {
setLargura(l: number): void { this.larg = l; this.alt = l; } // muda a altura junto!
setAltura(a: number): void { this.larg = a; this.alt = a; }
}
// Codigo escrito contra Retangulo, valido para qualquer Retangulo
function redimensionar(r: Retangulo): void {
r.setLargura(5);
r.setAltura(4);
console.assert(r.area() === 20, "esperava 20, veio " + r.area());
}
redimensionar(new Retangulo(0, 0)); // ok: 20
redimensionar(new Quadrado(0, 0)); // FALHA: 16 — o quadrado nao e substituivel
// CORRECAO: nao ha relacao de subtipo; sao formas irmas e imutaveis
interface Forma { area(): number }
class Ret implements Forma {
constructor(private l: number, private a: number) {}
comLargura(l: number): Ret { return new Ret(l, this.a); }
area(): number { return this.l * this.a; }
}
class Quad implements Forma {
constructor(private lado: number) {}
area(): number { return this.lado ** 2; }
}# VIOLACAO CLASSICA: o subtipo quebra a promessa do supertipo
class Retangulo:
def __init__(self, larg, alt):
self._larg, self._alt = larg, alt
def set_largura(self, l): self._larg = l
def set_altura(self, a): self._alt = a
def area(self): return self._larg * self._alt
class Quadrado(Retangulo):
def set_largura(self, l): self._larg = self._alt = l # muda a altura junto!
def set_altura(self, a): self._larg = self._alt = a
# Codigo escrito contra Retangulo, valido para qualquer Retangulo
def redimensionar(r):
r.set_largura(5)
r.set_altura(4)
assert r.area() == 20, f"esperava 20, veio {r.area()}"
redimensionar(Retangulo(0, 0)) # ok
# redimensionar(Quadrado(0)) # AssertionError: 16
# CORRECAO: formas irmas e imutaveis, sem relacao de subtipo
class Ret:
def __init__(self, l, a): self._l, self._a = l, a
def com_largura(self, l): return Ret(l, self._a)
def area(self): return self._l * self._a
class Quad:
def __init__(self, lado): self._lado = lado
def area(self): return self._lado ** 2Casos de uso
Conta salário numa hierarquia de contas
Cenário
Um banco modela ContaCorrente, ContaPoupanca e ContaSalario herdando de Conta, que promete sacar(valor) para qualquer valor até o saldo.
Como o padrão entra
A conta salário não pode transferir para terceiros nem sacar acima de um limite. Em vez de lançar exceção em sacar, o modelo separa capacidades: interfaces distintas para o que cada conta realmente faz.
Trade-off: O código do chamador fica mais explícito sobre capacidades e, ao mesmo tempo, mais verboso — não existe mais um tipo 'Conta' universal que sirva para todo caso de uso.
Coleção somente-leitura
Cenário
Uma API devolve uma lista imutável tipada como a interface de lista da linguagem, cujos métodos de escrita lançam exceção.
Como o padrão entra
Separar em duas interfaces — uma de leitura e outra que adiciona escrita — faz o tipo dizer a verdade. Quem só lê recebe a interface de leitura e nunca tem acesso ao que não pode usar.
Trade-off: É a origem de um dos exemplos mais debatidos do problema, presente em bibliotecas de várias linguagens; corrigir depois de publicada a API costuma ser inviável, e o remédio vira documentação.
Implementação de repositório em memória para testes
Cenário
Um repositório falso é usado nos testes no lugar do real, implementando a mesma interface.
Como o padrão entra
Uma suíte de contrato roda contra as duas implementações e cobra as mesmas garantias: ordenação, unicidade, comportamento em caso de ausência. O falso só é aceito se passar.
Trade-off: Manter o falso fiel exige disciplina contínua; sem a suíte compartilhada, ele diverge devagar e os testes passam a validar um comportamento que a produção não tem.
- 1
Escrever o contrato do supertipo
Liste o que o chamador pode assumir: pré-condições, pós-condições e invariantes. Isso é a promessa — não a assinatura sozinha.
- 2
Rodar a suíte contra todos os subtipos
Os mesmos testes do contrato devem passar em cada implementação. Se um subtipo precisa de caso especial, ele não é substituível.
- 3
Caçar 'não suportado' e instanceof
Método que só lança e código polimórfico que pergunta o tipo concreto são violações anunciadas — a hierarquia está mentindo.
- 4
Quebrar a herança quando não couber
Se o filho não cumpre a promessa, a correção não é adaptar com exceção: são tipos irmãos, composição ou interfaces menores (ISP).
O subtipo que lança 'não suportado'
Compila, roda, passa no code review. E quebra depois.
A hierarquia compila e o diagrama parece certo — mas um subtipo implementa o método só para lançar. Quem programa contra o pai precisa saber qual filho recebeu.
abstract class Conta {
constructor(protected saldo: number) {}
abstract sacar(valor: number): void;
}
class ContaCorrente extends Conta {
sacar(valor: number): void {
if (valor > this.saldo) throw new Error("saldo insuficiente");
this.saldo -= valor;
}
}
// Compila. Encaixa no tipo. Quebra a promessa.
class ContaSalario extends Conta {
sacar(_valor: number): void {
throw new Error("nao suportado"); // <- LSP morto aqui
}
}
function pagar(contas: Conta[], valor: number) {
for (const c of contas) c.sacar(valor); // explode com ContaSalario
}
// O chamador precisa saber QUAL conta recebeu — o polimorfismo mentiu.O que quebra
- Em runtimeCódigo que funcionava com
ContaCorrenteexplode ao receberContaSalario, sem aviso em compilação. - No chamadorAparece
if (c instanceof ContaSalario)— o polimorfismo morreu e o benefício do supertipo sumiu. - Na documentação'Funciona para todas as contas, exceto salário' — cada exceção assim é uma violação declarada.
A saída: Separe capacidades: quem não saca não implementa sacar. Interfaces menores (ISP) fazem o tipo dizer a verdade; herança que força mentira não é herança útil.
Armadilhas
Modelar pela taxonomia do mundo real
'Um quadrado é um retângulo', 'um pinguim é uma ave', 'uma conta salário é uma conta'. Todas verdadeiras no mundo e potencialmente falsas em código, porque o que importa é se o subtipo cumpre as promessas comportamentais do supertipo. A pergunta certa não é 'é um?', é 'pode substituir?'.
Resolver com exceção de 'não suportado'
Quando o subtipo não consegue cumprir um método, a saída fácil é lançar exceção. Isso transforma um erro de modelagem em bug de execução e obriga todo chamador a saber qual implementação recebeu. É sinal de que a interface precisa ser dividida — que é onde o ISP entra.
Ignorar que mutabilidade cria promessas
Boa parte das violações clássicas desaparece com objetos imutáveis: sem setters, não há como o subtipo quebrar a independência entre campos. Antes de brigar com a hierarquia, vale perguntar se o tipo precisava mesmo ser mutável.
Quando usar × quando evitar
Use quando
3- Sempre que existir herança ou implementação de interface.
- Ao revisar hierarquias com exceções de 'não suportado'.
- Ao publicar contratos que terceiros vão implementar.
Evite quando
3- Modelar por semelhança do mundo real em vez de por comportamento.
- Usar herança para reaproveitar código quando a substituição não se sustenta.
- Aceitar exceções como forma de 'adaptar' um subtipo que não cabe.
Testar o que ficou
3 armadilhas de LSP — Substituição de Liskov 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.
List: ArrayList × LinkedListQualquer implementação de List entra onde List é esperado; trocar ArrayList por LinkedList não quebra quem só depende do contrato.Arrays.asList que quebra no add()A List de tamanho fixo devolvida por Arrays.asList lança em .add(): o subtipo que promete menos que o supertipo, a violação clássica do JDK.drivers atrás de uma interfacePostgres ou MySQL por trás da mesma interface de banco só são substituíveis se cada driver cumprir o contrato inteiro, sem surpresas.
O padrão em uma linha
Sem nome de domínio, sem cerimônia. Só a ideia.
// Subtipo substitui o base sem surpresa.
function migrar(aves: Ave[]) { for (const a of aves) a.voar(); }O que ele cobra
Nenhum salto a mais entre a chamada e o efeito.
- Manter o contrato do supertipo honesto limita o que cada subtipo pode fazer diferente
- Verificar a substituibilidade de verdade exige testes sobre o contrato, não só sobre a classe
Não vale se: não há herança nem polimorfismo em jogo — sem subtipos substituindo um supertipo, a regra não tem sobre o que valer.
Onde isto aparece
Continue por aqui
- 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.
- ISP — Segregação de InterfacesNenhum cliente deve ser forçado a depender de métodos que não usa: é melhor várias interfaces pequenas e específicas do que uma grande e genérica.
- DIP — Inversão de DependênciaMódulos de alto nível não devem depender de módulos de baixo nível; ambos devem depender de abstrações — e a abstração pertence a quem a usa, não a quem a implementa.