DEV Community

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

Posted on • Edited on

1

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.

Billboard image

Monitor more than uptime.

With Checkly, you can use Playwright tests and Javascript to monitor end-to-end scenarios in your NextJS, Astro, Remix, or other application.

Get started now!

Top comments (0)

Billboard image

Imagine monitoring that's actually built for developers

Join Vercel, CrowdStrike, and thousands of other teams that trust Checkly to streamline monitor creation and configuration with Monitoring as Code.

Start Monitoring

👋 Kindness is contagious

Engage with a sea of insights in this enlightening article, highly esteemed within the encouraging DEV Community. Programmers of every skill level are invited to participate and enrich our shared knowledge.

A simple "thank you" can uplift someone's spirits. Express your appreciation in the comments section!

On DEV, sharing knowledge smooths our journey and strengthens our community bonds. Found this useful? A brief thank you to the author can mean a lot.

Okay