Este é o terceiro artigo da série Princípios SOLID – Boas práticas de programação com C#. Nele falaremos da terceira letra do acrônimo do SOLID: LSP – Liskov’s Substitution Principle. Você pode ler o artigo anterior clicando aqui.
Princípio da Substituição de Liskov: Uma classe base deve poder ser substituída pela sua classe derivada.
Esse princípio foi originado pela matemática Bárbara Liskov em 1980, que estabelecia regras para conjuntos matemáticos. Uncle Bob aplicou o princípio, definido por ela para o uso de herança, na Orientação à Objetos como sendo: ao implementar herança, você deve sempre lembrar do contrato estabelecido pela classe pai.
Em outras palavras, podemos entender herança como aplicáveis em relações X é um Y e não em X tem um Y ou X faz uso de Y.
Veja bem, este é o primeiro filtro, porém apenas essa regra não é o suficiente para que a herança seja implementada corretamente. Neste caso é necessário verificar a relação entre a classe pai e classe filha. Vejamos a seguir o exemplo do cálculo da área das figuras geométricas quadrado e retângulo.
Matematicamente, a área de um retângulo é calculada como altura X (multiplicado) largura, e todo quadrado é um retângulo, porém com os lados iguais.
Abstraindo essas definições poderíamos implementar da seguinte forma:
public class Retangulo { public virtual double Altura { get; set; } public virtual double Largura { get; set; } public double Area { get { return Altura * Largura; } } }
Onde a responsabilidade de calcular a área como altura X largura é do retângulo:
public class Quadrado : Retangulo { public override double Altura { set { base.Altura = base.Largura = value; } } public override double Largura { set { base.Altura = base.Largura = value; } } }
Onde a classe quadrado obrigatoriamente estabelece que as dimensões da altura e da largura precisam ser iguais.
Assim, para calcular teríamos por exemplo:
public class CalculoArea { private static void ObterAreaRetangulo(Retangulo ret) { Console.Clear(); Console.WriteLine("Calculo da área do Retangulo"); Console.WriteLine(); Console.WriteLine(ret.Altura + " * " + ret.Largura); Console.WriteLine(); Console.WriteLine(ret.Area); Console.ReadKey(); } public static void Calcular() { var quad = new Quadrado() { Altura = 10, Largura = 5 }; ObterAreaRetangulo(quad); } }
Executando o código temos:
25 de resultado!
Mas como é possível que tenhamos definido altura = 10 e largura = 5 e o resultado final foi área 25? E como foi possível definir um quadrado com lados diferentes ? A regra para ser quadrado não deveria ser dois lados iguais?
Sim, é exatamente esse o erro! A classe filha que é quadrado não pode ser substituída pela classe pai que é retângulo. Logo, para esse caso aparente de herança não é possível aplicá-la por não seguir o princípio de Liskov!
Para solucionar esse caso, poderíamos criar uma classe genérica para essas formas geométricas, o paralelogramo:
public abstract class Paralelogramo { protected Paralelogramo(int altura, int largura) { Altura = altura; Largura = largura; } public double Altura { get; private set; } public double Largura { get; private set ; } public double Area { get { return Altura * Largura; } } }
E restringir na classe quadrado, que é um paralelogramo peculiar – dois lados iguais:
public class Quadrado : Paralelogramo { public Quadrado(int altura, int largura) : base(altura, largura) { if(largura != altura) throw new ArgumentException("Os dois lados do quadrado precisam ser iguais"); } }
E retângulo seria apenas herdeiro de paralelogramo:
public class Retangulo : Paralelogramo { public Retangulo(int altura, int largura):base(altura,largura) { } }
Agora sim, para calcular a área de ambos os formatos geométricos poderíamos utilizar corretamente o polimorfismo e executar passando hora um quadrado, hora um retângulo, que não quebraria a regra de negócio (regra matemática, para o nosso exemplo aqui):
public class CalculoArea { private static void ObterAreaParalelogramo(Paralelogramo ret) { Console.Clear(); Console.WriteLine("Calculo da área do Retangulo"); Console.WriteLine(); Console.WriteLine(ret.Altura + " * " + ret.Largura); Console.WriteLine(); Console.WriteLine(ret.Area); Console.ReadKey(); } public static void Calcular() { var quad = new Quadrado(5,5); var ret = new Retangulo(10, 5); ObterAreaParalelogramo(quad); ObterAreaParalelogramo(ret); } }
E caso o programador tentasse passar algum valor incorreto para o quadrado (informando lados de valores diferentes por exemplo), iria ser tratado na própria classe Quadrado (no caso aqui retornando uma exception apenas para fins explicativos e didáticos).
A dica aqui é:
- Não utilize herança para reaproveitamento de código. Sempre que for implementar esse recurso poderoso da OO, verifique se é possível alternar a classe filha pela classe pai sem nenhum efeito colateral.
- Sempre pondere utilizar composição para que o código tenha mais flexibilidade.
No próximo artigo vamos falar sobre o princípio da Segregação de Interface (ISP) e entender melhor porque programar orientado a interfaces deixa seu código mais coeso e de fácil manutenção.
Referências
OOP – O princípio da substituição de Liskov (LSP)
PIRES, E. SOLID: Teoria e Prática.
ANICHE, M. Orientação a Objetos e SOLID para ninjas. São Paulo – Casa do Código
Samyla Dutra
Mineira, atuo na área de desenvolvimento desde 2017 e sou graduada em Engenharia de Computação pelo CEFET-MG. Gosto de entender a profundidade do backend e como melhorar a escrita de código. E isso se torna melhor em equipe! Amo estar em contato com a natureza, trilhar montanhas, ler livros e cozinhar.