DEV Community

Cover image for LSP - O Princípio da Substituição de Liskov
Thiago Souza
Thiago Souza

Posted on

LSP - O Princípio da Substituição de Liskov

O LSP (Liskov Substitution Principle) é mais um dos 5 princípios SOLID que todo bom programador e/ou arquiteto de software deveria conhecer. No entanto requer uma atenção especial pois ele é abstrato e mais difícil de entender, principalmente para quem está começando com SOLID.

Definição do LSP

Barbara Liskov, em maio de 1988, escreveu o seguinte texto para definir subtipos:

Se, para cada objeto o1 de tipo S, houver um objeto o2 de tipo T, de modo que, para todos os programas de tipo P não seja modificado quando o1 for substituído por o2, então S é um subtipo de T.

Nazaré Confusa

Sim! O nó no cérebro acontece de forma natural quando você tem o primeiro contato com esta definição. Mas não se desespere pois o texto escrito por Liskov pode ser simplificado e entendido como:

Sempre que o programa esperar receber uma instância de um tipo T, um subtipo S derivado de T deve poder substituí-lo sem que o programa precise de qualquer adequação para atender uma instância do subtipo S. Caso contrário, S não deveria ser um subtipo derivado de T.

Importante: ⚠️

👉 O LSP não se trata apenas de uma orientação sobre o uso da herança. Com o passar dos anos, o LSP se transformou em um princípio de design de software aplicável a qualquer tipo de interface e implementação.

👉 De forma mais abrangente, o segredo do sucesso é preservar as expectativas e garantir que o comportamento dos subtipos seja compatível com a intenção do tipo base.

Fechou? Agora podemos continuar e você já pode tirar um print desta informação e fazer um belo papel de parede! 😊

Violação do LSP

Montei um exemplo em .NET C# (simples e para fins didáticos) que deve ajudar a entender uma das formas de violar o Princípio da Substituição de Liskov.

Observe com calma cada uma das classes:

public class Vehicle 
{
    public bool IsIgnitionOn { get; protected set; }

    public bool IsMoving { get; private set; }

    public virtual void StartIgnition()
    {
        this.IsIgnitionOn = true;
    }

    public void StartMove()
    { 
        this.IsMoving = true;
    }
}

public class Car : Vehicle
{
    public override void StartIgnition()
    {
        this.IsIgnitionOn = true;
    }
}

public class Motorcycle : Vehicle
{
    public override void StartIgnition()
    {
        this.IsIgnitionOn = true;
    }
}

public class Bicycle : Vehicle
{
    public override void StartIgnition()
    {
        throw new NotImplementedException();
    }
}
Enter fullscreen mode Exit fullscreen mode

Agora vamos ver o que acontece quando executamos o método StartIgnition para diferentes instâncias de Vehicle:

public void StartVehicleIgnition(Vehicle vehicle)
{
    vehicle.StartIgnition();

    if (vehicle.IsIgnitionOn) 
    {
        Console.WriteLine(
            $"O veículo está com a ignição ligada.");
    }
}

StartVehicleIgnition(new Vehicle()); 
// ✅ O veículo está com a ignição ligada.

StartVehicleIgnition(new Car()); 
// ✅ O veículo está com a ignição ligada.

StartVehicleIgnition(new Motorcycle()); 
// ✅ O veículo está com a ignição ligada.

StartVehicleIgnition(new Bicycle()); 
// ❌ NotImplementedException
Enter fullscreen mode Exit fullscreen mode

Neste caso fica evidente que, apesar de a bicicleta ser um veículo, no escopo deste programa, uma instância de Bicycle não consegue se adequar ao método StartIgnition e muito menos à propriedade IsIgnitionOn que representa um estado interno herdado da classe base Vehicle.

Alterar o programa, incluindo um tratamento de erro para atender única e exclusivamente uma ou mais instâncias de Bicycle, deveria ser considerado um crime.

Mas como eu sempre digo: Você não está aqui por acaso! A entropia do universo, de uma forma singular, te trouxe até aqui e agora você terá um bom exemplo para compartilhar com os seus colegas de trabalho para que eles também não cometam mais esse tipo erro.

Aplicação do LSP

Para corrigir e evitar que problemas como estes ocorram, poderiamos seguir com a seguinte abordagem:

public class Vehicle 
{
    public bool IsMoving { get; private set; }

    public void StartMove()
    {  
        this.IsMoving = true;
    }
}

public interface IVehicleMotorized 
{
    decimal IsIgnitionOn { get; }

    void StartIgnition();
}

public class Car : Vehicle, IVehicleMotorized 
{
    public decimal IsIgnitionOn { get; private set; }

    public void StartIgnition()
    {
        this.IsIgnitionOn = true;
    }
}

public class Motorcycle : Vehicle, IVehicleMotorized 
{
    public decimal IsIgnitionOn { get; private set; }

    public void StartIgnition()
    {
        this.IsIgnitionOn = true;
    }
}

public class Bicycle : Vehicle
{
    // Atributos e métodos específicos para bicicletas.
}
Enter fullscreen mode Exit fullscreen mode

Neste caso criamos uma interface denominada IVehicleMotorized e fizemos as classes Car e Motorcycle implementarem as propriedades e comportamentos da nossa nova abstração.

Quanto à classe Vehicle, removemos tudo o que não pode ser utilizado em bicicletas e deixamos apenas comportamentos/métodos que estejam diretamente relacionados com o que chamamos de veículos no escopo deste programa.

Vamos refatorar o trecho onde utilizamos o método StartIgnition e a propriedade IsIgnitionOn para garantir que apenas classes que implementem a interface IVehicleMotorized possam ser acionadas no método StartVehicleIgnition:

public void StartVehicleIgnition(IVehicleMotorized  vehicle)
{
    vehicle.StartIgnition();

    if (vehicle.IsIgnitionOn) 
    {
        Console.WriteLine(
            $"O veículo está com a ignição ligada.");
    }
}

StartVehicleIgnition(new Car()); 
// ✅ O veículo está com a ignição ligada.

StartVehicleIgnition(new Motorcycle()); 
// ✅ O veículo está com a ignição ligada.
Enter fullscreen mode Exit fullscreen mode

Para concluir, vamos criar um programa de exemplo onde nenhum comportamento inesperado acontece ao realizar a substituição da classe base Vehicle por qualquer uma das outras classes derivadas:

public void StartMoveVehicle(Vehicle vehicle)
{
    vehicle.StartMove();

    if (vehicle.IsMoving) {
        Console.WriteLine("O veículo está em movimento.");
    }
}

StartMoveVehicle(new Vehicle()); 
// ✅ O veículo está em movimento.

StartMoveVehicle(new Car()); 
// ✅ O veículo está em movimento.

StartMoveVehicle(new Motorcycle()); 
// ✅ O veículo está em movimento.

StartMoveVehicle(new Bicycle()); 
// ✅ O veículo está em movimento.
Enter fullscreen mode Exit fullscreen mode

Finalizamos por aqui! 😊

Como mencionei anteriormente, este foi um exemplo de uma dentre outras formas de violação deste princípio. Esta discussão pode ser bem extensa e acho que merece um vídeo no futuro. 👀

Antes de ir embora, me diz aí: você já cometeu ou testemunhou algum tipo de violação do LSP?


Obrigado pela sua atenção e espero que este artigo tenha sido útil para você.

Me siga para receber mais conteúdos como este. ❤️


Recomendação de leitura:
Arquitetura Limpa - O Guia do Artesão para Estrutura e Design de Software
Robert Cecil Martin, 2019.

Top comments (0)