Quadrado herda de retângulo? Ou, dando uma olhada no princípio de Liskov

Email

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.

Email
Esse post foi publicado em Sem categoria e marcado por Giovanni Bassi. Marcar link permanente.

Sobre Giovanni Bassi

Arquiteto e desenvolvedor, agilista, pai, filho, namorado, escalador, provocador.
Programa porque gosta, e começou a trabalhar com isso porque acha que trabalhar como administrador é meio chato. Por esse motivo sempre diz que nunca mais vai virar gerente de ninguém. E também porque acredita que pessoas autogerenciadas funcionam melhor e por acreditar que heterarquia é melhor que hierarquia. Mas isso é outro assunto.
Foi reconhecido  Microsoft MVP depois que alguém notou que ele não dormia a noite pra ficar escrevendo artigos, cuidando e participando do .Net Architects, gravando o podcast Tecnoretórica, escrevendo posts no blog e falando o que bem entende no twitter @giovannibassi. E por falar nisso é no twitter que conta pra todos que gerencia de projetos deve ser feita pelo time e não por um gerentes, que greves em TI são coisas sem sentido e que stored procedure com regras de negócio são malígnas.
Você já deve ter percebido (até porque está lá na primeira frase) que Giovanni é agilista. De tanto gostar disso ele trouxe os programas de certificação e treinamento  PSD e PSM da Scrum.org pro Brasil, e por causa deles, do MVP e de algum trabalho que aparece tem que ficar indo pros EUA de vez enquando, coisa que prefere não fazer. (É bem comum você ouvir ele perguntando porque a Scrum.org e a Microsoft não estão na Itália, por exemplo.)
Junto com alguns Jedis criou a Lambda3, que, apesar de ser pequena e de não ser muito comum no Brasil, insiste em fazer projetos e consultoria direito. Por causa da Lambda3 ele tem trabalhado mais do que quando era consultor independente, mas menos do que a maioria das pessoas. Quer dizer, isso se você considerar que os trabalhos junto à comunidade não são trabalho, caso contrário ele trabalha mais que a maioria das pessoas.
Recentemente ele resolveu que merecia viver melhor e ganhar uns anos de vida e desistiu de ser sedentário, fazendo algum barulho de vez em quando com os amigos no twitter com a hashtag #DotNetEmForma. Por causa do convite recente de amigos do lado Open Source (que ele respeita e admira), começou a escalar, e agora está sempre com as mãos machucadas. Mas ainda dá pra programar. Você encontra ele sempre em algum evento, como o TechEd, e o DNAD, mas também outros menos comuns para o pessoal do .NET, como a RubyConf. Nesses eventos, ou ele está vendo palestras, ou batendo papo com alguém, ou codando alguma aplicação que alguém achou que dava pra fazer durante o evento.
  • http://programandoem.net/ 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://blog.tucaz.net/ Antonio Carlos Zegunis Filho (tucaz)

    Mto bom Giggio!

  • 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: http://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 :)

  • 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

  • http://unplugged.giggio.net/ 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.

  • louise

    I loved this site!!!Congratulacions!!!

  • 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!..

  • http://rodbv.wordpress.com/ 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://unplugged.giggio.net/ Giovanni Bassi

    Oi Rodrigo. Facil: interfaces. Não haveria dependência de comportamento, só de interface. Talvez um "IPoligonoDe4Lados"…

  • http://rodbv.wordpress.com/ 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://unplugged.giggio.net/ 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…

  • suellen quirino

    ooiiiii alguem ai pode me ajudar a fazer umas contas?

  • 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

  • http://unplugged.giggio.net/ 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.

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

  • http://unplugged.giggio.net/ 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.

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

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