DEV Community

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

Posted on • Edited 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)