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.

DEFINIÇÃO

O Princípio de Substituição de Liskov leva esse nome por ter sido criado por Barbara Liskov, em 1988. A definição formal de Liskov diz que:

“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”

Tal definição foi resumida e popularizada por Robert C. Martin (Uncle Bob), em seu livro “Agile Principles Patterns and Practices”, como:

“Classes derivadas devem poder ser substitutas de suas classes base”

Em outras palavras, toda e qualquer classe derivada deve poder ser usada como se fosse a classe base.

Vejamos como clarear melhor o conceito com exemplos.

EXEMPLO DE VIOLAÇÃO

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


public class Arquivo
{
// outros métodos...
}

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 nenhuma das classes derivadas pode ser usada como a classe base. É 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 usar as derivadas de forma polimórfica (como, por ex, arquivo.Gerar();). Ao invés disso, 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. (A solução completa está no artigo sobre OCP citado anteriormente.)

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 derivada no lugar da classe base em tempo 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, em tempo de execução, 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, isto é, ao quadruplicar o comprimento, inadvertidamente, a altura também foi quadruplicada!

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.

CONCLUSÃO

Ao atender o Princípio de Substituição de Liskov (LSP), ou seja, ao garantir que as classes derivadas sejam usadas transparentemente onde se vê uma classe base, todo código que depende da classe base será capaz de usar, em tempo de execução, qualquer uma das derivadas sem sequer saber da existência delas.

Desta forma, estamos garantindo também o OCP, facilitando a extensão do software e deixa-lo livre de mau funcionamento.

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.
—————————–
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)