Se você acha que, porque todo quadrado é um retângulo na geometria, também deve ser em um sistema, está na hora de rever seus conceitos. Ou você ainda é do tempo que herança se define como "é um". Um quadrado "é um" retângulo.

Imagine o retângulo:

public class Retangulo
{
    public int Altura {get; set;}
    public int Largura {get; set;}
    public int Area {get; set;}
}

E um quadrado:

public class Quadrado : Retangulo
{
    private int _lado;
    public int Altura 
    {
        get
        { return _lado; }
        set
        {
            this._lado = value;
        }
    }
    //mesma coisa para largura
    //usando a variável _lado
}

Neste caso, um quadrado herda de um retângulo. Só que, em um retângulo, os lados variam independentemente. O que acontece com este teste unitário se ele receber um quadrado?

public void Area_eh_igual_altura_vezes_largura()
{
    Retangulo r = ObterRetangulo();
    r.Altura = 10;
    r.Largura = 5;
    Assert.AreEqual(50, r.Area);
}

Ele falha! Ao setar a largura para 5, acabei setando também a altura, deixando a área com 25, e não 50. Oras, mas um retângulo não é um quadrado?

A questão é que, neste caso, não é. Ele é conceitualmente, mas não no caso de reaproveitamento de código hierarquicamente, que é o que herança faz. Herança não tem nada a ver com "é um". Esse conceito até ajuda, mas não é sempre verdadeiro, como acabo de mostrar.

O quadrado quebra o Princípio de substituição de Liskov, onde uma classe filha deve substituir plenamente uma classe base. Se um conceito era válido na base, deve ser válido na filha. Nesse caso, o conceito é que os lados variam independentemente, o que não foi respeitado pelo quadrado. Em sistemas, se você não respeita o princípio, a aplicação de polimorfismo pode acabar introduzindo bugs horrorosos, como foi exatamente o caso com o teste.


Postado na(s) categoria(s) Arquitetura pelo giovanni bassi em 26 de fevereiro de 2009 às 12:41 | Tags: ,

Comentários


fevereiro 26. 2009 15:14
Juliano Oliveira
Giovanni,

O Princípio de Substituição de Liskov ficou bem claro na sua frase:

"Se um conceito era válido na base, deve ser válido na filha. Nesse caso, o conceito é que os lados variam independentemente, o que não foi respeitado pelo quadrado."

Porém no seu exemplo, eu não entendi pq não passaria no teste.
Você instanciou o Retangulo, logo a implementação da propriedade Area do retangulo seria algo como

get { return Altura * Largura }

E no Quadrado:

get { return _lado * _lado }

Mas não entendi pq o exemplo que você deu de teste o Retangulo não passaria no teste.

[]´s

http://programandoem.net/http://programandoem.net/


fevereiro 26. 2009 17:22
Antonio Carlos Zegunis Filho (tucaz)
Mto bom Giggio!

http://blog.tucaz.net/http://blog.tucaz.net/


Brazil KeitaroSan
fevereiro 27. 2009 00:25
KeitaroSan
Oi Bassi, já conhecia o OCP, mas esse princípio do Liskov é novo para mim e apesar de ter achado interessante não consigo concordar plenamente com ele.
Entendi bem qual foi a sua colocação sobre esse exemplo do Rectangle e do Square, inclusive esse parece ser um exemplo clássico, já que lendo um outro artigo(em inglês: www.objectmentor.com/resources/articles/lsp.pdf) sobre Liskov era colocado o mesmo exemplo sendo que em C++.

Agora queria deixar algumas observações/dúvidas aqui que você poderia responder:

1) A definição que você deu sobre o retângulo é algo que discordo plenamente, afinal de contas o retângulo tem a definição de quadrilátero planar fechado onde seus lados opostos tem OBRIGATORIAMENTE o mesmo tamanho e seus ângulos internos também devem possuir OBRIGATORIAMENTE 90 graus, ou seja, o fato de poder manusear o tamanho das arestas adjacentes de forma individual não é suficiente para dizer que um poligono é um Retangulo, e o Quadrado obedece a todas essas regras, inclusive as regras de cálculo de área, perimetro ou diagonal do Retangulo são aplicáveis a um Quadrado.
http://en.wikipedia.org/wiki/Rectangle

Tá, tudo isso é besteira partindo do ponto de vista que o post não é sobre matemática e você mesmo mencionou que do ponto de vista Geométrico o Quadrado é um Retângulo, eu sei, mas como a matemática é exata eu tinha que falar desse ponto também.

2) Agora falando em questão de modelagem de sistema, eu te digo que se eu olhasse por exemplo algum método que recebesse um objeto do tipo Retangulo e eu não pudesse passar para ele um objeto do tipo Quadrado, seguindo o raciocício de Liskov como seu post demonstra, eu me sentiria extremamente incomodado, ainda mais considerando que geralmente o modelo é uma abstração do negócio ao qual ele pretende atender, e se o negócio for a área de Geometria então acredito que seria correto possuir esta modelagem.

3) Infelizmente não sou um exímio programador de testes, e pelo que venho acompanhando do seu ótimo trabalho através do seu blog e de algumas matérias em revistas já deu para notar que essa é uma prática bem comum em seus trabalhos, o que acho bem legal. Eu mesmo gostaria de trabalhar com testes mas ainda não tive a oportunidade. Agora a dúvida que tenho é a seguinte: O teste não deveria ser em cima de uma implementação específica? Afinal de contas eu posso possuir uma classe base que define um coportamento padrão e em suas classes específicas possuir um comportamento diferente, ou seja, não acho que seja correto esperar que o teste seja feito somente em cima da implementação base, mas sim da implementação específica. Até entendi o ponto onde o princípio de Liskov é semelhante ao "Design by Contract", onde tenho pré-condições e pós-condições e onde eu não poderia na subclasse restringir mais as pré-condições da superclasse ou "enfraquecer" as pós-condições, concordo com isso parcialmente, faz muito sentido, porém existe aquela regra que toda regra possui sua exceção(stack overflow, hehehe, piadinha infame) e eu acredito que estejamos falando de uma exceção a essa regra.
Um exemplo que vem a cabeça agora sobre essa questão de herança e comportamento modificado na subclasse são as classes que representam os Dialects do Hibernate e uma implementação específica disso é o Dialect->MsSql2000Dialect->MsSql2005Dialect no método GetLimitString, que tem um comportamento para o MsSql2000Dialect e um comportamento totalmente reescrito para o MsSql2005Dialect, acredito que não seja possível testar este método simplesmente pedindo uma instância qualquer de MsSql2000Dialect como você pediu uma de Retangulo, mas sim garantindo que a instância não seja de nenhuma subclasse de MsSql2000Dialect onde seu comportamento pudesse ter sido reescrito. Eu sei, eu sei, eu dei uma volta danada tentando arrumar um exemplo prático e real, espero que você tenha conseguido entender o que estou tentando falar...
O seu teste não falhou devido ao cálculo de área estar errado, na verdade está é muito correto, o teste falhou foi devido ao comportamento encapsulado nos sets, já que eles fizeram uma modificação em um lugar que o seu teste não esperava.


Eu gostei muito do post, mas infelizmente não concordo com tudo que o mesmo contém e é claro, quem sou eu para achar que minhas opniões são corretas, apenas queria compartilhá-las ainda que ninguém concorde, hehehe

então é isso Smile

no site


Brazil Gustavo Rocha
fevereiro 27. 2009 00:35
Gustavo Rocha
tbm concordo com o garoto KeitaroSan.
O principio é muito correto, faz todo sentido, mas me parece rigido demais nao dando margem a excecao. Se a extensao nao permitir a substituicao comeco a repensar a praticidade.
De fato, eu acho sim que um quadrado É um retangulo, coisa que foi dita no inicio da materia. Essa é a realidade. Logo, um teste deve poder receber um quadrado e nao "dar erro" mas apresentar o comportamento esperado de um quadrado.
É um principio excepcional como inicio de raciocinio mas deve ser maleavel e nao para reaproveitar codigo mas expandir a possibilidade do É UM.

claro que nao tenho as teorias matematicas do Keitaro nem ia achar o exemplo do dialect do hibernate, ne. Mas é isso: cada um no seu quadrado, desde que todos possam ser retangulos.
abs

no site


fevereiro 27. 2009 12:11
Giovanni Bassi
Gustavo e Keitaro,

Todos nós concordamos que um quadrado é um retângulo na geometria. Isso nem é algo para concordar. É e acabou. A questão é se ele um quadrado é, ou deve ser, um retângulo na POO.
Segundo Liskov não é e não deve ser. Vocês colocam bons exemplos, que indicam que sim. Essa não é uma discussão nova, como o próprio Keitaro disse, portanto existem diversos pontos de vista.
Entendo a visão de que uma classe que herda de outra o faz para acrescentar e possivelmente mudar comportamento. No entanto, esta visão da margem aos erros como o que eu apresentei.
Quanto aos testes precisarem tratar de objetos concretos, colocada pelo Keitaro, diria que depende. Há testes com propósitos diferentes. Eu poderia, sem problemas, fazer um teste para quadrado, e outro para retângulo, e seriam testes unitários, e seriam perfeitos. Mas a questão não é essa. A questão é que temos que testar o comportamento que será usado no aplicativo final.
Se quadrados são subclasses de retângulos, em algum lugar do código da aplicação pode existir uma dependência sobre o comportamento do retângulo, que diz que os lados variam independentemente, e, ao receber um quadrado, esse comportamento é inválido, e vai gerar um bug. E nós não queremos bugs, os testes existem justamente para evitá-los.
Entendo que isso coloca restrição, e como todo princípio, não deve ser levado a ferro e fogo. Mas a princípio eu fico com Liskov, fazendo exceções quando necessário.

http://unplugged.giggio.net/http://unplugged.giggio.net/


Canada louise
março 12. 2009 16:16
louise
I loved this site!!!Congratulacions!!!

no site


Brazil Igor Quirino
março 14. 2009 00:02
Igor Quirino
Se tiver um ponto de vista mais focado, vai concordar com o Bassi..

Seguinte:
Não necessáriamente agente vai saber que é um 'Quadrado' ou um 'Retangulo'

Imaginamos o seguinte:
FormaBase > Retangulo > Quadrado
e no teste ele pedir pela FormaBase, quando for um Retangulo o calculo será correto, mas quando for Quadrado não.

E não necessáriamente devemos levar em conta Apenas Quadrado e Retangulo.. Mas no sistema em Geral.
Afinal.. Esse Erro não vai acontecer só quando for Retangulo e Quadrado..
Cada um trata de um jeito, e o resultado pode ser diferente, se for esperado um resultado igual.. Pan!!!.. Erro!..

Levemos em conta que o objeto Instanciado não é conhecido, Apenas o Base..

Abraço!..
Meu ponto de vista!..

no site


março 16. 2009 18:03
Rodrigo
Ouvi esses dias o "Uncle" Bob Martin discutindo exatamente essa questão de retângulos x quadrados no podcast do Scott Hanselman, e como sempre, deu pano pra manga.

Concordo contigo que, se formos seguirmos Liskov, deveríamos ter retângulos e quadrados como classes independentes. Mas aí vem outro problema, retângulos e quadrados, dentro de um sistema mais complexo certamente possuirão um monte de comportamentos em comum, e seguindo o princípio DRY (Don't Repeat Yourself), vamos querer que esse código em comum seja escrito em apenas um lugar. Como você implementaria tal arquitetura?

http://rodbv.wordpress.com/http://rodbv.wordpress.com/


março 17. 2009 00:43
Giovanni Bassi
Oi Rodrigo. Facil: interfaces. Não haveria dependência de comportamento, só de interface. Talvez um "IPoligonoDe4Lados"...

http://unplugged.giggio.net/http://unplugged.giggio.net/


março 17. 2009 07:38
Rodrigo
Realmente, com interfaces e algo como strategy pattern daria pra resolver isso.

Uma outra observação interessante é que a raiz do problema é que suas classes não são imutáveis, o que provavelmente seria melhor nesse caso. Uma vez criado um quadrado de altura 20, provavelmente é melhor não deixar que o programa mude a altura pra 30, a não ser criando um novo quadrado (igual acontece com strings). Geometricamente não faz sentido uma figura mudar de formato e continuar sendo a mesma figura.

Aliás, quanto mais eu penso em imutabilidade, mais eu vejo o quando isso reduz bugs, principalmente em sistemas com algo grau de concorrência, paralelismo, etc.

http://rodbv.wordpress.com/http://rodbv.wordpress.com/


março 17. 2009 10:52
Giovanni Bassi
Sem dúvida! A questão é que mutabilidade é algo requerido em diversos cenários, e temos que lidar com ela direto. Mas com certeza introduz possibilidade de bugs...

http://unplugged.giggio.net/http://unplugged.giggio.net/


Brazil suellen quirino
março 18. 2009 20:34
suellen quirino
ooiiiii alguem ai pode me ajudar a fazer umas contas?

no site


Brazil Rafael
novembro 22. 2009 19:48
Rafael
se a linha final do teste fosse:

Assert.AreEqual(r.Altura * r.Largura, r.Area);

o teste passaria mesmo com quadrado.

Eu discordo dessa visao, acho que se todo quadrado é um retangulo, então o seu modelo tem que refletir isso.

abraços

no site


novembro 23. 2009 01:12
Giovanni Bassi
Rafael, se o teste fosse esse, ele estaria sendo feito sem certeza de que está passando. r.Altura, r.Largura, e r.Area, poderiam todos ser zero, e o teste passaria.
Você pode continuar acreditando que quadrado herda de retangulo, e continuar acreditando que relações de herança são relações "é um", e que elas refletem o mundo real. Mas você vai ter problemas, cedo ou tarde. Pode escrever.

http://unplugged.giggio.net/http://unplugged.giggio.net/


Brazil Carlos Adriano
abril 11. 2010 06:06
Carlos Adriano
Pra acabar com a discussão:

O princípio da substituição não vai atrapalhar em nada na regra de negócio desse sistema.
Um quadrado pode sim herdar de um retangulo. Se a linguagem que vc utiliza, é orientada a objetos (como vejo no exemplo), então vc usa o overide de método, e faz com que ao setar a base o sistema automáticamente altere a altura pra que fiquem sempre iguais, isso apenas na classe quadrado, sem ferir a classe retangulo. Ou Seja, quando o usuario chamar um setBase, o sistema pega o valor do parametro e chama tb o setAltura da super classe. Simples assim, usando o poder da orientação a objetos através do recurso de redefinição de métodos.

Abraços, aguardo respostas, sendo elas confirmação do que tow dizendo, ou contradição se eu estiver errado.

no site


abril 11. 2010 14:54
Giovanni Bassi
Carlos, se um quadrado pode herdar de retangulo como vc está dizendo, então posso passar um quadrado onde um retangulo é esperado. usuarios da classe retangulo terao entao a mudança indevida de um lado do retangulo (na verdade um quadrado) quando eu alterar o outro lado. E isso é um bug.
Veja o teste unitário que escrevi e vai entender.

http://unplugged.giggio.net/http://unplugged.giggio.net/


Brazil Carlos Adriano
abril 11. 2010 22:42
Carlos Adriano
É verdade, olhando por essa óptica você tem razão.

Mas eu penso... Como resolver esse problema então? Usar uma classe abstrata? ou não é esse o foco? Se quadrado é uma especialização de retangulo, seria normal que alguns métodos fossem redefinidos. Isso me deixou confuso agora.

no site


Brazil João Henrique
junho 13. 2010 02:19
João Henrique
Eu entendi a questão de ambos os lados.
Carlos Adriano, não é o quadro que é um especialização e sim o retângulo.
Para um retângulo ser quadrado ele ou tem que ter todos seus lados congruentes  o as diagonais perpendiculares.
Qualquer uma dessas regras faz com o retângulo seja um quadrado.

Mas qual é o problema, não poderia haver dois construtores nessa classe, isso por sí só já não resolveria.

Quanto ao teste é um, achei interessante a observação...
E a outra teoria também é legal(Liskov) e de fato tem coisa que no teste é um passa, mas se for analisar pela regra de negócio  e  contexto que o programa esta sendo implantado se tornaria inlógico.

no site

Comentar


(Vai mostrar seu Gravatar)

  Country flag

biuquote
  • Comentário
  • Pré-visualização
Loading



Quem é Giovanni Bassi

Giovanni Bassi Sou uma pessoa apaixonada por tecnologia e especificamente por .Net. Sou consultor independente especialista em .Net, focado em arquitetura e melhores práticas. Tenho dezenas de artigos publicados na .Net Magazine, revista da qual sou editor técnico. Ministro palestras e cursos de vez em quando, e quando dá tempo eu respiro um pouco. Mais detalhes nesta página.

Busca

Selos

Eu vou ao TechEd Brasil 2010, e você?

MVP

MCPD

MCSD

.Net Magazine

Abaixo ao if!

Calendário

«  julho 2010  »
seteququsedo
2829301234
567891011
12131415161718
19202122232425
2627282930311
2345678
Ver detalhamento de posts no calendário

Blogs interessantes

    OPMLDownload OPML file

    Postagens recentes

    Comentários recentes

    Disclaimer / Aviso
    As opiniões colocadas neste blog são minhas e pessoais e não expressam necessariamente as opiniões de meus empregadores, pareceiros e amigos. Da mesma forma, os comentários feitos por leitores do blog não expressam a minha opinião.

    © Copyright 2010 .Net Unplugged
    Log in