Aplicando SOLID com C# — LSP (Liskov Substitution Principle)

Continuando com a nossa série de artigos sobre os padrões SOLID, o assunto aqui abordado será o LSP (Liskov Substitution Principle) ou em português Princípio da Substituição de Liskov. Eu diria que o LSP é mais uma questão de análise e questionamentos sobre o sistema do que propriamente “mão na massa” e adiante vamos entender o porquê.

Esse conceito foi desenvolvido em um artigo científico de autoria de Barbara Liskov e apresentado em 1987, tendo sido publicado em 1994 em conjunto com Jannette Wing. A teoria original do artigo era:

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

Simples, não?

Com tudo, a ideia de Liskov ganhou força de verdade quando foi publicado por Robert C. Martin no livro Agile Software Development, Principles, Patterns, and Practices, que tempos depois ganhou uma versão para C# no livro intitulado de Agile Principles, Patterns, and Practices in C# (Recomendo muito a leitura).

Agile Principles, Patterns, and Practices in C#

Imagem 01 — Livro Agile Principles, Patterns, and Practices in C#

Após a publicação de Robert Martin a teoria de Liskov foi simplificada com a seguinte afirmação:

Uma classe base deve poder ser substituída pela sua classe derivada.

Este princípio diz que a instância de uma classe base deve possibilitar sua substituição por instâncias de classes herdadas sem que se necessite qualquer alteração no código.

Em outras palavras, nos fornece uma forma de saber se uma herança está implementada de forma correta ou não. Através do uso de polimorfismo, ao usar um método ou propriedade, seja da classe base ou da classe especialista, o resultado sempre deve estar correto sem qualquer alteração.

Classes filhas nunca deveriam infringir as definições de tipo da classe pai.

Dois fatores bem importantes devem ser avaliados para aplicar o LSP:
1 — A classe base possui comportamentos inúteis para a classe derivada?
2 — O comportamento externo é alterado se substituímos a classe base pela derivada?

Se uma das duas se confirmarem, usando o LSP concluímos que uma relação de base e derivada está incorreta.
Veja a seguir essas duas violações na prática:

Ao utilizar heranças, se uma classe derivada não implementa todos os métodos da classe base ou se parte dos métodos ou propriedades da classe base não tem sentido algum para as derivadas, segundo o LSP, essa herança está errada e poderá trazer problemas futuros ao programa. Por exemplo, imagine um sistema onde é preciso calcular descontos para realizar vendas a futuros clientes e também calcular descontos a contatos que já são clientes:

[code language=”csharp”]
public class Cliente
 {
 public int ClienteId { get; set; }
 public string Nome { get; set; }
 public DateTime DataCadastro { get; set; }
 public TipoCliente TipoCliente { get; set; }
 public bool ClienteEspecial()
 {
 return DataCadastro.Year < 2010;
 }
 public decimal CalcularDesconto(decimal valor)
 {
 switch (TipoCliente)
 {
 case TipoCliente.Gold:
 return valor * 0.40m; //desconto de 60%
 case TipoCliente.Silver:
 return valor * 0.80m; //Desconto de 20%
 default:
 return valor;
 }
 }
 }
[/code]

[code language=”csharp”]
 public class PossivelCliente: Cliente
 {
 public int PossivelClienteId { get; set; }

public override decimal CalcularDesconto(decimal valor)
 {
 return base.CalcularDesconto(valor);
 }

public override bool ClienteEspecial()
 {
 //Ops, eu ainda não sou cliente, muito menos especial
 throw new NotImplementedException();
 }
 }
[/code]

Vamos ver uma simulação de cálculo de desconto para um cliente já cadastrado. Usaremos uma aplicação do tipo Console para melhor ilustrar:

[code language=”csharp”]
 class Program
 {
 static void Main(string[] args)
 {
 Cliente cliente = new Cliente();
 cliente.TipoCliente = TipoCliente.Gold;
 cliente.DataCadastro = new DateTime(2008, 12, 1);
 cliente.Nome = “Diego”;
 var valorDesconto = cliente.CalcularDesconto(100);

Console.WriteLine(“O cliente {0} pagará o valor de R$ {1} — {2}”, cliente.Nome,
 valorDesconto, cliente.ClienteEspecial() ? “Cliente Especial” : “Cliente Normal”);

Console.ReadKey();
 }
 }[/code]

A saída deverá ser:
- O cliente Diego pagará o valor de R$ 40,00 — Cliente Especial

E se fizermos a mesma coisa para um “Possível Cliente”, que ainda não é de fato um cliente, mas precisa calcular descontos para que por exemplo uma venda seja fechada e se torne cliente, veja os problemas que teremos usando a herança:

[code language=”csharp”]
 class Program
 {
 static void Main(string[] args)
 {
 Cliente cliente = new PossivelCliente();
 cliente.TipoCliente = TipoCliente.Gold;
 cliente.DataCadastro = new DateTime(2008, 12, 1); //Possível Cliente não pode ter data de cadastro
 cliente.Nome = “Diego”;
 var valorDesconto = cliente.CalcularDesconto(100);

Console.WriteLine(“O cliente {0} pagará o valor de R$ {1} — {2}”, cliente.Nome,
 valorDesconto, cliente.ClienteEspecial() ? “Cliente Especial” : “Cliente Normal”);

Console.ReadKey();
 }
 }
