OOP - O princípio da substituição de Liskov (LSP)
Neste artigo vou tratar de um assunto que é um dos pilares da programação orientada a objetos: O princípio da substituição de Liskov. (LSP) |
|
O princípio LSP foi definido por Barbara Liskov da seguinte forma: (http://pt.wikipedia.org/wiki/Princ%C3%ADpio_da_substitui%C3%A7%C3%A3o_de_Liskov)
Nas entrelinhas:
"Se você pode invocar um método q() de uma classe T (base), deve poder também invocar o método q() de uma classe T'(derivada) que é derivada com herança de T (base)."
De forma bem simples o princípio de Liskov sugere que :
"Sempre que uma classe cliente esperar uma instância de uma classe base X, uma instância de uma subclasse Y de X deve poder ser usada no seu lugar."
Em outras palavras: "Uma classe base deve poder ser substituída pela sua classe derivada."
O objetivo é ter certeza de que novas classes derivadas estão estendendo das classes base mas sem alterar o seu comportamento.
Por que o princípio de LisKov é importante ?
Porque sem aplicar o princípio de Liskov a hierarquia de classes seria uma bagunça e os testes de unidade para a superclasse nunca teriam sucesso para a subclasse.
Uma rápida revisão de conceitos
Herança
Exemplo:
Pessoa
- classe Pai, classe Base ou SuperClasse
- A classe
Pessoa
é a classe genérica; -
PessoaFisica É uma pessoa; |
Vamos lembrar os conceitos de herança: Quando uma classe herda de outra classe dissemos que temos a relação "É Um".
Quando a classe Pessoa Juridica ou Pessoa Fisica herda da classe Pessoa, ela passa a ser uma classe Pessoa estendida, pois contém todos os métodos e propriedades de Pessoa (nome e endereço) e mais as suas próprias funcionalidades (CPF ou CNPJ).
Entendendo o LSP na prática : Violando o LSP
Para ilustrar o princípio LSP eu vou me basear em um exemplo clássico que pode ser encontrado em diversos artigos na web e que eu adaptei para a linguagem C#.
Vamos supor que temos um projeto que implementa duas classes : Retangulo e Quadrado.
A classe Quadrado é uma
Retangulo que
tem uma característica especial: a largura e a altura
são iguais. Vamos definir duas classes para representar ambas as contas.
A notação
UML ao lado ilustra o relacionamento de herança
existente entre duas classes onde temos que A primeira vista estamos no caminho certo e nosso desenho parece ser consistente.
De forma até intuitiva
concordamos que um quadrado é um retângulo (pelo menos
conceitualmente) e
No entanto, esse tipo de
pensamento pode levar a alguns problemas sutis, mas
significativos. |
Vamos criar um pequeno projeto na linguagem C#, implementar as duas classes e testar se o nosso modelo não viola o principio LSP.
Crie um novo projeto usando o Visual C# 2010 Express Edition do tipo Console Application com o nome LSP_Violacao;
No menu Project selecione Add Class, informe o nome Retangulo.cs e clique em Add;
Em seguida defina o seguinte código na classe:
namespace LSP_Violacao { class Retangulo { protected int m_largura; protected int m_altura; public virtual void setLargura(int largura) { m_largura = largura;} public virtual void setAltura(int altura) { m_altura = altura; } public int getLargura() { return m_largura; } public int getAltura() { return m_altura; } public int getArea() { return m_largura * m_altura; } } } |
Na classe Retangulo temos:
Observe a palavra-chave virtual no método setLargura();
A palavra-chave
virtual
é usada para modificar um método,
propriedade, indexador ou declaração de evento e
permitir
que ele seja sobrescrito em uma classe derivada.
Na classe
Quadrado
iremos sobre-escrever o método setLargura();
Não estou usando as propriedades automáticas para que
fique
bem claro.
Vamos agora criar a classe Quadrado.
No menu Project selecione Add Class, informe o nome Quadrado.cs e clique em Add;
A seguir defina o seguinte código nesta classe:
namespace LSP_Violacao { class Quadrado : Retangulo { public override void setLargura(int largura) { m_largura = largura; m_altura = largura; } public override void setAltura(int altura) { m_largura = altura; m_altura = altura; } } } |
A classe Quadrado herda da classe Retangulo.
O método setLargura() desta classe sobrescreve o método da classe Base (setLargura);
Use o modificador
override
para modificar um método, uma propriedade,
um indexador, ou um evento. Um método de substituição
fornece uma nova
implementação de um membro herdado de uma classe base.
O método sobrescrito por uma declaração de override é
conhecido como o
método base sobrescrito. O método base sobrescrito deve
ter a mesma
assinatura que o método de substituição.
Você não pode substituir um método não-virtual ou estático. O método sobrescrito da classe base deve ser virtual, abstract, ou override.
Como a classe Quadrado herda da classe Retangulo ela vai herdar os métodos setLargura e setAltura , o que para um quadrado não faz muito sentido visto que a largura e a altura são iguais. É por isso que estamos sobrescrevendo esses dois métodos na classe Quadrado e definindo a largura igual a altura.
Agora estamos prontos para testar a nossa aplicação e verificar se as definições do nosso projeto estiverem de acordo com o princípio LSP a utilização da classe Quadrado deverá poder ser usada sem problema algum no lugar da classe base Retangulo.
Vamos incluir o código abaixo no evento Main do arquivo Programa.cs:
using System; namespace LSP_Violacao { class Program { private static Retangulo getNovoRetangulo() { //um factory return new Quadrado(); } static void Main(string[] args) { //vamos criar um novo retangulo Retangulo r = Program.getNovoRetangulo(); //definindo a largura e altura do retangulo r.setLargura(5); r.setAltura(10); // o usuário sabe que r é um retângulo // e assume que ele pode definir largura e altura // como para a classe base(Retangulo) Console.WriteLine(r.getArea()); Console.ReadKey(); // O valor retornado é 100 e não 50 como era esperado } } } |
Ao executarmos o projeto iremos obter o resultado exibido na figura abaixo.
Não é o resultado esperado.
Ao usar uma instância da classe quadrado que sobrescreveu o método da classe Base Retangulo, para calcular a área de um retângulo obtivemos um valor incorreto.
Dessa forma o princípio LSP foi violado pois o resultado deveria ser o correto.
No exemplo temos que a uma instância da classe Quadrado é devolvida por uma factory com base em algumas condições e nós não sabemos exatamente que tipo de objeto será retornada.
Se o princípio LSP estivesse sendo corretamente aplicado o fato de usar a instância de Quadrado não implicaria em mudança de comportamento como veremos que irá ocorrer.
Estamos criando um novo retângulo definindo a altura de 5 e a largura de 10 para obter a área.
O resultado deveria ser 50 mas obtemos 100.
Qual o problema ???
Ao definir a largura para 10, acabei definindo também a altura, deixando a área com valor de 100 e não 50 como era esperado.
Neste caso um quadrado não é um retangulo , e ao aplicarmos o princípio "É Um" da herança de forma automática vimos que ele não funciona para todos os casos.
A instância da classe Quadrado quando usada quebra o código produzindo um resultado errado e isso viola o principio de LisKov onde uma classe filha (Quadrado) deve poder substituir uma classe base(Retangulo).
Para obter o resultado correto basta alterar o código do método estático getNovoRetangulo();
private
static Retangulo getNovoRetangulo() { //um factory return new Retangulo(); } |
Fazendo isso o resultado fica correto mas mostra que não podemos usar a instância de Quadrado como Retangulo.
Este exemplo mostra uma violação clara do princípio LSP onde a inclusão da classe Quadrado herdando de Retangulo mudou o comportamento da classe base violando assim o princípio Open-Closed.
O principio LSP é uma extensão do Princípio Open Closed e isso significa que temos que ter certeza de que as novas classes derivadas estão estendendo as classes base, sem alterar seu comportamento.
Eu sei é apenas OOP , mas eu gosto...
"Não se turbe o vosso coração;crede em Deus, crede também em mim." (João 14:1)
Referências: