C# - Exemplos de violação do princípio LSP


  Hoje veremos alguns exemplo de violação do princípio SOLID LSP.

O princípio da substituição de Liskov (LSP) , um dos princípios SOLID, declara que uma classe base deve poder ser substituída por sua classe derivada sem alteração no comportamento final.

O princípio LSP foi definido por Barbara Liskov da seguinte forma:

"Se q(x) é uma propriedade demonstrável dos objetos x de tipo T. Então q(y) deve ser verdadeiro para objetos y de tipo S onde S é um subtipo de T."

Nas entrelinhas:

"Se você pode invocar um método q() de uma classe T (base), deve poder também invocar o método q() de uma classe T'(derivada) que é derivada com herança de T (base)."

De forma bem simples o princípio de Liskov sugere que :

"Sempre que uma classe cliente esperar uma instância de uma classe base X, uma instância de uma subclasse Y de X deve poder ser usada no seu lugar."

Em outras palavras: "Uma classe base deve poder ser substituída pela sua classe derivada."

Por que o princípio de LisKov é importante ?

Porque sem aplicar o princípio de Liskov a hierarquia de classes seria uma bagunça e os testes de unidade para a superclasse nunca teriam sucesso para a subclasse.

A seguir vou apresentar alguns exemplos onde ocorre a violação do princípio. Eles não cobrem todos os casos possíveis casos de violação mas são bem comuns e fáceis de serem identificados.

Para apresentar os exemplos vou usar projetos do tipo Console usando o VS 2022 e o ambiente do  .NET 7.0 usando o recurso Top Level Statement de forma a ter um código simples e direto.

1- Violação

Lançar uma exceção do tipo NotImplementedException de uma classe derivada é um sinal de que a relação de herança pai-filho não está seguindo o princípio LSP.    

No trecho de código a seguir temos classe IPassaro que não pode representar um Kiwi, pois a classe Kiwi não tem uma implementação para o comportamento voar.

IPassaro passaro = new Kiwi();

Console.WriteLine(passaro.ContaOvos());

passaro.Voar();

Console.ReadKey();

interface IPassaro
{
 
int ContaOvos();
 
void Voar();
}

public
class Pardal : IPassaro
{
 
public int ContaOvos()
  {
   
return 4;
  }

 
public void Voar()
  {
    Console.WriteLine(
"Pardal voando...");
  }
}

public class Kiwi : IPassaro
{
 
public int ContaOvos()
  {
    
return 2;
  }

 
public void Voar()=> throw new NotImplementedException();

}

2 - Violação

Ocultar um método pai virtual em uma classe filha usando a palavra-chave new também viola o LSP.

No código abaixo, quando o tipo de declaração é alterado da classe base FuncionarioBonusAnual para a classe derivada GerenteBonusAnual, o valor do bônus anual é alterado, o que significa que eles não podem ser substituídos sem alterar o resultado esperado.

var funciAnualBonus = new FuncionarioBonusAnual();
Console.WriteLine(funciAnualBonus.GetBonus());

var gerenteAnualBonus = new FuncionarioBonusAnual();
Console.WriteLine(gerenteAnualBonus.GetBonus());

Console.ReadKey();

public class FuncionarioBonusAnual
{
 
public virtual decimal GetBonus()
  {
   
return 4000.00m;
  }
}

public class GerenteBonusAnual : FuncionarioBonusAnual
{
 
public new decimal GetBonus()
  {
   
return 6000.00m;
  }
}

3 - Violação

Retornar um valor de um tipo que possui restrições desconhecidas da assinatura do método. Como retornar um valor do tipo ReadOnlyCollection de um método da classe derivada enquanto o tipo de retorno em sua assinatura é ICollection.

O código abaixo demonstra como essa abordagem está gerando um comportamento inesperado para o consumidor do código.

using System.Collections.ObjectModel;

IFuncionario funcionarios = new Funcionarios();

ICollection<string> listaNomes = funcionarios.GetFuncionariosNomes();

Console.ReadKey();

public interface IFuncionario
{
   ICollection<
string> GetFuncionariosNomes();
}

public class Funcionarios : IFuncionario
{
  
public ICollection<string> GetFuncionariosNomes()
   {
     
return new ReadOnlyCollection<string>(new List<string>
      {
         
"Maria", "Amanda", "Ana", "Carlos", "Paulo","Diná"
       });
    }
}

Desta forma, recomenda-se manter os componentes de código (classes ou projetos) separados e depender de abstrações em vez de implementações concretas, o que torna a aplicação do princípio da Substituição de Liskov mais importante.

 A implementação das classes derivadas pode não ser conhecida pelo cliente ou em tempo de design e implementar um método que retorne um valor ou aceite parâmetros de uma maneira que não esteja clara em sua interface (ou classe base) enganará quem chama esse código que por sua vez, pode interromper a execução do código ou resultar em comportamentos inesperados em tempo de execução

E estamos conversados...

"E do modo por que Moisés levantou a serpente no deserto, assim importa que o Filho do Homem seja levantado, para que todo o que nele crê tenha a vida eterna."
João 3:14-15

Referências:


José Carlos Macoratti