C# - Violações dos princípios SOLID - III


 Hoje veremos violações dos principíos SOLID que fazem parte do dia a dia de todo o desenvolvedor.

Continuando o artigo anterior vamos tratar com as violações do princípio LSP.

O Princípio da Substituição de Liskov (Liskov Substitution Principle, ou LSP) é um dos princípios SOLID que foi introduzido por Barbara Liskov em 1987. O princípio é baseado no conceito de substituibilidade de objetos e afirma que:

"Se S é um subtipo de T, então objetos do tipo T podem ser substituídos por objetos do tipo S sem afetar a corretude do programa".

Em outras palavras, isso significa que uma classe derivada ou subtipo deve ser capaz de ser usada no lugar de sua classe base ou supertipo, sem quebrar o comportamento esperado do programa.

Violação do princípio LSP

Uma violação comum do princípio da substituição de Liskov (LSP - Liskov Substitution Principle) é quando uma subclasse não pode ser usada de forma transparente no lugar de sua classe base.

Por exemplo, considere um sistema de gerenciamento de animais de estimação que possua uma classe base Pet e duas subclasses Dog e Cat. A classe Pet possui um método chamado Feed(), que alimenta o animal de estimação.

A classe Dog sobrescreve o método Feed() para exibir a mensagem "O cachorro está sendo alimentado", enquanto a classe Cat sobrescreve o método Feed() para exibir a mensagem "O gato está sendo alimentado".

Agora, suponha que o sistema possui uma método que recebe um objeto do tipo Pet e chama o método Feed(). Se a classe Cat sobrescrever o método Feed() para lançar uma exceção, por exemplo, o método que recebe um objeto Pet e espera poder alimentá-lo pode falhar quando recebe um objeto Cat, pois o comportamento do método foi alterado.


Veja um exemplo de código que ilustra essa violação usando o C# 11 no VS 2022 com o recurso do Top Level Statement:

PetFeeder feeder = new PetFeeder();

Pet pet = new Cat(); // Instância de Cat passada como Pet

feeder.FeedPet(pet);

Console.ReadKey();

public class Pet
{
 
public virtual void Feed()
  {
    Console.WriteLine(
"O animal está sendo alimentado");
  }
}

public class Dog : Pet
{

   public
override void Feed()
   {
     Console.WriteLine(
"O cachorro está sendo alimentado");
   }
}

public class Cat : Pet
{
  
public override void Feed()
   {
    
throw new InvalidOperationException("Os gatos não comem isso");
   }
}

public class PetFeeder
{
  
public void FeedPet(Pet pet)
   {
     pet.Feed();
   }
}

Nesse exemplo, quando a classe Cat é passada para o método FeedPet(), que espera um objeto do tipo Pet, ocorre uma exceção. Isso acontece porque o método Feed() da classe Cat não respeita o contrato definido pela classe base Pet.

Para solucionar essa violação do princípio LSP, a classe Cat deve respeitar o contrato definido pela classe base Pet. Uma solução seria a classe Cat sobrescrever o método Feed() para exibir a mensagem "O gato está sendo alimentado", em vez de lançar uma exceção. Dessa forma, a classe Cat poderia ser usada de forma transparente no lugar da classe base Pet, sem quebrar o comportamento esperado.

...
public
class Cat : Pet
{
 
public override void Feed()
  {
   Console.WriteLine(
"O gato está sendo alimentado");
  }
}
...

Nesse exemplo, quando a classe Cat é passada para o método FeedPet(), que espera um objeto do tipo Pet, o comportamento esperado é exibido na tela, sem que ocorra uma exceção. Dessa forma, a classe Cat pode ser usada de forma transparente no lugar da classe base Pet, respeitando o princípio LSP.

Padrões que podem violar o princípio

