LSP: a essência do princípio de substituição de Barbara Liskov no desenvolvimento

Autor: IT Sectr Publicado: 2026-05-12 Tempo de leitura: 9 min

LSP (Liskov Substitution Principle) — o terceiro princípio do SOLID, que define as condições para herança correta na programação orientada a objetos. O princípio foi formulado por Barbara Liskov em 1987 e formalizado como: se S é um subtipo de T, então objetos do tipo T podem ser substituídos por objetos do tipo S sem alterar as propriedades do programa. Conforme observado no livro de Robert Martin Clean Architecture (2017), o princípio de substituição exige que uma subclasse não enfraqueça o contrato da classe base.

Pontos principais

  • LSP — princípio de substituição de Liskov, terceiro princípio SOLID sobre herança correta
  • Subclasse deve preservar o contrato da classe base — pré-condições e pós-condições
  • Violação de LSP se manifesta no «problema do quadrado e retângulo» e exceções lançadas
  • Composição é frequentemente preferível à herança para conformidade com LSP
  • Design por Contrato (Design by Contract) — forma formal de verificar LSP

O que é LSP (Liskov Substitution Principle)?

LSP (Liskov Substitution Principle) — princípio de substituição formulado por Barbara Liskov na conferência OOPSLA em 1987. Definição formal: seja q(x) uma propriedade demonstrável de objetos x do tipo T. Então q(y) deve ser demonstrável para objetos y do tipo S, onde S é um subtipo de T. Simplificando: objetos de uma subclasse devem se comportar de modo que o código que trabalha com a classe base continue funcionando corretamente também com a subclasse.

Na prática, LSP significa que uma subclasse não deve violar o contrato da classe base. O contrato inclui pré-condições (o que é necessário para chamar um método), pós-condições (o que é garantido após a chamada) e invariantes (condições que persistem ao longo da vida do objeto). Uma subclasse pode fortalecer pré-condições ou enfraquecer pós-condições — isso é precisamente uma violação de LSP.

Um exemplo clássico de violação de LSP é um quadrado herdando de um retângulo. O método setWidth do retângulo define a largura, enquanto no quadrado define tanto a largura quanto a altura. Um cliente que espera o comportamento de um retângulo (alterar um lado não afeta o outro) obtém um resultado inesperado. Um quadrado não é um subtipo válido de um retângulo.

Condições formais do LSP

LSP estabelece três condições para herança correta: as pré-condições da subclasse não podem ser mais fortes que as pré-condições da classe base (a subclasse não exige mais), as pós-condições da subclasse não podem ser mais fracas que as pós-condições da classe base (a subclasse garante não menos), e os invariantes da classe base devem ser preservados na subclasse. Essas condições são conhecidas como a regra de Design por Contrato segundo Bertrand Meyer.

Se pelo menos uma condição for violada, o código que usa polimorfismo pode falhar. O compilador não verifica contratos semânticos, apenas sintáticos. Portanto, LSP é uma questão de disciplina arquitetural, não de tipagem estática.

Como funciona o princípio de substituição de Liskov

O mecanismo do LSP baseia-se na compatibilidade comportamental dos tipos. Se a classe S herda da classe T, o código cliente deve poder usar S onde T é esperado sem alterar seu comportamento. Isso inclui não apenas as assinaturas dos métodos, mas também sua semântica.

LSP não proíbe uma subclasse de adicionar novo comportamento. É proibido violar as expectativas do código escrito para a classe base. Se a classe base garante que o método save não lança exceções, a subclasse não deve lançá-las. Se a classe base retorna um valor não negativo, a subclasse não deve retornar um negativo.

Em projetos reais, o LSP é mais frequentemente violado ao adicionar lógica condicional nos métodos da subclasse: «se condição — lançar exceção», «se condição — retornar null». Cada uma dessas «surpresas» prejudica o polimorfismo e obriga o código cliente a verificar o tipo do objeto antes de chamar — o que contradiz a própria ideia do design orientado a objetos.

