LSP - Liskov Substitution Principle
Definição formal
Seção intitulada “Definição formal”O princípio origina-se de Barbara Liskov (palestra de 1987, artigo com Jeannette Wing, 1994) na forma da substitutabilidade forte de tipos comportamentais:
Seja
φ(x)uma propriedade demonstrável sobre objetosxdo tipoT. Entãoφ(y)deve ser verdadeira para objetosydo tipoS, ondeSé subtipo deT.
Em termos práticos, um subtipo deve respeitar o contrato do supertipo. O contrato inclui:
- Pré-condições não podem ser fortalecidas no subtipo (o subtipo não pode exigir mais do cliente do que o supertipo exigia).
- Pós-condições não podem ser enfraquecidas no subtipo (o subtipo deve entregar pelo menos o que o supertipo garantia).
- Invariantes do supertipo devem ser preservadas.
- Restrição de histórico: operações no subtipo não podem permitir mudanças de estado proibidas pelo supertipo (ex.: tornar mutável algo imutável).
- Regras de variância: covariância de retornos, contravariância de parâmetros, e nenhuma exceção nova fora da hierarquia esperada.
Compilar não é o suficiente — Java garante a compatibilidade de assinatura, mas não garante a compatibilidade de comportamento, que é o que o LSP exige.
O problema que resolve
Seção intitulada “O problema que resolve”Sem LSP, o polimorfismo torna-se uma armadilha. O cliente que programa contra o supertipo passa a precisar de if (obj instanceof SubtipoProblematico) para tratar casos especiais — o que destrói o OCP - Open-Closed Principle e reintroduz acoplamento. LSP é a condição que faz o polimorfismo (e, portanto, o OCP) ser confiável.
O exemplo canônico é Retângulo/Quadrado: um Quadrado “é um” Retângulo do ponto de vista matemático, mas se Retangulo expõe setLargura e setAltura independentes, Quadrado precisa violar essa independência (ao setar largura, muda altura junto). Um cliente que espera area == largura * altura após setLargura(5) num objeto Retangulo que na verdade é Quadrado obtém resultado surpreendente. O “é-um” intuitivo não implica substitutabilidade comportamental.
Como identificar violações
Seção intitulada “Como identificar violações”Trade-offs e quando não aplicar dogmaticamente
Seção intitulada “Trade-offs e quando não aplicar dogmaticamente”LSP não é opcional no sentido de “às vezes vale a pena violar” — violá-lo silenciosamente é sempre um bug latente. O que é discutível é o modelo de herança escolhido. Muitas violações de LSP são, na verdade, sintomas de que a relação não deveria ser de herança (is-a), mas de composição (has-a) ou de interfaces separadas (ISP - Interface Segregation Principle).
Exemplo em Java + Spring Boot 3
Seção intitulada “Exemplo em Java + Spring Boot 3”Antes — violando LSP
Seção intitulada “Antes — violando LSP”ContaPoupanca fortalece a pré-condição de sacar, lançando exceção onde a superclasse permitiria a operação, quebrando clientes que dependem do contrato de Conta.
public class Conta {
protected BigDecimal saldo;
public Conta(BigDecimal saldoInicial) { this.saldo = saldoInicial; }
public void sacar(BigDecimal valor) { this.saldo = this.saldo.subtract(valor); }
public BigDecimal saldo() { return saldo; }}
public class ContaPoupanca extends Conta {
public ContaPoupanca(BigDecimal saldoInicial) { super(saldoInicial); }
@Override public void sacar(BigDecimal valor) { if (valor.compareTo(new BigDecimal("100")) > 0) { throw new UnsupportedOperationException("Poupança não permite saque acima de 100"); } super.sacar(valor); }}Um serviço que recebe Conta e faz conta.sacar(valor) funciona para Conta, mas explode para ContaPoupanca. O cliente precisaria de instanceof para se proteger — violação de LSP.
Depois — contrato honrado com abstração adequada
Seção intitulada “Depois — contrato honrado com abstração adequada”Modelamos a política de saque explicitamente no contrato: sacar pode falhar de forma prevista e retorna um resultado. A pós-condição comum que todos os tipos honram: o saque nunca ultrapassa o disponível daquela conta (o saldo puro na poupança; saldo + limite na corrente, onde o saldo pode ficar negativo até o limite) e a recusa é sinalizada de forma uniforme via ResultadoSaque — nunca por exceção inesperada.
public sealed interface Conta permits ContaCorrente, ContaPoupanca { ResultadoSaque sacar(BigDecimal valor); BigDecimal saldo();}
public sealed interface ResultadoSaque permits SaqueRealizado, SaqueRecusado {}
public record SaqueRealizado(BigDecimal novoSaldo) implements ResultadoSaque {}
public record SaqueRecusado(String motivo) implements ResultadoSaque {}public final class ContaCorrente implements Conta {
private BigDecimal saldo; private final BigDecimal limite;
public ContaCorrente(BigDecimal saldoInicial, BigDecimal limite) { this.saldo = saldoInicial; this.limite = limite; }
@Override public ResultadoSaque sacar(BigDecimal valor) { BigDecimal disponivel = saldo.add(limite); if (valor.compareTo(disponivel) > 0) { return new SaqueRecusado("Saldo e limite insuficientes"); } this.saldo = this.saldo.subtract(valor); return new SaqueRealizado(saldo); }
@Override public BigDecimal saldo() { return saldo; }}public final class ContaPoupanca implements Conta {
private BigDecimal saldo;
public ContaPoupanca(BigDecimal saldoInicial) { this.saldo = saldoInicial; }
@Override public ResultadoSaque sacar(BigDecimal valor) { if (valor.compareTo(saldo) > 0) { return new SaqueRecusado("Saldo insuficiente"); } this.saldo = this.saldo.subtract(valor); return new SaqueRealizado(saldo); }
@Override public BigDecimal saldo() { return saldo; }}@Servicepublic class ServicoSaque {
public String executar(Conta conta, BigDecimal valor) { ResultadoSaque resultado = conta.sacar(valor); return switch (resultado) { case SaqueRealizado r -> "Saque ok, saldo: " + r.novoSaldo(); case SaqueRecusado r -> "Recusado: " + r.motivo(); }; }}Agora ServicoSaque trata qualquer Conta de maneira uniforme: nenhuma implementação lança exceção surpresa, e a recusa é parte explícita do contrato (pós-condição honrada por todos). Não há instanceof de proteção.
Diagrama
Seção intitulada “Diagrama”classDiagram
class Conta {
<<interface>>
+sacar(BigDecimal) ResultadoSaque
+saldo() BigDecimal
}
class ContaCorrente {
+sacar(BigDecimal) ResultadoSaque
+saldo() BigDecimal
}
class ContaPoupanca {
+sacar(BigDecimal) ResultadoSaque
+saldo() BigDecimal
}
class ResultadoSaque {
<<interface>>
}
class SaqueRealizado
class SaqueRecusado
Conta <|.. ContaCorrente
Conta <|.. ContaPoupanca
ResultadoSaque <|.. SaqueRealizado
ResultadoSaque <|.. SaqueRecusado
Conta ..> ResultadoSaque
Anti-padrões e erros comuns
Seção intitulada “Anti-padrões e erros comuns”Relações
Seção intitulada “Relações”- Faz parte de: SOLID
- Sustenta: OCP - Open-Closed Principle (polimorfismo confiável é pré-requisito para estender sem modificar).
- Complementa: ISP - Interface Segregation Principle (interfaces menores tornam contratos mais fáceis de honrar) e DIP - Dependency Inversion Principle.
- Impacta modelagem de agregados e entidades em Domain-Driven Design.
- Índice: Home
Perguntas de revisão
Seção intitulada “Perguntas de revisão”- Compilar sem erros garante conformidade com LSP? Por quê?
Resposta
Não. O compilador garante compatibilidade de assinatura (sintaxe), mas LSP exige compatibilidade de comportamento — respeitar pré-condições, pós-condições e invariantes do supertipo, o que o compilador não verifica.
- O que significa “não fortalecer pré-condições” e “não enfraquecer pós-condições”?
Resposta
O subtipo não pode exigir mais do cliente do que o supertipo exigia (pré-condição), e deve entregar pelo menos o que o supertipo garantia (pós-condição). Violar qualquer um quebra a substitutabilidade.
- Por que o problema Retângulo/Quadrado viola LSP?
Resposta
Porque um cliente que espera setar largura e altura independentemente (invariante de Retangulo) obtém comportamento surpreendente com Quadrado, que acopla os dois lados. O “é-um” intuitivo não corresponde à substitutabilidade comportamental.
- Qual refatoração costuma eliminar violações de LSP causadas por herança forçada?
Resposta
Trocar herança por composição/delegação — Replace Inheritance with Delegation. Quando a subclasse só aproveita parte do supertipo (o smell Refused Bequest), a herança a obriga a honrar um contrato que ela não cumpre. Delegar a um colaborador elimina a relação de substituição indevida, e costuma vir acompanhada de Extract Interface para expor só o papel realmente necessário (ligando a ISP).