Um padrão comum que pode violar o princípio LSP é o padrão "Template Method". Este padrão define a estrutura geral de um algoritmo em uma classe base, deixando a implementação de alguns passos para classes derivadas. No entanto, se uma classe derivada precisar alterar o comportamento de um método na classe base, ela poderá violar o princípio LSP.

Outro padrão que pode violar o princípio LSP é o padrão "Strategy". Este padrão permite que você altere o comportamento de um algoritmo em tempo de execução, definindo uma interface comum para todas as estratégias. No entanto, se uma estratégia específica violar o contrato definido pela interface comum, ela poderá violar o princípio LSP.

Violação do princípio pelo padrão Strategy

Imagine que você está desenvolvendo um sistema de vendas que inclui diferentes formas de pagamento, como dinheiro, cartão de crédito e boleto bancário. Você decide implementar o padrão Strategy para permitir que o cliente escolha a forma de pagamento desejada durante o processo de checkout.

Você define uma interface IPagamento comum para todas as estratégias de pagamento e implementa as classes PagamentoDinheiro, PagamentoCartao e PagamentoBoleto como estratégias que implementam a interface IPagamento.

No entanto, digamos que você decida adicionar uma nova funcionalidade que permita que os clientes paguem em parcelas usando cartão de crédito. Para isso, você altera a classe PagamentoCartao para incluir um novo método PagarEmParcelas(). Embora isso permita que os clientes paguem em parcelas usando cartão de crédito, isso também significa que a classe PagamentoCartao agora viola o contrato definido pela interface IPagamento.

Agora, imagine que você precise atualizar a classe Checkout para lidar com a nova funcionalidade de pagamento em parcelas. Como Checkout depende da interface IPagamento, ele espera que todas as estratégias de pagamento implementem apenas os métodos definidos pela interface. No entanto, a classe PagamentoCartao agora tem um método adicional que não é definido pela interface, o que significa que a implementação de PagamentoCartao viola o contrato definido por IPagamento.

Com isso, você acabou de violar o princípio LSP, pois a classe PagamentoCartao agora não é substituível pela interface IPagamento, o que pode levar a problemas de compatibilidade e manutenção do código no futuro.

Código do exemplo:

Checkout checkout = new Checkout(new PagamentoDinheiro());
// Pagamento em dinheiro de R$100,00
checkout.FinalizarCompra(100.0); 
checkout = new Checkout(new PagamentoCartao());
// Pagamento com cartão de R$100,00
checkout.FinalizarCompra(100.0); 
// Violação do princípio LSP
checkout = new Checkout(new PagamentoCartao());
// Pagamento em 3 parcelas de R$33,33
((PagamentoCartao)checkout).PagarEmParcelas(100.0, 3); 
checkout = new Checkout(new PagamentoBoleto());
checkout.FinalizarCompra(100.0); // Pagamento com boleto de R$100,00
Console.ReadLine();

interface IPagamento
{
    void Pagar(double valor);
}
class PagamentoDinheiro : IPagamento
{
    public void Pagar(double valor)
    {
        Console.WriteLine($"Pagamento em dinheiro de {valor:C}");
    }
}
class PagamentoCartao : IPagamento
{
    public void Pagar(double valor)
    {
        Console.WriteLine($"Pagamento com cartão de {valor:C}");
    }
    // Violação do contrato
    public void PagarEmParcelas(double valor, int parcelas)
    {
        Console.WriteLine($"Pagamento em {parcelas} parcelas de {(valor / parcelas):C}");
    }
}
class PagamentoBoleto : IPagamento
{
    public void Pagar(double valor)
    {
        Console.WriteLine($"Pagamento com boleto de {valor:C}");
    }
}
class Checkout
{
    private IPagamento _pagamento;
    public Checkout(IPagamento pagamento)
    {
        _pagamento = pagamento;
    }
    public void FinalizarCompra(double valor)
    {
        _pagamento.Pagar(valor);
    }
}

 