Em projetos mobile, uma violação típica de LSP ocorre ao criar ViewModels base. Se BaseViewModel garante que o método onCleared libera todos os recursos, e uma subclasse sobrescreve este método como vazio — qualquer código que depende da limpeza de recursos através de uma chamada polimórfica a onCleared funcionará incorretamente. LSP exige que a subclasse chame super.onCleared() ou realize o mesmo trabalho por si mesma. Composição via LifecycleObserver é uma alternativa que elimina a violação de LSP no gerenciamento do ciclo de vida.

Sinais de violação de LSP no código

Indicadores principais de violação de LSP incluem: verificar o tipo do objeto via instanceof ou is antes de chamar um método, implementações vazias de métodos (stubs), lançar NotImplementedError ou UnsupportedOperationException, retornar null em vez de um valor. Cada um desses padrões sinaliza que a subclasse não é um subtipo válido.

Outro sinal comum é a herança com o objetivo de reutilizar código em vez de modelar uma relação «é-um» (is-a). A classe Bird tem um método fly(). A classe Penguin herda de Bird e sobrescreve fly() como vazio ou lançando uma exceção. Isso é uma violação de LSP: um pinguim não é um subtipo válido de ave.

No desenvolvimento mobile, LSP é violado ao criar classes base ViewHolder, Fragment ou ViewController com métodos stub. Se uma subclasse não usa metade dos métodos da classe base — a herança foi escolhida incorretamente. Composição ou segregação de interfaces resolve o problema de forma mais correta.

Teste de LSP

Um teste simples para verificar LSP: escreva um teste unitário para a classe base que verifique seu contrato (valores retornados, exceções, efeitos colaterais). Execute este teste para cada subclasse. Se o teste falhar — LSP foi violado. Esta abordagem é chamada «teste através do contrato da classe base».

Em projetos Android, tal teste é útil para ViewModel e Repository. Se BaseViewModel garante um estado Loading antes de um erro, e uma subclasse lança um erro sem Loading — o teste detectará a violação de LSP no estágio de CI.

Exemplos de LSP no desenvolvimento mobile

Vejamos um exemplo Android com o tratamento de ClickListener. Uma violação de LSP ocorre quando a implementação base garante algo e a subclasse viola.

kotlin
// Classe base com garantia: onClick será chamado
open class BaseClickListener {
    open fun onClick(view: View) {
        // tratamento básico
    }
}

// Violação de LSP: subclasse adiciona condição que lança exceção
class RestrictedClickListener : BaseClickListener() {
    override fun onClick(view: View) {
        if (!isLoggedIn) {
            throw IllegalStateException("Not logged in")
        }
        super.onClick(view)
    }
}

// Solução correta: contrato não violado
class ConditionalClickListener : BaseClickListener() {
    override fun onClick(view: View) {
        if (isLoggedIn) {
            super.onClick(view)
        }
    }
}

Um exemplo iOS com o protocolo DataSource demonstra violação de LSP ao retornar nil em vez de dados:

swift
// Protocolo com contrato: retorna dados ou erro
protocol DataProvider {
    func fetchData() async throws -> [String]
}

// Violação de LSP: retorna nil sem erro
class SilentFailProvider: DataProvider {
    func fetchData() async throws -> [String] {
        return [] // array vazio em vez de erro
    }
}

// Conformidade correta com LSP
class NetworkProvider: DataProvider {
    func fetchData() async throws -> [String] {
        throw NetworkError.timeout
    }
}

Regra prática: se uma subclasse não pode cumprir o contrato da classe base, ela não deveria ser uma subclasse. Uma alternativa é extrair uma interface com um contrato mínimo e implementá-la em cada tipo à sua maneira.

LSP e herança: quando escolher composição

A composição é preferível à herança em situações onde a relação «é-um» (is-a) é ambígua ou condicional. Um exemplo clássico: Manager é um Employee? Sim. Mas Square é um Rectangle válido? LSP diz «não». Se você duvida da correção da herança — escolha composição.

No desenvolvimento mobile, a composição é frequentemente usada através de injeção de dependências: em vez de herdar o comportamento de uma classe base, uma classe o recebe através de um construtor. Um ViewModel não herda de Repository, mas o aceita como dependência. Isso elimina a violação de LSP por definição — sem herança, não há violação de contrato.

Sinais de que a herança deve ser substituída por composição: a subclasse não usa alguns métodos da classe base, a subclasse sobrescreve métodos com stubs vazios, o código cliente verifica o tipo do objeto via instanceof. Nestes casos, a herança foi escolhida incorretamente e LSP foi violado.

Solução através de interfaces

As interfaces resolvem o problema de LSP sem herança: cada tipo implementa exatamente os métodos de que precisa. Em vez de uma classe base comum Bird com um método fly() (onde Penguin não voa) — uma interface Flyable que apenas as aves voadoras implementam. Penguin implementa Bird sem o método fly() — LSP não é violado.

Na arquitetura Android, esta abordagem é aplicada através de interfaces segregadas de UseCase: em vez de um grande UseCase com métodos getAll, getById, save, delete — interfaces separadas GetItemsUseCase, SaveItemUseCase. Um cliente depende apenas da interface necessária, e qualquer classe que implemente essa interface é correta da perspectiva do LSP.

Perguntas frequentes

Como o LSP difere da herança simples?

A herança é um mecanismo da linguagem; LSP é uma regra para o uso correto desse mecanismo. A herança garante compatibilidade de assinaturas (sintaxe); LSP exige compatibilidade de comportamento (semântica). Herança sem LSP dá polimorfismo que quebra em tempo de execução.

Null em uma subclasse sempre viola LSP?

Se a classe base garante um retorno não nulo — sim. Se o contrato permite null (valor opcional) — não. LSP não proíbe null; proíbe enfraquecer o contrato. Estude a documentação da classe base e verifique se o contrato da subclasse é compatível.

Como o LSP se aplica a protocolos em Swift?

LSP se aplica a protocolos da mesma forma que a classes. Uma implementação de protocolo deve cumprir o contrato semântico: se um protocolo define um método como non-throwing, a implementação não deve lançar erros. Swift não verifica isso no nível do compilador — a responsabilidade é do desenvolvedor.

O LSP pode ser violado ao usar sealed class?

Sealed class em Kotlin é um caso especial porque a hierarquia é fechada e conhecida pelo compilador. LSP se aplica a sealed class em menor grau porque todos os subtipos são explicitamente enumerados na expressão when. Um erro de uma subclasse selada será local, não um erro polimórfico oculto.

Como testar a conformidade com LSP em um projeto?

Escreva um teste parametrizado para a classe base que é executado para todas as suas subclasses. O teste verifica contratos comportamentais chave: valores retornados, exceções, estados. Se o teste falhar em uma das subclasses — LSP foi violado. Em CI, tal teste previne regressão de código polimórfico.

Resumo

  • LSP (Liskov Substitution Principle) — princípio de substituição, terceiro no SOLID, sobre compatibilidade semântica da herança
  • Subclasse deve preservar o contrato da classe base: pré-condições, pós-condições e invariantes
  • Verificações instanceof e sobrescritas vazias de métodos são os principais sinais de violação de LSP
  • Composição e interfaces resolvem o problema de LSP onde a herança é incorreta
  • O problema do quadrado e retângulo é um exemplo clássico de incompatibilidade de subtipos
  • Um teste de contrato para a classe base, executado para todas as subclasses, detecta violação de LSP em CI
  • Sealed class em Kotlin reduz riscos de LSP devido à hierarquia fechada conhecida pelo compilador

Vamos desenvolver um aplicativo móvel chave na mão

A IT Sectr cria aplicativos para iOS e Android para startups e empresas desde 2017. Nós vamos aconselhá-lo e propor a melhor solução.

Discutir o projeto

Leia também