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.
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();
}
}
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
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.
}
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.
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.
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)