Na linha de código em destque o código tenta realizar uma conversão explícita de checkout para PagamentoCartao, o que só é possível se checkout for de fato uma instância de PagamentoCartao ou de uma classe derivada. Se a estratégia de pagamento atual for outra classe que não é derivada de PagamentoCartao, essa conversão vai falhar em tempo de execução.

Por isso, para evitar esse tipo de problema, é importante utilizar sempre a interface comum para acessar a estratégia de pagamento, como fizemos no exemplo corrigido. Assim, podemos trocar a implementação da estratégia sem precisar alterar o código do cliente que utiliza essa estratégia.

Para resolver esse problema, podemos adicionar o método PagarEmParcelas() na interface IPagamento, de forma que todas as estratégias de pagamento devem implementá-lo. Dessa forma, podemos remover a violação do princípio LSP e utilizar as diferentes estratégias de pagamento sem precisar fazer a conversão explícita.

O código modificado ficaria assim:

Checkout checkout = new Checkout(new PagamentoDinheiro());
checkout.FinalizarCompra(100.0);

checkout = new Checkout(new PagamentoCartao());
checkout.FinalizarCompra(100.0);

checkout.FinalizarCompraParcelado(100.0, 3);

checkout = new Checkout(new PagamentoBoleto());
checkout.FinalizarCompra(100.0);

Console.ReadLine();

interface IPagamento
{
    void Pagar(double valor);
    void PagarEmParcelas(double valor, int parcelas);

}

class PagamentoDinheiro : IPagamento
{
    public void Pagar(double valor)
    {
        Console.WriteLine($"Pagamento em dinheiro de {valor:C}");
    }

    public void PagarEmParcelas(double valor, int parcelas)
    {
        Console.WriteLine("Não é possível parcelar pagamentos em dinheiro.");
    }
}

class PagamentoCartao : IPagamento
{
    public void Pagar(double valor)
    {
        Console.WriteLine($"Pagamento com cartão de {valor:C}");
    }

    public void PagarEmParcelas(double valor, int parcelas)
    {
        Console.WriteLine($"Pagamento em {parcelas} parcelas de {(valor / parcelas):C}");
    }
}

class PagamentoBoleto : IPagamento
{
    public void Pagar(double valor)
    {
        Console.WriteLine($"Pagamento com boleto de {valor:C}");
    }

    public void PagarEmParcelas(double valor, int parcelas)
    {
        Console.WriteLine("Não é possível parcelar pagamentos com boleto.");
    }
}

class Checkout
{
    private IPagamento _pagamento;

    public Checkout(IPagamento pagamento)
    {
        _pagamento = pagamento;
    }

    public void FinalizarCompra(double valor)
    {
        _pagamento.Pagar(valor);
    }

    public void FinalizarCompraParcelado(double valor, int parcelas)
    {
        _pagamento.PagarEmParcelas(valor, parcelas);
    }
}

 

Agora adicionamos o método PagarEmParcelas() na interface IPagamento e o implementamos em todas as estratégias de pagamento. Dessa forma, podemos chamar o método FinalizarCompraParcelado() na classe Checkout, que chama o método PagarEmParcelas() na estratégia de pagamento atual. Isso garante que todas as estratégias de pagamento implementam o mesmo comportamento, evitando a violação do princípio LSP.

Além disso, observe que na classe Checkout, utilizamos diretamente a referência para um objeto que implementa a interface IPagamento, em vez de fazer a conversão explícita para uma classe específica. Isso torna o código mais genérico e flexível, permitindo a troca de estratégias de pagamento de forma mais fácil e sem precisar alterar a classe Checkout.

Com essas modificações, o código agora está em conformidade com o princípio LSP e podemos utilizar o padrão Strategy de forma correta e segura.

E estamos conversados ...

"Tendo sido, pois, justificados pela fé, temos paz com Deus, por nosso Senhor Jesus Cristo;"
Romanos 5:1

Referências:


José Carlos Macoratti