Olá, povo!

De volta aos princípios SOLID com o terceiro princípio do acrônimo: o Princípio de Substituição de Liskov (Liskov Substitution Principle), ou simplesmente LSP. Recomendo que leiam os 2 posts anteriores da série <aqui> e <aqui>, principalmente o post sobre o OCP, que está diretamente relacionado a este.

Vamos lá então!

DEFINIÇÃO

O Princípio de Substituição de Liskov leva esse nome por ter sido criado por Barbara Liskov, em 1988 (você pensou que algum “liskov” seria substituído é?).

Sua definição mais usada diz que:

“Classes derivadas devem poder ser substituídas por suas classes base”

Que é uma forma mais simples de explicar a definição formal de Liskov:

“Se para cada objeto o1 do tipo S há um objeto o2 do tipo T de forma que, para todos os programas P definidos em termos de T, o comportamento de P é inalterado quando o1 é substituído por o2 então S é um subtipo de T”

Vejamos como clarear melhor o conceito com os exemplos.

EXEMPLO DE VIOLAÇÃO

Já mostrei no post sobre OCP um exemplo que fere o LSP. Replico aqui o exemplo:


public class Arquivo
{
}

public class ArquivoWord : Arquivo
{
    public void GerarDocX()
    {
        // codigo para geracao do arquivo
    }
}

public class ArquivoPdf : Arquivo
{
    public void GerarPdf()
    {
        // codigo para geracao do arquivo
    }
}

Observem que nesta hierarquia de classes, ArquivoWord e ArquivoPdf herdam de Arquivo, provavelmente para reaproveitar algum campo/comportamento, mas cada uma das derivadas tem seu próprio método de geração, ignorando o uso de polimorfismo.

Este design “capenga” fere o LSP, uma vez que não conseguiremos substituir ArquivoWord e ArquivoPdf pela classe base Arquivo quando quisermos utilizar os métodos de geração de arquivo. É exatamente o que acontece no nosso conhecido GeradorDeArquivos:


public class GeradorDeArquivos
{
    public void GerarArquivos(IList<Arquivo> arquivos)
    {
        foreach(var arquivo in arquivos)
        {
            if (arquivo is ArquivoWord)
                ((ArquivoWord)arquivo).GerarDocX();
            else if (arquivo is ArquivoPdf)
                ((ArquivoPdf)arquivo).GerarPdf();
        }
    }
}

Vejam que no método acima não conseguimos substituir ArquivoWord e ArquivoPdf por Arquivo, pois os métodos de geração são específicos das classes derivadas. Sendo assim, somos obrigados a verificar o tipo e fazer o downcast para chamar o método apropriado.

Em resumo, ao ferir o LSP (na herança entre os tipos de Arquivo) acabamos ferindo o OCP por consequência.

VIOLAÇÃO MAIS SUTIL (O CLÁSSICO “QUADRADO É UM RETÂNGULO”)

Vejamos agora um exemplo mais sutil de violação do LSP. Nele, até conseguimos usar a classe-base no lugar da derivada, em termos de compilação, mas, em tempo de execução, temos um comportamento inesperado (leia-se “bug”).

Segue o design, onde um Quadrado é uma classe derivada de Retângulo:

public class Retangulo
{
    public virtual double Altura { get; set; }
    public virtual double Comprimento { get; set; }
    public double Area { get { return Altura * Comprimento; } }
}

public class Quadrado : Retangulo
{
    public override double Altura
    {
        set { base.Altura = base.Comprimento = value; }
    }

    public override double Comprimento
    {
        set { base.Altura = base.Comprimento = value; }
    }
}

Percebam acima que o Quadrado sobrescreve Altura e Comprimento para manter sua regra de que ambos devem ser iguais. Com isso, a classe derivada viola uma regra estabelecida na classe base: a de que altura e comprimento variam independentemente.

Vejamos que implicações podemos ter com isso, observando o seguinte código-cliente:


public void MetodoQualquer(Retangulo retangulo)
{
    retangulo.Altura = retangulo.Altura * 2;
    retangulo.Comprimento = retangulo.Comprimento * 4;
    // faz alguma coisa com essa nova área...
}

Notem que o programador assumiu que estava lidando com um retângulo (afinal veja o tipo do parâmetro do método) e aplicou um cálculo que variasse suas dimensões.

No entanto, caso o método receba um Quadrado (o que é perfeitamente possível, já que Quadrado herda de Retangulo) teremos um comportamento inesperado: o cálculo para redimensionar o retângulo o transformará em um quadrado!!

O problema acima só existe porque, como dito anteriormente, a classe derivada não respeita a regra da classe base de variar os lados de forma independente. Sendo assim, do ponto de vista computacional, Quadrado não é um Retangulo, pois ambos possuem comportamentos diferentes em relação a alteração de seus lados.

PROBLEMAS

Como visto nos 2 exemplos, ferir o LSP pode provocar comportamentos inesperados no software por suposições equivocadas quando ao funcionamento das classes derivadas de uma hierarquia de classes.

Além disso, sua violação pode implicar em uma violação no OCP, causando todos os demais problemas consequentes desta. (Vide OCP.)

CONCLUSÃO

Ao atender o Princípio de Substituição de Liskov (LSP), ou seja, ao garantir que as classes derivadas sejam completamente substituíveis por suas classes-base, todo código que utilizar a classe base será capaz de atender o OCP, facilitando a manutenção e extensão do software, além de ser um código mais seguro, livre de mau funcionamento como o mostrado no exemplo do quadrado.

Resumindo: preste atenção na sua hierarquia de classes! Faça bom uso do polimorfismo e não esqueça que uma relação “É UM” se refere a COMPORTAMENTO.

É isso.

Abraços!
—————————–

Toda a série:
Princípio da Responsabilidade Única (SRP)
Princípio do Aberto/Fechado (OCP)
Princípio da Substituição de Liskov (LSP)
Princípio da Segregação de Interface (ISP)
Princípio da Inversão de Dependência (DIP)