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

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)

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

Adicione o seu

  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?

    Curtir

  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?

    Curtir

    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

      Curtir

      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…

        Curtir

      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

        Curtir

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

    Curtir

  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 ?

    Curtir

    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

      Curtir

  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.

    Curtir

  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.

    Curtir

    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!

      Curtir

  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

    Curtir

    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

      Curtir

      1. Olá Robson

        agora ficou mais claro…

        Muito obrigado pelo retorno.

        Grande abraço

        Curtir

  8. Olá, achei seu site ontem e estou achando o conteúdo bem explicado o que melhora muito o entendimento sobre o assunto. Mas gostaria de deixar uma sugestão ou uma dúvida, não sei ao certo. Vi que você mostrou o código de “como não deve ser feito”, achei o exemplo bem claro. Continuei lendo para ver o código de “como deveria ser feito”, e não tem. Entendi que deve-se utilizar interfaces de acordo com o exemplo, mas você não vê a necessidade de mostrar como deveria através de um exemplo? Muitos leigos precisam de algo mais concreto para entender melhor. Parabéns pelo texto. Obrigado

    Curtir

  9. Sobre a seguinte definição:

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

    Estou confuso sobre a definição acima devido o exemplo do quadrado e retângulo.
    No exemplo do quadrado e retângulo se invoca o quadrado (classe filha) para que ela substitua o método de calcular área da classe base. Logo, a frase correta seria :

    “Uma classe base deve poder ser substituída pela sua classe derivada.” – Macoratti
    Pela definição de Liskov apresentada neste artigo acredito que seria a frase acima mais adequada.

    Consultei alguns sites e conteúdos de professores para tentar sanar minha dúvida sem necessitar de perguntar, entretanto, obtive varias definições. Algumas falavam da filha substituindo a mãe e outras da mãe substituindo a filha. Em alguns sites encontrei até as duas
    Gostaria de saber se o principio diz que a mãe pode substituir a filha ou o inverso ou ambos.
    Obrigado

    Curtir

    1. Oi, Luiz,

      Desisti de escrever outro artigo.

      Revendo o texto do livro, creio que a tradução correta para a definição do Uncle Bob seja “poder ser substitutas de” e não “substituíveis por”, o que mudava a conotação para algo mais “compile time”.

      Dito isso, alterei essa definição e fiz umas pequenas mudanças neste post, explicando do ponto de vista de runtime. Acredito que agora fique mais claro e de total encontro à definição do Macoratti, que você citou.

      []s e qualquer dúvida estou por aqui!

      Curtir

  10. Ola Robson!
    No momento que comenta a respeito da violação do “Quadrado e um Retângulo”, após o exemplo com código do método “MetodoQualquer(…)” você escreve:
    “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!”

    Neste momento eu entendi que não há problemas, pois ao receber um Quadrado, a sua transformação por meio do cálculo estará correta, uma vez que se trata de um quadrado. Logo o comportamento não seria inesperado. Certo?
    O problema que vi seria: o primeiro redimensionamento seria substituído pelo segundo. Ou seja, a Altura*2 nunca sofreia efeito se o parâmetro fosse um quadrado.
    Por favor, me corrija se entendi errado.
    Muito obrigado. Aprendi muito. Parabéns.

    Curtir

    1. Oi, Stephenson
      Como Liskov costuma trazer várias dúvidas, irei escrever em breve outro artigo sobre este princípio. Fique ligado!
      Peço desculpas pela demora e por não responder neste exato momento.

      Curtir

    2. Oi, Stepherson

      Desisti de escrever outro artigo. (Fiz umas pequenas mudanças neste para deixá-lo mais claro.)

      Respondendo sua dúvida:
      O problema, neste caso, é que o código cliente pode supor pela API do `Retangulo` que altura e comprimento são coisas independentes e pode lidar com elas assim (código especifico para quando um for o dobro do outro, por ex). Isso não será verdade caso um objeto `Quadrado` seja passado em tempo de execução, porque, ao “setar” o comprimento, a altura será inadvertidamente setada “por tabela” ficando igual ao comprimento!

      Em uma herança correta, não podemos definir uma invariante como essa na classe derivada (invariante = altura e comprimento devem ser iguais!) sendo que isso não é verdade na classe base.

      Qualquer dúvida só chamar!

      Curtir

    1. Opa, blz? (Desculpe pela demora em responder).
      Não. Se são realmente métodos especificos da sub-classe e forem invocados num contexto onde vc depende especificamente da sub-classe, não há problema e não fere o LSP.
      O problema é tentar substituir a classe base com uma derivada e obter um comportamento inesperado.
      Ficou claro?
      []s

      Curtir

Participe! Vamos trocar uma ideia sobre desenvolvimento de software!

Blog no WordPress.com.

Acima ↑