[Pesquisar este blog]

domingo, 7 de maio de 2017

Os Princípios SOLID::LSP (parte IV)

Parte I | Parte II | Parte III | Parte IV | Parte V | Parte VI
Os princípios SOLID, organizados por Robert C. Martin, constituem um conjunto consistente de diretrizes para o projeto e programação orientada a objetos que pretendem auxiliar os projetistas de software, além dos programadores, na construção de sistemas estruturados para que sejam mais fáceis de manter e de ampliar com o passar do tempo.
O desenvolvimento de software é uma atividade inerentemente complexa, assim os custos relacionados a construção de sistemas grandes são usualmente altos. Torna-se imperativo, então, projetar sistemas que, além de agregar valor, possam ser utilizados por muito tempo, como única forma de compensar o investimento em sua construção. Assim, o projeto de um software deve se preocupar não apenas com as funcionalidades requeridas, mas também na sua manutenção e ampliação, evitando que seu envelhecimento dificulte ou impeça sua utilização.

Os princípios enunciados por Martin contém recomendações importantes que devem ser consideradas na construção; mas também são um guia para refatoração do código, além de se enquadrarem nas estratégias ágeis de construção de software.

Como visto, são cinco os princípios que formam o acrônimo SOLID:
·      Single Responsability Principle
·      Open-Closed Principle
·      Liskov's Substitution Principle
·      Interface Segregation Principle
·      Dependency Inversion Principle

Neste post será abordado o LSP ou Princípio da Substituição de Liskov.

LSP::Liskov's Substitution Principle

Os tipos derivados devem ser completamente substituíveis por seus tipos base.
O Liskov's Substitution Principle ou Princípio da Substituição de Liskov ou apenas LSP foi proposto por Barbara Liskov (e outros) em 1981, preocupando-se com as questões de abstração e manutenabilidade do software. Este princípio é formalmente enunciado como:
"Se para cada objeto o1 do tipo S existe um objeto o2 do tipo T tal 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."
Esta definição é mais simples do que parece. Considere um programa que utiliza um objeto o1 de um tipo determinado, por exemplo da classe S. Também considere um outro objeto o2, mas da classe T, sendo que S é uma subclasse de T (portanto T é um supertipo de S). A figura abaixo ilustra esta situação. O LSP significa que o programa deve funcionar corretamente quando o objeto o1 é substituído pelo objeto o2, pois este objeto o2 é um supertipo de o1.
Quando se afirma que o programa continua a funcionar corretamente, ou seja, que seu comportamento não se altera, não está se afirmando que os resultados produzidos por um objeto de um tipo sejam iguais aos produzidos por outros de suas subclasses, mas espera-se que as mesmas operações estejam disponíveis e que os resultados sejam do mesmo tipo.

Por isso o LSP também é conhecido como princípio da conformidade de tipo:
"Se S é um real subtipo de T, então S deve se conformar a T. Em outras palavras, um objeto de tipo S pode ser provido em qualquer contexto no qual um objeto do tipo T seja esperado, e a exatidão ainda será preservada quando qualquer operação de acesso do objeto for executada." (PAGE-JONES, 2001, pg.286)
Assim, num projeto orientado a objeto adequado e consistente, o tipo de cada classe deverá se conformar com o tipo de sua superclasse, de maneira que hierarquias de classes devem seguir este princípio, permitindo que o polimorfismo seja usado com comodidade e precisão. Assim sendo, as dependências do cliente em relação a classe podem ser substituídas por suas subclasses, sem que o cliente saiba da mudança.

O projeto de hierarquias de classes onde é garantido que as subclasses operem da mesma maneira que suas superclasses, isto é, funcionalidade diferente, mas dentro do comportamento esperado, é conhecida também como projeto por contrato (design by contract). Neste sentido a relação típica é um/is a que identifica as relações de herança deve ser repensada como é substituível por/is substitute for.

Um Exemplo

Considere como pode ser modelada a relação existente entre quadrados e retângulos; tal como círculos e elipses. A geometria euclidiana afirma que um todo quadrado é um retângulo; assim como os círculos são elipses. Do ponto de vista da programação orientada a objetos, como a pergunta "é do tipo?" é respondida satisfatoriamente, identificam-se relações típicas de herança entre retângulos (supertipo) e quadrado (subtipo), assim como elipses (supertipo) e círculos (subtipo), como ilustra a figura que segue.
Aqui existe uma situação que merece atenção. Se uma instância de Retangulo for substituída por uma instância de Quadrado, o programa poderá falhar se for esperado que a altura e largura variem livremente, pois no caso do Quadrado, existe uma invariante que altura=largura. A substituição inversa (um objeto Quadrado substituído por outro Retangulo) pode causar problemas pela mesma razão, se esperado que as dimensões sejam mantidas iguais. Nesta situação temos uma violação do LSP, pois o código se comporta diferentemente do que foi estabelecido pela geometria.

Tornar as classes imutáveis, de maneira que novos objetos consistentes sejam "fabricados" por demanda, é uma solução possível, como ilustrado pelas interfaces seguem.

public interface Retangulo {
    double getLargura();
    double getAltura();
    Retangulo comAltura(double a);
    Retangulo comLargura(double l);
}
public interface Quadrado extends Retangulo {
    Quadrado comTamanho(double t);
}

O mesmo problema não ocorreria com uma implementação típica de Elipse e Circulo, pois o comportamento do código permitiria respeitar as definições geométricas.

O exemplo enfatiza que um violação do LSP causa, geralmente, comportamento indefinido, principalmente quando a substituição é dinâmica. A dificuldade reside no fato de funcionar durante o desenvolvimento, mas provocar bugs aleatórios durante a produção.

Conclusões

O Princípio da Substituição de Liskov destaca que a validade de um modelo não é intrínseca e que depende de sua inserção e coerência com o contexto do cliente. Além disso, o comportamento de objetos de uma hierarquia deve ser o mesmo: um quadrado é um retângulo geometricamente, mas não se comporta como um! Já círculos e elipses exibem o mesmo comportamento! Então, concluímos que na orientação a objetos, a relação de herança pertence ao comportamento público dos objetos.

O cumprimento deste princípio requer atenção no projeto de hierarquias de classes usadas como substituições dinâmicas. Seu atendimento ajudar evitar: 
  • Comportamento indefinido decorrente de substituições entre supertipos e subtipos;
  • Esforço desproporcional para detecção e correção dos problemas decorrentes;
  • Problemas de fragilidade e rigidez!

Para Saber Mais


  • LARMAN, Craig. Utilizando UML e padrões: uma introdução à análise e ao projeto orientados à objetos e ao Processo Unificado. Porto Alegre: Bookman, 2007.
  • MARTIN, R. C.; et. al. Clean Code: a handbook of agile software craftsmanship. Boston: Pearson Education, 2009.
  • MARTIN, R. C. The Clean Coder. Upper Sadle River: Prentice-Hall, 2011.
  • MARTIN, R. C. Principles of OOD. Disponível em http://butunclebob.com/ArticleS.UncleBob.PrinciplesOfOod, recuperado em 17/02/2017.
  • MARTIN, R. C. Get a SOLID start. Disponível em http://objectmentor.com, recuperado em 17/02/2017.
  • PAGE-JONES, Meilir. Fundamentos do Desenho Orientado a Objetos. São Paulo: Makron Books, 2001.
  • SOMMERVILLE, I. Software Engineering. 9th. Ed. Boston: Addison-Wesley, 2011.