.NET - Conceitos OOP - Um quadrado é um retângulo ?
Hoje vamos recordar alguns conceitos OOP e o princípio da substituição de Liskov. |
Todo o quadrado é um retângulo ?
Para responder esta pergunta vamos nos basear na matemática.
Sim. Todo quadrado é também um retângulo. Porém nem todo retângulo é um quadrado.
Tanto o quadrado bem como o retângulo são PARALELOGRAMOS (quadriláteros convexos que possuem dois pares de lados paralelos) que possuem propriedades comuns a ambos:
Todos os ângulos internos e externos são retos (90°);
As suas duas diagonais são congruentes (têm medidas iguais);
Os lados opostos são congruentes (têm medidas iguais).
(fonte: http://www.profcardy.com/cardicas/tirateima.php?id=19)
Dessa forma em matemática temos que:
Concluímos que um quadrado é um caso especial de retângulo onde a altura e a largura dos lados são iguais.
Herança - o relacionamento "É um"
Quando modelamos quadrados e retângulos usando linguagens orientada a objetos a definição do relacionamento entre quadrado e retângulo pode causar confusão e nos levar a erros.
Vejamos...
Vamos começar com uma recordação dos conceitos da orientação a objetos
Superclasse – É uma classe que é um supertipo de uma ou mais classes (também chamadas de subclasses).
Dessa forma uma superclasse é uma classe a partir da qual todas as suas características são herdadas por suas subclasses. Em outras palavras, todas as características de uma superclasse são reusáveis por aquelas classes que são seus subtipos.
Subclasse – É uma classe que é um subtipo de uma ou mais classes (denominadas superclasses).
Assim uma subclasse herda todas as características de suas superclasses. Em outras palavras, todas as características de uma classe são reusáveis por suas subclasses.
Como exemplo pensemos na classe Mamíferos que pode ser entendida como uma superclasse e a classe cães como uma subclasse da classe mamíferos, assim como a subclasse gatos, e a subclasse equinos, etc.
A classe Mamífero é a classe base para as subclasses Cães e Gatos.
Dessa forma existe um relacionamento entre a superclasse e a subclasse, e, podemos dizer que um Cão/Gato "É um" mamífero.
Mas atenção !!! o conceito do relacionamento "É um" pode ser levar a erros.
Ao determinar se um tipo deve herdar de outro, é importante lembrar que estamos lidando com representações de objetos do mundo real, e não com os próprios objetos.
Na relação acima, um cão da raça Lhasa Apso é um membro da classe mamífero.
O comportamento de um objeto cão labrador é também uma extensão do comportamento de um membro da classe Mamíferos.
Um relacionamento de herança é, portanto, adequado.
Há muitas situações em que a relação entre dois objetos físicos se adeqüa ao relacionamento "É um", mas os seus comportamentos não.
Quadrados e Retângulos - Quando um quadrado Não É um retãngulo
Uma das situações na qual a relação entre os objetos físicos e suas representações difere é a relação entre quadrados e retângulos.
Geometricamente falando, um quadrado é um retângulo. Ambos cumprem os requisitos de um retângulo, que são formas de quatro lados, onde todos os cantos são ângulos retos. Quadrados têm uma propriedade adicional que todos os lados têm o mesmo comprimento.
O relacionamento físico "É um" entre um quadrado e um retângulo pode levar você a desenvolver uma hierarquia de classes onde Quadrado é uma subclasse de Retangulo.
Você pode argumentar que o relacionamento físico pode ser mapeado diretamente para um relacionamento de herança no código.
Mas esse não é o caso por causa de alguns problemas sutis mas significativos em matéria de princípios básicos da orientação a objetos conhecido como princípios sólidos (SOLID), neste caso específico o princípio da substituição de Liskov.
SOLID é um acrônimo para as 5 principais padrões de desenho : SRP The Single Responsibility Principle: -- Uma classe deverá ter apenas um , e somente um , motivo para mudar; OCP The Open Closed Principle: -- Você deverá poder estender o comportamento de uma classe sem modificá-la; LSP The Liskov Substitution Principle: -- A classes derivadas devem poder ser substituídas pelas suas classes bases; ISP The Interface Segregation Principle: -- make fine grained interfaces that are client specific. DIP The Dependency Inversion Principle -- Dependa de abstração de não de implementação. |
Vamos então mostrar que a relação de herança no caso de quadrados e retângulos falha, criando as classes Quadrado e Retangulo e analisando os problemas que surgem dessa abordagem.
Vou usar um código bem simples para destacar o problema.
Abra o Visual C# 2010 Express Edition e crie um novo projeto do tipo Console Application com o nome QuadradosRetangulosoOOP;
A seguir cria uma classe chamada Retangulo com as propriedades Altura e Largura e um método CalcularArea com o seguinte código:
namespace QuadradosRetangulosOOP { public class Retagulo { public virtual int Altura { get; set; } public virtual int Largura { get; set; } public int CalcularArea() { return Altura * Largura; } } } |
A seguir crie uma classe Quadrado usando a classe Retangulo como sua classe base:
namespace QuadradosRetangulosOOP { public class Quadrado : Retangulo { } } |
O tipo Quadrado não é ideal, uma vez que permite que quadrados sejam definidos sem uma altura e largura correspondente.
Atualmente Quadrado nada mais é do que um retângulo com um novo nome.
Uma maneira de atualizar a classe, para garantir que os objetos representados sejam de fato um quadrado, é substituir as propriedades set. Quando uma dimensão for definida, a outra pode ser atualizada para corresponder.
Vamos sobrescrever as propriedades Altura e Largura conforme o código mostrado abaixo:
namespace QuadradosRetangulosOOP { public class Quadrado : Retangulo { public override int Altura { get { return base.Altura; } set { base.Altura = base.Largura = value; } } public override int Largura { get { return base.Altura; } set { base.Altura = base.Largura = value; } } } } |
Identificando os problemas
Quando considerada isoladamente a classe Quadrado agora funciona corretamente. Não é possível a criação de um objeto quadrado com dimensões erradas.
No entanto, para fazer isso nós acabamos violando o princípio da substituição de Liskov (LSP) e introduzimos erros em clientes da classe Retangulo.
A violação mais óbvia do LSP é um problema com invariantes.
Clientes da classe Retangulo sabem que quando a propriedade altura é alterada a propriedade largura é invariável. Com a introdução da classe Quadrado esta regra foi quebrada.
Considere o seguinte código.
using System; namespace QuadradosRetangulosOOP { public class Program { static void Main(string[] args) { Retangulo r1 = new Retangulo(); r1.Altura = 4; r1.Largura = 5; Retangulo r2 = new Retangulo(); r2.Largura = 5; r2.Altura = 4; bool coincide = r1.Altura == r2.Altura && r1.Largura == r2.Largura; // coincide = True Console.WriteLine(coincide); Console.ReadKey(); } } } |
Dois objetos retângulos r1 e r2 são criados e suas alturas e larguras são definidas.
No primeiro caso, r1, a altura é definida antes da largura e no segundo caso, r2, a largura é definida em primeiro lugar. Isto não importa, ambas as propriedades são invariantes.
A última linha do código compara os dois retângulos e determina que eles têm dimensões correspondentes.
O princípio da substituição de Liskov (LSP) determina que a substituição de um objeto de uma subclasse não deve mudar o seu comportamento ou o funcionamento correto do programa.
No entanto, se substituirmos quadrados por retângulos o resultado da comparação é falsa.
Isto ocorre porque quando mudamos uma dimensão do quadrado a outra dimensão é afetada e a ordem em que a largura e altura são especificadas agora torna-se importante.
O problema se agrava a medida que mais propriedades e métodos forem definidos na classe base.
Solucionando o problema
A solução para o problema é não usar o relacionamento de herança.("É um")
Uma alternativa seria ter uma classe base "Forma" ou uma interface "IForma" que seja compartilhada por retângulos e quadrados.
A classe base não incluiria propriedades de altura e largura, não permitindo assim que as dimensões de diferentes formas sejam manipuladas por suas implementações.
Uma subclasse Retangulo teria as propriedades altura e largura, e, uma classe Quadrado poderia ter um valor de tamanho único.
O princípio da substituição Liskov nos diz que relações de herança devem ser baseadas no comportamento externo de tipos e não sobre as características dos objetos do mundo real que eles podem representar.
Apesar de um quadrado ser um retângulo, o comportamento externo das duas representações é incompatível, de modo que a herança é inválida.
Esse problema se aplica aos quadrados e retângulos, círculos, elipses e muitos outros objetos físicos que você pode desejar modelar.
A Herança é uma ferramenta poderosa para o desenvolvedor orientado a objetos, mas você deve ter muito cuidado e considerar o comportamento externo antes de aplicar a herança na sua modelagem.
1Pe 2:20
Pois, que glória é essa, se, quando cometeis pecado e sois por isso
esbofeteados, sofreis com paciência? Mas se, quando fazeis o bem e sois
afligidos, o sofreis com paciência, isso é agradável a Deus.
1Pe 2:21
Porque para isso fostes chamados, porquanto também Cristo padeceu por vós,
deixando-vos exemplo, para que sigais as suas pisadas.
Referências: