O princípio de substituição de Liskov determina que objetos de uma superclasse devem poder ser trocados por objetos de uma subclasse sem alterar o comportamento esperado do programa. A regra vale para herança e para interfaces. Quando a subclasse muda a semântica de um método, a quebra aparece em tempo de execução, não na compilação.
O que é o princípio de substituição de Liskov?
O princípio de substituição de Liskov diz que objetos de uma superclasse devem poder ser substituídos por objetos de uma subclasse sem alterar o comportamento esperado do programa. É a letra L do acrônimo SOLID, um conjunto de cinco princípios de design orientado a objetos. A formulação original vem da cientista da computação Barbara Liskov, que descreveu a ideia em termos de subtipos: se S é um subtipo de T, objetos do tipo T podem ser trocados por objetos do tipo S sem quebrar as propriedades desejáveis do programa.
Subtipo aqui cobre os dois caminhos do polimorfismo: herança de uma superclasse e implementação de uma interface. Toda vez que uma classe está hierarquicamente abaixo de outra, herdando ou implementando, o princípio se aplica.
A parte crítica é o trecho final: sem alterar o comportamento esperado. Compiladores modernos já forçam a mesma assinatura, o mesmo número e ordem de parâmetros e a mesma estrutura de retorno. Isso resolveu o problema estrutural de décadas passadas, quando sobrescrever um método podia mudar parâmetros livremente. O princípio continua válido porque trata de outra camada: o comportamento, que o compilador não verifica.
Quem é Barbara Liskov e por que ela dá nome ao princípio?
Barbara Liskov é uma das cientistas da computação mais reconhecidas da área, professora do MIT e referência em técnicas de orientação a objetos. A ideia foi apresentada pela primeira vez em 1993, numa conferência, e formalizada por ela com Jeannette Wing em 1994, no artigo "A Behavioral Notion of Subtyping". Robert C. Martin, o Uncle Bob, popularizou o princípio na comunidade de software ao integrá-lo ao SOLID. A definição canônica do princípio preserva exatamente essa relação entre subtipo e substituibilidade.
Na época da formulação, linguagens e compiladores tinham capacidades bem menores que as de hoje. Uma superclasse definia a assinatura, mas quem sobrescrevia podia alterar ordem de parâmetros ou estrutura de retorno. O princípio nasceu para estabelecer uma padronização mínima de contrato. As linguagens depois absorveram a parte sintática; a parte semântica, de comportamento, continua responsabilidade do designer do código.
Buscar a trajetória dela vale a pena: Liskov contribuiu para abstrações de dados, tolerância a falhas e linguagens como a CLU, e continua formando engenheiros no MIT.
Como o exemplo do retângulo e do quadrado viola o LSP?
O exemplo clássico usa um software de desenho, algo como o Microsoft Paint, com duas classes: Rectangle, com largura e altura, e Square. Por uma decisão de design didática, o quadrado é implementado como subclasse do retângulo. Visualmente faz sentido: um quadrado é um retângulo com todos os lados iguais. Em código, a decisão cria conflito.
O retângulo permite mudar largura e altura de forma independente. O quadrado não: para continuar sendo quadrado, alterar a largura precisa alterar a altura também, e vice-versa. A subclasse então sobrescreve os setters e cria um acoplamento entre propriedades que a superclasse nunca prometeu.
ts class Rectangle { constructor(width, height) { this.width = width; this.height = height; } setWidth(w) { this.width = w; } setHeight(h) { this.height = h; } getArea() { return this.width * this.height; } }
class Square extends Rectangle { constructor(side) { super(side, side); } setWidth(w) { this.width = w; this.height = w; } setHeight(h) { this.width = h; this.height = h; } }
Note que o construtor do quadrado recebe um único valor, o lado, enquanto o do retângulo recebe dois. Já é um mau cheiro, um code smell, indicando que a hierarquia merece revisão.
| Classe | Contrato de mutação | Cálculo de área |
|---|---|---|
Rectangle | largura e altura mudam de forma independente | largura × altura |
Square | mudar um lado muda todos os lados | lado × lado |
Shape (proposta) | nenhum contrato de mutação, apenas getArea() | definido por cada subclasse |
As duas primeiras linhas mostram o problema: contratos de mutação incompatíveis não podem compartilhar a mesma hierarquia de subtipo.
Onde a quebra do princípio se materializa: tempo de execução
A violação do LSP aparece quando o programa roda, não na estrutura do código. Diferente do princípio da responsabilidade única e do open/closed, que são problemas visíveis na organização das classes, o LSP se materializa na execução, com valores errados ou erros inesperados.
Considere uma função que recebe qualquer Rectangle, altera a largura para 5, a altura para 4 e calcula a área:
ts function useRectangle(rect: Rectangle) { rect.setWidth(5); rect.setHeight(4); return rect.getArea(); // esperado: 20 }
Com um Rectangle(2, 3), o resultado é 5 × 4 = 20, exatamente o esperado. Com um Square(2), o resultado é 16. Ao alterar a largura, o setter do quadrado mudou a altura junto; ao alterar a altura, mudou a largura de novo. A área guarda sempre o último valor, 4 × 4.
A quebra é exatamente essa: o comportamento garantido pela superclasse não foi preservado na subclasse. E não se trata só de valores errados; o mesmo mecanismo produz exceções inesperadas e mudanças de comportamento que só aparecem com objetos de tipos diferentes em tempo de execução.
O teste do "é um" funciona para validar herança?
O teste do "é um" ajuda, mas só quando a afirmação é 100% verdadeira em todas as situações. Ao aprender orientação a objetos, você provavelmente encontrou a regra: se cachorro é um animal vertebrado, então cachorro herda de animal vertebrado.
Aplicando ao exemplo: um quadrado é sempre um retângulo? Não. Se você precisa mudar a largura de forma independente, o quadrado deixa de ser quadrado. A afirmação não é 100% verdadeira, então a herança não se sustenta. Nesse caso, manter o quadrado como subclasse do retângulo quebra o comportamento esperado da classe base e viola o princípio de substituição de Liskov.
Use o teste como alerta inicial, nunca como prova final. A verificação decisiva é comportamental: a subclasse cumpre todas as promessas da superclasse em qualquer cenário de uso?
Como corrigir a hierarquia sem quebrar o LSP?
A correção proposta no vídeo sobe um nível na abstração: uma classe abstrata Shape, cuja única responsabilidade é representar uma forma geométrica qualquer e declarar o método getArea(). A classe abstrata não define comportamento, apenas o contrato de que cada subclasse retorna um número representando sua área. A refatoração segue quatro passos:
- Crie a classe abstrata
Shapedeclarando apenasgetArea(): number, sem implementação. - Faça
RectangleestenderShape, mantendo largura e altura independentes. - Faça
SquareestenderShapediretamente, expondo sósetSide(), que garante todos os lados iguais. - Mantenha
useRectanglerecebendo retângulos e crieuseShapepara aceitar qualquer subtipo deShape.
ts abstract class Shape { abstract getArea(): number; }
class Rectangle extends Shape { / setters independentes, getArea() = largura × altura / } class Square extends Shape { / apenas setSide(), getArea() = lado × lado / }
Cada forma agora garante suas próprias regras de existência. O retângulo mantém largura e altura independentes; o quadrado só expõe a mudança de lado, o que respeita sua regra de todos os lados iguais.
Com isso, useRectangle continua recebendo retângulos e funcionando. O quadrado deixa de ser subtipo de retângulo, então o editor, o VS Code no exemplo, acusa que Square não é assignable ao tipo Rectangle. Tentar passar um quadrado para essa função vira erro de compilação, e não bug silencioso.
O quadrado continua utilizável pela nova função useShape, que aceita qualquer subtipo de Shape e chama getArea(). Como o único contrato da superclasse é retornar a área, tanto retângulo quanto quadrado cumprem o esperado: retângulo 5 × 4 = 20, quadrado 2 × 2 = 4. Polimorfismo preservado, princípio respeitado.
A lição de design: quando duas classes compartilham cálculo mas têm regras de mutação incompatíveis, extraia o que é comum para um tipo mais alto e não as force numa hierarquia que não é verdadeira.
Quais sinais práticos indicam violação do princípio de substituição de Liskov?
Você pode caçar violações de LSP procurando padrões concretos no dia a dia. O mais comum é a subclasse que precisa de condicionais para "se encaixar": código que checa if (shape instanceof Square) antes de agir é um sinal claro de que o contrato da superclasse não é universal.
- Subclasse lança exceção que a superclasse nunca lança, ou retorna tipos diferentes do prometido.
- Sobrescrita de método ignora ou anula parte do comportamento da classe base, como setters que alteram propriedades extras.
- Condicionais por tipo espalhadas pelo código para lidar com diferenças entre subclasses.
- Testes que passam para a superclasse e falham quando o mesmo fluxo roda com a subclasse.
- Método sobrescrito que precisa ser "desligado" na filha, por exemplo lançando
NotImplementedError.
Todos esses sinais apontam para o mesmo problema: o contrato implícito da superclasse é mais estreito do que a hierarquia sugere. Corrigir significa reorganizar a abstração, geralmente extraindo uma base menor e mais honesta, como a classe Shape do exemplo anterior.
Por que o LSP é considerado o princípio mais crítico do SOLID?
O LSP é tratado como o mais crítico porque a violação não aparece na leitura do código. SRP e open/closed são problemas estruturais: você abre a classe e vê a bagunça. O LSP quebra silenciosamente, na execução, com valores errados ou comportamentos inesperados.
Essa característica muda a estratégia de detecção. Revisão de código sozinha raramente pega a quebra; você precisa de testes que exercitem subclasses nos mesmos fluxos da superclasse e de atenção aos sinais da seção anterior. Em linguagens tipadas como TypeScript, o erro de assignable é o primeiro alerta. Em linguagens de tipagem dinâmica, produção costuma ser o primeiro.
FAQ sobre o princípio de substituição de Liskov
O princípio de substituição de Liskov vale só para herança?
Não. Ele vale para qualquer relação de subtipo, incluindo herança de classes e implementação de interfaces. Sempre que um objeto pode aparecer no lugar de outro pelo sistema de tipos, o contrato de comportamento se aplica.
Compiladores modernos já resolvem o LSP automaticamente?
Eles resolvem apenas a parte estrutural: número de parâmetros, ordem e tipo de retorno. A parte de comportamento, como efeitos colaterais e semântica dos métodos, continua sendo responsabilidade do designer do código.
Por que o exemplo do quadrado e do retângulo é usado tanto?
Porque é o exemplo mais clássico, presente nos materiais do Uncle Bob e em praticamente toda literatura sobre o tema. Ele mostra de forma simples que a relação matemática "é um" nem sempre sobrevive às regras de mutação do código.
O LSP se aplica também em linguagens com tipagem dinâmica?
Aplica, e com mais risco. Sem verificação de tipos em compilação, a quebra só aparece em tempo de execução, geralmente em produção, então disciplina de design e testes se tornam ainda mais importantes.
Qual a diferença entre subtipo e subclasse no contexto do LSP?
Subclasse é um mecanismo de linguagem: uma classe que estende outra. Subtipo é uma garantia de comportamento: a filha pode aparecer em qualquer lugar onde a mãe era esperada. Toda subclasse deveria ser um subtipo; quando não é, você tem a violação clássica do LSP.
O que fazer quando já identifiquei uma hierarquia quebrada?
Procure o contrato mínimo comum entre as classes envolvidas e extraia-o para uma base menor, como Shape no exemplo. Depois, corte a herança direta entre as classes com regras de mutação conflitantes e confie no compilador para apontar os pontos de uso que precisam de ajuste.
Do conceito à prática: leve o LSP para o seu código hoje
O que o exemplo do retângulo e do quadrado deixa de lição é simples: herança é contrato de comportamento, e o compilador não fiscaliza contrato, só sintaxe. Para aprofundar nesse e em outros princípios SOLID com TypeScript, materiais como o Crazystack Typescript e o Bootcamp do Dev Doido, do criador Gustavo Dev Doido, percorrem o mesmo caminho de refatorar hierarquias quebradas em abstrações honestas.
Transforme suas aulas sobre LSP em artigos
Se você aprendeu LSP assistindo a um vídeo, provavelmente também ensina ou estuda por esse formato. Aula boa gravada merece texto bom: escrito, o conteúdo vira referência pesquisável em vez de link perdido na playlist.
No Skala Blog, você cola o link de um vídeo do YouTube, o áudio é transcrito e o texto vira um artigo estruturado, pronto para revisar e publicar.
Fork this article
Start a new branch from the same video, shaped your way. You keep the credit; the original keeps the attribution.
A fork in another language is filed as a translation of this article, so the two pages point at each other. You can unlink it later from the editor.
0/240
You are creating
- Format
- For
- Language
- Source
- Your angle
You will be asked to sign in before it is generated.
Buy credits