Quando estamos estudando os Princípios SOLID, um dos conceitos mais valiosos — e, muitas vezes, o mais confuso — é o Princípio de Substituição de Liskov, também conhecido pela sigla LSP (do inglês, Liskov Substitution Principle). Ele pode parecer abstrato no início, mas dominar ele é super importante para quem busca escrever um código limpo, escalável e resiliente.
Neste post, vou te explicar no detalhe o que é o LSP, sua origem, para que serve e como garantir que seu código respeite esse princípio. Prepare-se para transformar sua forma de pensar sobre herança em programação orientada a objetos.
Quem criou o LSP e por quê?
Em 1988, a cientista da computação Barbara Liskov estabeleceu esse conceito em seu artigo “Data Abstraction and Hierarchy”. Ela notou que, para que a herança seja eficaz, as subclasses precisam cumprir as promessas feitas pelas superclasses. Isso impede erros inesperados no código do cliente, aumentando a capacidade de reutilização e a manutenção dos sistemas.
O que é o Princípio de Substituição de Liskov (LSP)?
O Princípio de Substituição de Liskov afirma que “objetos de uma classe derivada devem poder substituir objetos de sua classe base sem alterar o comportamento esperado do programa”.
Talvez, lendo a frase a cima, você tenha ficado com a impressão de que basta a subclasse “funcionar do mesmo jeito”. Mas isso é apenas a superfície.
LSP trata de contratos, não de código
Imagine uma classe como um acordo: ela se compromete a agir de acordo com normas específicas. Essas normas podem abranger:
- Pré-condições: o que deve ser verdadeiro antes que um método ser executado (por exemplo: “só recebo números acima de zero”).
- Pós-condições: o que é garantido após a execução (por exemplo: “devolvo um objeto criado com sucesso”).
- Invariantes: normas que devem ser sempre verdadeiras (por exemplo: “todo usuário possui um endereço de email que é válido”).
O LSP exige que subclasses honrem essas promessas. Isso significa que:
- Elas não podem exigir mais do que a superclasse exige (ou seja, não devem fortalecer pré-condições).
- Subclasses não podem enfraquecer pós-condições (devem garantir no mínimo o que a superclasse garante).
- Elas devem manter as invariantes definidas pela superclasse.
Benefícios de seguir o LSP no desenvolvimento de software
- Redução de bugs: elimina comportamentos inesperados causados por subclasses incompatíveis.
- Facilidade de refatoração: assegura que modificações nas subclasses não quebrem contratos estabelecidos na base.
- Testabilidade: facilita a criação de testes, já que os comportamentos são previsíveis.
- Reutilização de código: promove hierarquias mais coerentes e sustentáveis.
Em resumo, respeitar o LSP é investir na saúde a longo prazo do seu sistema.
Exemplos práticos: como aplicar (e violar) o LSP
Ao contrário de outros princípios de design que oferecem espaço para diversas interpretações, o princípio da substituição traz alguns requisitos que as subclasses precisam cumprir, principalmente no que diz respeito aos seus métodos. Vamos examinar essa lista:
Parâmetros da subclasse devem ser iguais ou mais genéricos
❌ Violando o LSP
class Pagamento:
def pagar(self, valor: float):
print(f"Pagando R${valor}")
class CartaoCredito(Pagamento):
def pagar(self, valor: int): # ❌ parâmetro mais restrito
print(f"Pagando R${valor} com cartão")Ao sobrescrever métodos, impor limitações mais rigorosas nos parâmetros pode fazer com que a classe derivada não consiga lidar com chamadas válidas previstas pela classe base.
Um trecho de código que aguarda um float pode apresentar erros se receber umint, mesmo que intesteja contido no conjunto dos números de ponto flutuante.
✅ Correto
class CartaoCredito(Pagamento):
def pagar(self, valor: float): # ✅ mesmo tipo, respeita o contrato
print(f"Pagando R${valor} com cartão")Tipo de retorno deve ser o mesmo ou um subtipo
❌ Violando o LSP
class Pagamento:
def pagar(self, valor: float) -> str:
return f"Pago R${valor}"
class CartaoCredito(Pagamento):
def pagar(self, valor: float) -> int: # ❌ retorno mais restrito
return int(valor)Quem espera um str (superclasse) pode quebrar se receber um int (subclasse). Isso quebra o polimorfismo e afeta o funcionamento de código cliente que depende do tipo original.
✅ Correto
class CartaoCredito(Pagamento):
def pagar(self, valor: float) -> str: # ✅ mantém o tipo de retorno
return f"Pago R${valor} com cartão"Não lançar exceções inesperadas
❌ Violando o LSP
class Pagamento:
def pagar(self, valor: float):
print(f"Pagando R${valor}")
class Boleto(Pagamento):
def pagar(self, valor: float):
if valor < 4:
raise Exception("O valor para pagamento com boleto deve ser superior a R$4,00")
else:
print(f"Você pagou R${valor} por boleto")Um método em uma subclasse não deve lançar exceções que não são esperadas pela superclasse. Isso significa que os tipos de exceções devem ser iguais ou mais específicos (subtipos) daqueles que o método base pode lançar.
✅ Correto
# Definimos as exceções base para erros de pagamento
class ErroPagamento(Exception):
def __init__(self, mensagem="Ocorreu um erro no pagamento."):
super().__init__(mensagem)
class ValorPagamentoInvalido(ErroPagamento):
def __init__(self):
super().__init__("O valor do pagamento deve ser positivo.")
class ValorBoletoMuitoBaixo(ValorPagamentoInvalido):
def __init__(self):
super().__init__("O valor mínimo para pagamento com boleto é R$4,00.")
class Pagamento:
def pagar(self, valor: float):
if valor <= 0:
raise ValorPagamentoInvalido()
print(f"Pagamento de R${valor} realizado.")
class Boleto(Pagamento):
def pagar(self, valor: float):
if valor < 4:
raise ValorBoletoMuitoBaixo()
super().pagar(valor)
print("Pagamento via boleto processado.")Não fortalecer pré-condições
Esse foi umas das coisas que eu bati mais a cabeça para entender. Eu achava que criar uma pré-condição sempre iria ferir o princípio de substituição de Liskov, mas eu estava errada.
❌ Violando o LSP
Vamos imaginar uma classe Pagamento com um método pagar(valor). Se o contrato dessa classe permite qualquer valor como entrada e não menciona exceções, então uma subclasse como Boleto, que lança uma exceção para valores menores que R$4,00, está fortalecendo a pré-condição — e, portanto, quebrando o LSP.e
class Pagamento:
def pagar(self, valor: float):
# Qualquer valor é aceito. Não lança exceções.
print(f"Pagando R${valor}")
class Boleto(Pagamento):
def pagar(self, valor: float):
if valor < 4:
raise ValueError("Valor muito baixo para boleto")
print(f"Pagando R${valor} via boleto")
✅ Correto
Agora, se o contrato da superclasse já afirmava que certos valores geram erro, ou seja, se a possibilidade de exceção faz parte da promessa, então a subclasse continua respeitando o LSP, mesmo com validações mais rígidas. Ela está apenas reforçando a pós-condição: “se eu aceitar o valor, o pagamento foi processado com sucesso; caso contrário, uma exceção será lançada”.
# Mesmo exemplo do tópico anterior
class ErroPagamento(Exception):
def __init__(self, mensagem="Ocorreu um erro no pagamento."):
super().__init__(mensagem)
class ValorPagamentoInvalido(ErroPagamento):
def __init__(self):
super().__init__("O valor do pagamento deve ser positivo.")
class ValorBoletoMuitoBaixo(ValorPagamentoInvalido):
def __init__(self):
super().__init__("O valor mínimo para pagamento com boleto é R$4,00.")
class Pagamento:
def pagar(self, valor: float):
if valor <= 0:
raise ValorPagamentoInvalido()
print(f"Pagamento de R${valor} realizado.")
class Boleto(Pagamento):
def pagar(self, valor: float):
if valor < 4:
raise ValorBoletoMuitoBaixo()
super().pagar(valor)
print("Pagamento via boleto processado.")
Como garantir que seu código siga o Princípio de Substituição de Liskov
Esse princípio não exige que subclasses funcionem exatamente como a superclasse — exige que elas mantenham suas promessas.
O que você deve se perguntar:
A subclasse omite comportamentos esperados da superclasse?
Se um método da superclasse é esperado pelo código cliente, a subclasse precisa manter esse comportamento. Remover funcionalidades ou sobrescrevê-las sem equivalente pode quebrar essa expectativa.
A subclasse lança exceções inesperadas para o cliente da superclasse?
O contrato original permite uma chamada sem exceções? Se a subclasse lança novas exceções que a superclasse não previa, você está surpreendendo negativamente o cliente.
A subclasse exige condições de entrada mais rígidas do que a superclasse?
Se a superclasse aceita qualquer número, e a subclasse só aceita positivos, você está mudando as regras do jogo. Isso quebra o contrato da entrada.
A subclasse deixa de cumprir os efeitos esperados após a execução de um método (pós-condições)?
A superclasse garante que um recurso será fechado após uso? A subclasse deve manter essa promessa. Subclasses não podem fazer “menos” do que a superclasse promete.
A subclasse altera invariantes do objeto definidas pela superclasse?
Se a superclasse garante que um campo sempre terá determinado valor ou comportamento, a subclasse não pode modificar essa garantia silenciosamente.
O comportamento da subclasse continua previsível mesmo quando usada no lugar da superclasse?
Se um código escrito para a superclasse se comporta de forma incorreta com a subclasse, o contrato está quebrado — mesmo que “compilando” ou “rodando”.
Conclusão
O Princípio de Substituição de Liskov garante previsibilidade no comportamento de sistemas orientados a objetos. Ele nos força a pensar se realmente estamos criando hierarquias coerentes ou apenas herdando código por conveniência.
Se você quiser aprofundar ainda mais sua compreensão sobre design orientado a objetos e como escrever código mais sustentável, recomendo também a leitura dos meus posts sobre o Princípio da Responsabilidade Única (SRP) e sobre o Princípio Aberto/Fechado (OCP).
Agora queremos ouvir você: já enfrentou situações em que heranças mal aplicadas causaram problemas no seu código? Já utilizou o LSP de forma consciente em algum projeto? Compartilhe suas experiências nos comentários!
FAQ: Perguntas Frequentes sobre o LSP
- O LSP se aplica somente a linguagens orientadas a objetos?
Sim, ele é um princípio fundamental da programação orientada a objetos, mas os conceitos de substituição comportamental podem ser aplicados em outros paradigmas também. - Interfaces ajudam a evitar violações do LSP?
Sim. Interfaces bem definidas ajudam a separar responsabilidades e deixam o comportamento esperado mais claro. - É sempre errado usar herança?
Não. Herança é uma ferramenta poderosa, mas deve ser usada com responsabilidade. Quando há uma relação clara de comportamento do tipo “é-um-a”, ela pode ser apropriada. - Como detectar uma violação do LSP no meu código?
Testes automatizados são ótimos aliados. Troque a classe base por uma subclasse nos testes e observe se o comportamento continua válido.
Deixe um comentário