DEV Community

Alberto Luiz Souza
Alberto Luiz Souza

Posted on

Entendendo o Princípio de Substituição de Liskov na Prática

Disclaimer

Este texto foi inicialmente concebido pela IA Generativa em função da transcrição do episódio do nosso canal, Dev Eficiente. Se preferir acompanhar por vídeo, é só dar o play.

Introdução

O Princípio de Substituição de Liskov (LSP) é frequentemente citado como uma das bases do design orientado a objetos, mas sua definição pode parecer complexa à primeira vista. Para muitos desenvolvedores, as explicações teóricas sobre pré-condições e pós-condições, geralmente apresentadas em textos formais, não ajudam muito na compreensão prática. Neste post, vamos explorar o LSP de maneira simples e prática, usando exemplos de código para facilitar o entendimento.

O Que Diz o Princípio de Liskov?

De forma simplificada, o LSP afirma que uma subclasse deve ser substituível por sua classe pai sem que o comportamento esperado do sistema seja quebrado. Isso significa que, se uma classe filha não respeitar os contratos definidos pela classe pai, o sistema pode apresentar comportamentos inesperados.

Contratos: Pré-condições e Pós-condições

  • Pré-condição: O que o método precisa para funcionar corretamente.
  • Pós-condição: O que o método garante que será retornado após sua execução.

O LSP estipula que:

  • Pré-condições não podem ser reforçadas na subclasse, ou seja, a subclasse não pode exigir mais do que a classe pai.
  • Pós-condições não podem ser enfraquecidas, ou seja, a subclasse deve retornar resultados dentro das expectativas da classe pai.

Exemplificando o Princípio

Cenário Inicial: A Classe Base

Considere uma classe chamada ImpostoPadrão que calcula impostos. Ela possui um método calculaImposto que aceita um valor numérico positivo e retorna um valor positivo correspondente ao imposto:

public class ImpostoPadrão {
    public double calculaImposto(double valor) {
        if (valor <= 0) {
            throw new IllegalArgumentException("Valor inválido");
        }
        // Implementação do cálculo (exemplo simplificado)
        return valor * 0.1;
    }
}
Enter fullscreen mode Exit fullscreen mode

Essa classe define que:

  • A pré-condição é que o valor seja positivo.
  • A pós-condição é que o método sempre retorne um número maior que zero.

A Subclasse Problemática

Agora, imagine que criamos uma subclasse chamada ImpostoSobreServiço que modifica o comportamento do método calculaImposto para aceitar valores maiores que 100. Essa alteração reforça a pré-condição, causando possíveis problemas:


public class ImpostoSobreServiço extends ImpostoPadrão {
    @Override
    public double calculaImposto(double valor) {
        if (valor <= 100) {
            throw new IllegalArgumentException("Valor inválido para serviço");
        }
        return valor * 0.15;
    }
}


Enter fullscreen mode Exit fullscreen mode

Se um código existente dependente da classe ImpostoPadrão for reutilizado com a subclasse ImpostoSobreServiço, ele poderá falhar ao passar valores entre 0 e 100, mesmo que esses valores sejam válidos para a classe pai.

Ajustando para Respeitar o LSP

Para respeitar o LSP, a subclasse deve aceitar pelo menos os mesmos valores que a classe pai aceita. Por exemplo, poderíamos ajustar a pré-condição para algo mais permissivo:


public class ImpostoSobreServiço extends ImpostoPadrão {
    @Override
    public double calculaImposto(double valor) {
        if (valor <= 0) {
            throw new IllegalArgumentException("Valor inválido");
        }
        return valor * 0.15;
    }
}


Enter fullscreen mode Exit fullscreen mode

Nesse caso, qualquer código que espera a classe ImpostoPadrão funcionará perfeitamente com a subclasse, sem surpresas.

Ampliando o LSP para Outros Contextos

O LSP não se limita apenas à herança em linguagens orientadas a objetos. Ele também pode ser aplicado a contratos de APIs, interfaces e até mesmo a sistemas distribuídos. Por exemplo:

  • Alterar um contrato de API para aceitar menos entradas ou retornar resultados inesperados pode quebrar consumidores existentes.
  • Alterações em interfaces ou contratos devem considerar o impacto nos clientes dependentes.

Conclusão

O Princípio de Substituição de Liskov nos lembra de sempre projetar sistemas pensando nos contratos e nas expectativas dos clientes. Seja em classes, APIs ou interfaces, mudanças devem ser realizadas com cuidado para não causar surpresas desagradáveis. Respeitar o LSP não é apenas uma boa prática de design; é essencial para criar sistemas robustos e confiáveis.

Sobre a Jornada Dev + Eficiente

A Jornada Dev + Eficiente é um treinamento focado em fazer você crescer na carreira como uma pessoa cada vez mais especializada em Design e Arquitetura de Software.

A Jornada pavimenta este caminho fazendo com que você seja cada vez mais capaz de colocar código de qualidade em produção com cada vez mais velocidade.

Para conhecer mais, acesse https://deveficiente.com/kr/lp

Top comments (0)