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:

Princípios SOLID - Boas Práticas de Programação com C#: Parte 3 - LSP - Liskov’s Substitution Principle

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.