[/code]

Alguns problemas são gerados ao usar a classe “filha” no lugar da classe base. Acabamos herdando a propriedade DataCadastro que nem será usada na classe PossivelCliente, além de receber uma exception por executar um método que não tem sentido algum para um “possível cliente” que é o método ClienteEspecial, veja:

exception

Imagem 02 — Exception gerada usando a classe PossivelCliente que não implementa o método ClienteEspecial.

Vendo esse exemplo, podemos dizer que a utilização do recurso de herança não deve ser usado somente para reaproveitar código, e sim quando realmente faz sentido herdar as características e comportamentos da classe base. Geralmente, quando temos o padrão OCP (Open-Closed Principle) ou Princípio Aberto-Fechado implementado de forma correta, se torna mais fácil aplicar o LSP. Veja o artigo de OCP aqui.

No dia-a-dia, uma classe passa a herdar o comportamento de outra geralmente quando a resposta de perguntas como “objeto A é um objeto B?” ou “Conta Poupança é uma Conta?” ou até mesmo “Um cachorro é um animal?” é positiva. Temos que tomar muito cuidado com a sentença “É um”. Ainda que um “objeto A” tenha as mesmas características de “objeto B”, nem sempre uma herança entre esses objetos estará coerente. Por exemplo, se eu tornar a questão de “Conta Poupança é uma Conta?” sem pensar muito, vamos dizer que sim, então eu posso herdar o comportamento de Conta em Conta Poupança que vamos atender o LSP. Será? Vamos ver na prática:

Primeiro temos a classe Conta e seus comportamentos

[code language=”csharp”]
 public class Conta
 {
 public decimal ValorEmConta { get; set; }

//declaramos o método saque como virtual para podermos sobrescrever em suas “filhas”
 public virtual void Saque(decimal valor)
 {
 ValorEmConta -= valor;
 }
 }
[/code]

Agora temos a classe Conta Poupança que herda os comportamentos de Conta, já que, segundo nossa resposta positiva de “Conta Poupança é uma Conta?”, podemos então criar uma herança entre elas.

[code language=”csharp”]
 public class ContaPoupanca : Conta
 {
 public override void Saque(decimal valor)
 {
 if (ValorEmConta > valor)
 ValorEmConta -= valor;
 }
 }
[/code]

A princípio todas as características internas de Conta são usadas em Conta Poupança, mas vamos agora ao teste prático se usando o LSP essa herança está correta:

[code language=”csharp”]
 class Program
 {
 static void Main(string[] args)
 {
 var conta = new Conta();
 conta.ValorEmConta = 100;

conta.Saque(250);

Console.WriteLine(string.Format(“A conta tem o saldo de: {0} reais”,conta.ValorEmConta));
 Console.ReadLine();
 }
 }
[/code]

Executando esse código, teremos a saída:
- A conta tem o saldo de: -150 reais

O LSP diz que se trocar a classe pai pela filha, nada deverá ser modificado, vamos ver se no nosso exemplo, isso acontece realmente:

[code language=”csharp”]
 class Program
 {
 static void Main(string[] args)
 {
 var conta = new ContaPoupanca();
 conta.ValorEmConta = 100;

conta.Saque(250);

Console.WriteLine(string.Format(“A conta tem o saldo de: {0} reais”,conta.ValorEmConta));
 Console.ReadLine();
 }
 }
[/code]

Ops…

Algo está errado, o resultado alterou com a classe “filha”, temos a saída:
- A conta tem o saldo de: 100 reais

Apesar das classes possuírem características (métodos e propriedades) idênticas, o comportamento externo de cada uma é diferente, pois na conta normal eu posso possuir valores negativos, já na conta poupança eu só posso sacar valores menor ou igual ao saldo, logo fazendo nosso código quebrar.

Concluindo…
No começo deste artigo, eu disse que aplicar o LSP é mais uma questão de análise do que realmente codificar. Para que o LSP seja atendido, em alguns casos aplicar técnicas do ISP (Interface Segregation Principle), que será o tema da nossa próxima conversa, resolveria o problema, mas na maioria deles, a melhor forma é simplesmente não usar heranças onde o LSP é violado, pois certamente o código estará sujeito a futuras “gambiarras”.

Valeu, até a próxima…

Referências:

SOLID Principles in C# — An Overview
http://www.codeguru.com/columns/experts/solid-principles-in-c-an-overview.htm

SOLID architecture principles using simple C# examples
http://www.codeproject.com/Articles/703634/SOLID-Architecture-principles-using-simple-Csharp

SOLID Principles in C#
http://www.c-sharpcorner.com/UploadFile/damubetha/solid-principles-in-C-Sharp/

Like what you read? Give NetCoders a round of applause.

From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.