# Guia do Princípio de Substituição de Liskov para POO

> Published 2026-10-11T13:56:10.693Z on https://www.crazystack.com.br/pt/p/guia-do-principio-de-substituicao-de-liskov-para-poo/
> Source video: https://www.youtube.com/watch?v=2Vi2IpUUWfs

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](https://en.wikipedia.org/wiki/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](https://en.wikipedia.org/wiki/Liskov_substitution_principle) 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:

1. Crie a classe abstrata `Shape` declarando apenas `getArea(): number`, sem implementação.
2. Faça `Rectangle` estender `Shape`, mantendo largura e altura independentes.
3. Faça `Square` estender `Shape` diretamente, expondo só `setSide()`, que garante todos os lados iguais.
4. Mantenha `useRectangle` recebendo retângulos e crie `useShape` para aceitar qualquer subtipo de `Shape`.

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](https://crazystack.com.br) 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](https://skalablog.com), 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.

[Source video](https://www.youtube.com/watch?v=2Vi2IpUUWfs)
