Princípios SOLID: Princípio de Substituição de Liskov (LSP)

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)

Anúncios

17 comentários em “Princípios SOLID: Princípio de Substituição de Liskov (LSP)

  1. Muito legal essa série sobre SOLID.
    Só para esclarecer, quer dizer que não há nenhuma relação entre Quadrado e Retângulo?Uma interface resolveria este caso?

  2. Esses conceitos de SOLID. são muitos práticos e funcionais, mais acho que para aplicar isso em um software que foi desenvolvido em “métodos antigos”, e ainda continua assim, é bem complicado. Acho que para fazer tal mudança teria que desenvolver tudo de novo, teria que acontecer uma mudança de mente entre os programadores da equipe. Como fazer isso e aplicar em um software que já está rodando por ai?

    1. Olá, Geandre.
      Realmente, todo legado é um problema. Eu nunca sequer vi um legado com código orientado a objetos.
      Dá pra aplicar? Depende de uma série de fatores.. tempo disponível, qualidade do código atual, nível técnico, e, é claro, o custo/benefício disso.
      Por exemplo, você certamente deve ter outras prioridades do que fazer um grande esforço para refatorar um legado que está em vias de ser substituído por um produto novo, ou ainda, que vai ser totalmente descontinuado.
      Enfim, não há uma resposta do tipo “Sim” ou “Não” pra isso!
      O importante é ter esses conceitos na manga para aplicá-los quando for possível.
      []s

      1. aqui estou fazendo o seguinte, as novas funcionalidades que está sendo desenvolvida por mim estou desenvolvendo orientando a objeto, mais as funções que já existe no sistema é tudo criado de outra forma, e fira aquele bolo de códigos…

      2. Pois é, cara. Tem que tomar cuidado com isso. E também com a questão de fazer partes diferentes com tecnologias diferentes.
        Isso deve ser feito de forma organizada (se é que deve ser feito), com todos sabendo onde mexer e onde não mexer.
        Senão você pode acabar com um software Frankstein.
        Enfim, não dá pra opinar muito mais que isso sem estar vendo o código e entender o contexto.
        []s

  3. Muito interessante mesmo Robson,ótimo post..

    Sobre o código legado:
    Legado por sí só é compreensivel não ser “adaptável a novos paradigmas” mesmo que conceituais(não envolvendo demais tecnologias).
    Devido ao perfil técnico,know-how,profissinais de origem procedurais(e com fortes tendencias a permanecer no mesmo paradigma,já vi projeto que se resumia a isso; Data Entry > Procedures) e a “época” que foi desenvolvido ou com prazos malucos levando os profissionais a aplicar o famoso pattern RCP(Reuse by Copy and Paste,rsrs brincadeira).

    Minha opinião:
    O triste é quando o “legado” é construido em pleno ano de 2013, com a ferramenta que está no topo(Visual Studio 2012).

  4. Classes derivadas devem poder ser substituídas por suas classes base”
    Nesse método ao chamar ele imagine que eu queira enviar o classe base

    GerarArquivos(new Arquivo);

    public void GerarArquivos(IList arquivos)
    // implementação do método

    quando eu envio a classe base Arquivo eu não vou ter nenhum problema, pois a ele vai fazer os ifs e vai ver que ele não é nem arquivopdf nem arquivoDoc e não vai fazer nada, qual seria o problema ai ? nesse caso pode ser substituida sem pro ? onde tenho o problema do LSP ?

    1. Opa,
      Acho que vc quis dizer “enviar uma LISTA de arquivos”, já que o método recebe uma lista.. 🙂

      No exemplo “capenga”, como dito no artigo, não é possível usar Arquivo (classe-base) pois nem sequer métodos ele tem. Os métodos especializados estão somente nas classes derivadas. Sendo assim, não é possível usá-la.

      Para isso, devemos fazer modificações, que atenderão tanto o OCP como o LSP, que eu mostrei neste artigo:
      https://robsoncastilho.com.br/2013/02/23/principios-solid-principio-do-abertofechado-ocp/

      Dessa forma, os ‘ifs’ com checagem de tipo desaparecem e permitem que o método continue funcionando sem precisar ser alterado, sempre que surgirem novas classes derivadas de Arquivo.

      Espero ter conseguido responder!
      Qualquer coisa, estamos aí!
      []s

  5. Fala ai chefe,
    muito legais essas publicações sobre os padrões SOLID.
    clareou legal a mente.
    Faz do GRASP agora. srsrsrs
    agradeço desde já.
    um abraço.

  6. Deixa eu ver se entendi, O LSP diz que eu posso utilizar as classes ‘filhas’ no lugar da classe pai sem que haja nenhum problema certo ? Então para obedecer o LSP no 1º exemplo eu teria que ter um método abstrato GerarArquivo() para não precisar de implementações e gerar bugs como o do quadrado e o retângulo. E o OCP já está certo, pois eu faço modificações apenas nas classes filhas.

    No entanto, isso aumenta o meu acoplamento e pode aumentar a quantidade de código repetido.

    Estou certo ? Aguardo respostas.

    1. Olá, Kevin

      Em resumo, o princípio força o bom uso de herança. Se o seu código depende de T, ele deve funcionar corretamente e de forma transparente, recebendo um objeto do tipo T ou de qualquer tipo que herde de T. Ou seja, seu design não deve forçar que o código em questão quebre o OCP ou gere algum bug devido à alguma falsa suposição do programador.

      Não entendi quando você diz sobre o aumento de acoplamento e código repetido. Pode explicar melhor?
      []s!

  7. Olá Robson
    Um coisa que fiquei em dúvida foi, “se a minha classe derivada deve poder ser substituída pela classe base”, Porque eu deveria ter uma classe derivada? Porque não usar a classe base direto? No meu entendimento de “iniciante”, esse conceito diz que a classe derivada dever ser igual a classe base, pois uma coisa que tem uma função só pode ser substituida por uma capaz de realizar a mesma função… Poderia me dar uma luz? Abs

    1. Oi, Paulo
      Não sei se entendi muito bem a pergunta, mas o principio fala de substituição EM TEMPO DE EXECUÇÃO: se você passa uma classe base e seu programa continua funcionando normalmente, quer dizer que o principio está sendo atendido (o mesmo vale para qualquer derivada). Se o programa quebrou, quer dizer que você fez suposições no código que a classe base não estava preparada para atender: ou você apertou alguma regra da classe base na derivada (como a validação mais restritiva do exemplo Retangulo/Quadrado) ou afrouxou alguma pos-condicao na derivada (por exemplo, na derivada, alem de retornar o retorno também pode ser null).

      Todas as derivadas devem atender o contrato da classe-base. Assim, nenhum código cliente irá quebrar por não estar preparado para tratar a situação.

      Por fim, o LSP é mais sobre COMO usar herança e não QUANDO usar. Em geral, herança deve ser usada com bastante cuidado, quando as classes envolvidas realmente são parte de uma mesma família e de um mesmo domínio (ex: sua classe de negócio não herdar de uma classe ‘String’ ou de alguma lib de persistencia ou qualquer domínio não relacionado ao seu). Veja se não faz mais sentido utilizar composição. E se realmente a solução for herança, procurar seguir o LSP.

      Fui claro? Qualquer coisa, responde aí!
      []s

      1. Olá Robson

        agora ficou mais claro…

        Muito obrigado pelo retorno.

        Grande abraço

Participe! Vamos trocar uma ideia sobre desenvolvimento de software!

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair / Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair / Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair / Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair / Alterar )

Conectando a %s