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 Petfeeder.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);
}
}
|
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.FinalizarCompraParcelado(100.0, 3);
checkout = new
Checkout(new PagamentoBoleto());
Console.ReadLine();
class
PagamentoDinheiro : IPagamento
public void PagarEmParcelas(double valor, int parcelas)
class
PagamentoCartao : IPagamento
public void PagarEmParcelas(double valor, int parcelas)
class
PagamentoBoleto : IPagamento
public void PagarEmParcelas(double valor, int parcelas)
class
Checkout
public Checkout(IPagamento pagamento)
public void FinalizarCompra(double valor)
public void FinalizarCompraParcelado(double valor, int
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.
Na próxima parte do artigo vamos continuar mostrando as violações dos princípios SOLID.
E estamos conversados ...
"Tendo sido, pois, justificados pela fé, temos paz com Deus, por nosso Senhor
Jesus Cristo;"
Romanos
5:1
Referências:
LINQ - Gerando um Produto Cartesiano - Macoratti
C# - Salvando e Lendo informações em um arquivo XML - Macoratti