Por que o Square herdando do Rectangle seria problemático se substituirmos os métodos SetWidth e SetHeight?

103

Se um quadrado é um tipo de retângulo, então por que um quadrado não pode herdar de um retângulo? Ou porque é um projeto ruim?

Eu ouvi as pessoas dizerem:

If you made Square derive from Rectangle, then a Square should be usable anywhere you expect a rectangle

Qual é o problema aqui? E por que Square seria utilizável em qualquer lugar que você esperasse um retângulo? Só seria utilizável se criarmos o objeto Square e se substituirmos os métodos SetWidth e SetHeight por Square do que por que haveria algum problema?

If you had SetWidth and SetHeight methods on your Rectangle base class and if your Rectangle reference pointed to a Square, then SetWidth and SetHeight don't make sense because setting one would change the other to match it. In this case Square fails the Liskov Substitution Test with Rectangle and the abstraction of having Square inherit from Rectangle is a bad one.

Alguém pode explicar os argumentos acima? Novamente, se ultrapassarmos os métodos SetWidth e SetHeight no Square, não resolveria esse problema?

Eu também ouvi / li:

The real issue is that we are not modeling rectangles, but rather "reshapable rectangles" i.e., rectangles whose width or height can be modified after creation (and we still consider it to be the same object). If we look at the rectangle class in this way, it is clear that a square is not a "reshapable rectangle", because a square cannot be reshaped and still be a square (in general). Mathematically, we don't see the problem because mutability doesn't even make sense in a mathematical context

Aqui eu acredito que "re-dimensionável" é o termo correto. Os retângulos são "redimensionáveis" e, portanto, são quadrados. Estou faltando alguma coisa no argumento acima? Um quadrado pode ser redimensionado como qualquer retângulo.

    
por user793468 07.05.2014 / 07:21
fonte

12 respostas

186

Basicamente, queremos que as coisas se comportem de maneira sensata.

Considere o seguinte problema:

Recebo um grupo de retângulos e quero aumentar sua área em 10%. Então, o que eu faço é definir o comprimento do retângulo para 1,1 vezes o que era antes.

public void IncreaseRectangleSizeByTenPercent(IEnumerable<Rectangle> rectangles)
{
  foreach(var rectangle in rectangles)
  {
    rectangle.Length = rectangle.Length * 1.1;
  }
}

Agora, neste caso, todos os meus retângulos agora têm seu comprimento aumentado em 10%, o que aumentará sua área em 10%. Infelizmente, alguém passou por mim uma mistura de quadrados e retângulos, e quando o comprimento do retângulo foi alterado, o mesmo aconteceu com a largura.

Meus testes de unidade são aprovados porque eu escrevi todos os meus testes de unidade para usar uma coleção de retângulos. Eu agora introduzi um bug sutil no meu aplicativo, que pode passar despercebido por meses.

Pior ainda, Jim da contabilidade vê meu método e escreve algum outro código que usa o fato de que, se ele passar quadrados para o meu método, ele obterá um aumento de 21% no tamanho. Jim é feliz e ninguém é mais sábio.

Jim é promovido por excelente trabalho para uma divisão diferente. Alfred se junta à empresa como júnior. Em seu primeiro relatório de bug, Jill, da Advertising, relatou que a passagem de quadrados para esse método resulta em um aumento de 21% e quer que o bug seja corrigido. Alfred vê que Squares e Rectangles são usados em todo o código e percebe que quebrar a cadeia de herança é impossível. Ele também não tem acesso ao código-fonte da Contabilidade. Então Alfred corrige o bug assim:

public void IncreaseRectangleSizeByTenPercent(IEnumerable<Rectangle> rectangles)
{
  foreach(var rectangle in rectangles)
  {
    if (typeof(rectangle) == Rectangle)
    {
      rectangle.Length = rectangle.Length * 1.1;
    }
    if (typeof(rectangle) == Square)
    {
      rectangle.Length = rectangle.Length * 1.04880884817;
    }
  }
}

Alfred está feliz com suas habilidades de hacker e Jill assina que o bug foi corrigido.

No próximo mês, ninguém é pago porque a Contabilidade estava dependente de poder passar quadrados para o método IncreaseRectangleSizeByTenPercent e obter um aumento na área de 21%. Toda a empresa entra no modo "correção de prioridade 1" para rastrear a origem do problema. Eles rastreiam o problema para a correção de Alfred. Eles sabem que precisam manter a Contabilidade e a Publicidade felizes. Então, eles corrigem o problema identificando o usuário com a chamada do método da seguinte forma:

public void IncreaseRectangleSizeByTenPercent(IEnumerable<Rectangle> rectangles)
{
  IncreaseRectangleSizeByTenPercent(
    rectangles, 
    new User() { Department = Department.Accounting });
}

public void IncreaseRectangleSizeByTenPercent(IEnumerable<Rectangle> rectangles, User user)
{
  foreach(var rectangle in rectangles)
  {
    if (typeof(rectangle) == Rectangle || user.Department == Department.Accounting)
    {
      rectangle.Length = rectangle.Length * 1.1;
    }
    else if (typeof(rectangle) == Square)
    {
      rectangle.Length = rectangle.Length * 1.04880884817;
    }
  }
}

E assim por diante e assim por diante.

Esta anedota é baseada em situações do mundo real que enfrentam programadores diariamente. Violações do Princípio da Substituição de Liskov podem introduzir bugs muito sutis que só são captados anos depois de serem escritos, quando a correção da violação vai quebrar um monte de coisas e não consertar ele irá irritar seu maior problema cliente.

Existem duas formas realistas de corrigir este problema.

A primeira maneira é tornar o Rectangle imutável. Se o usuário do Retângulo não puder alterar as propriedades Comprimento e Largura, esse problema desaparece. Se você quiser um retângulo com comprimento e largura diferentes, crie um novo. Quadrados podem herdar de retângulos alegremente.

A segunda maneira é quebrar a cadeia de herança entre quadrados e retângulos. Se um quadrado for definido como tendo uma única propriedade SideLength e os retângulos tiverem uma propriedade Length e Width e não houver herança, será impossível acidentalmente quebrar as coisas esperando um retângulo e obtendo um quadrado. Em termos de C #, você pode usar seal de sua classe de retângulo, o que garante que todos os Retângulos que você recebe sejam na verdade Retângulos.

Nesse caso, gosto da maneira "objetos imutáveis" de consertar o problema. A identidade de um retângulo é seu comprimento e largura. Faz sentido que quando você quer mudar a identidade de um objeto, o que você realmente quer é um objeto novo . Se você perder um cliente antigo e ganhar um novo cliente, não alterará o campo Customer.Id do cliente antigo para o novo, criará um novo Customer .

Violações do princípio de Substituição de Liskov são comuns no mundo real, principalmente porque muito código lá fora é escrito por pessoas que são incompetentes / sob pressão de tempo / não se importam / cometem erros. Pode e leva a alguns problemas muito desagradáveis. Na maioria dos casos, você quer preferir a composição sobre a herança .

    
por 07.05.2014 / 09:19
fonte
29

Se todos os seus objetos são imutáveis, não há problema. Cada quadrado também é um retângulo. Todas as propriedades de um retângulo também são propriedades de um quadrado.

O problema começa quando você adiciona a capacidade de modificar os objetos. Ou realmente - quando você começa a passar argumentos para o objeto, não apenas lendo getters de propriedades.

Existem modificações que você pode fazer em um retângulo que mantém todas as invariantes da sua classe Rectangle, mas não todas as invariantes da Square, como alterar a largura ou a altura. De repente, o comportamento de um retângulo não é apenas suas propriedades, mas também suas possíveis modificações. Não é apenas o que você obtém do retângulo, é também o que você pode colocar .

Se o seu Retângulo tiver um método setWidth documentado como alterando a largura e não modificando a altura, o Square não poderá ter um método compatível. Se você alterar a largura e não a altura, o resultado não será mais um quadrado válido. Se você optar por modificar a largura e a altura do Square ao usar setWidth , não estará implementando a especificação de setWidth de Rectangle. Você simplesmente não pode vencer.

Quando você olha o que você pode "colocar" em um retângulo e um quadrado, que mensagens você pode enviar para eles, você provavelmente encontrará qualquer mensagem que possa enviar validamente para um quadrado. Você também pode enviar para um Retângulo.

É uma questão de co-variância vs. contra-variância.

Os métodos de uma subclasse apropriada, em que as instâncias podem ser usadas em todos os casos em que a superclasse é esperada, exigem que cada método:

  • Retorna apenas os valores que a superclasse retornaria - isto é, o tipo de retorno deve ser um subtipo do tipo de retorno do método da superclasse. O retorno é co-variante.
  • Aceite todos os valores que o supertipo aceitaria - isto é, os tipos de argumentos devem ser supertipos dos tipos de argumentos do método da superclasse. Argumentos são contra-variantes.

Então, de volta para Rectangle e Square: se o Square pode ser uma subclasse do Rectangle depende inteiramente dos métodos que o Rectangle possui.

Se o retângulo tiver setters individuais para largura e altura, o Square não fará uma boa subclasse.

Da mesma forma, se você fizer alguns métodos serem co-variantes nos argumentos, como ter compareTo(Rectangle) no retângulo e compareTo(Square) no quadrado, você terá um problema ao usar um quadrado como um retângulo.

Se você projetar seu Square e Rectangle para ser compatível, provavelmente funcionará, mas eles devem ser desenvolvidos juntos ou eu aposto que não funcionará.

    
por 07.05.2014 / 13:19
fonte
16

Há muitas boas respostas aqui; A resposta de Stephen, em particular, ilustra bem por que violações do princípio da substituição levam a conflitos reais entre equipes.

Eu pensei que poderia falar brevemente sobre o problema específico de retângulos e quadrados, em vez de usá-lo como uma metáfora para outras violações do LSP.

Existe um problema adicional com o quadrado-é-um-tipo-de-rectângulo especial que raramente é mencionado, ou seja: porque estamos a parar com quadrados e rectângulos ? Se estamos dispostos a dizer que um quadrado é um tipo especial de retângulo, então certamente também devemos estar dispostos a dizer:

  • Um quadrado é um tipo especial de losango - é um losango com ângulos quadrados.
  • Um losango é um tipo especial de paralelogramo - é um paralelogramo com lados iguais.
  • Um retângulo é um tipo especial de paralelogramo - é um paralelogramo com ângulos quadrados
  • Um retângulo, um quadrado e um paralelogramo são todos um tipo especial de trapézio - eles são trapezóides com dois conjuntos de lados paralelos
  • Todas as opções acima são tipos especiais de quadriláteros
  • Todas as opções acima são tipos especiais de formas planas
  • E assim por diante; Eu poderia continuar por algum tempo aqui.

Que diabos deveriam ter todos os relacionamentos aqui? Linguagens baseadas em herança de classes, como C # ou Java, não foram projetadas para representar esses tipos de relacionamentos complexos com vários tipos diferentes de restrições. É melhor simplesmente evitar a questão por não tentar representar todas essas coisas como classes com relacionamentos de subtipagem.

    
por 09.05.2014 / 18:44
fonte
13

De uma perspectiva matemática, um quadrado é um retângulo. Se um matemático modifica o quadrado para não aderir mais ao contrato quadrado, ele se transforma em um retângulo.

Mas no design OO, isso é um problema. Um objeto é o que é, e isso inclui comportamentos , bem como estado. Se eu mantenho um objeto quadrado, mas alguém o modifica para ser um retângulo, isso viola o contrato do quadrado sem nenhuma falha minha. Isso faz com que todos os tipos de coisas ruins aconteçam.

O fator chave aqui é mutabilidade . Uma forma pode mudar depois de construída?

  • Mutável: se as formas puderem ser alteradas depois de construídas, um quadrado não poderá ter uma relação é-um com o retângulo. O contrato de um retângulo inclui a restrição de que os lados opostos devem ter o mesmo comprimento, mas os lados adjacentes não precisam ser. O quadrado deve ter quatro lados iguais. Modificar um quadrado por meio de uma interface retangular pode violar o contrato em quadrado.

  • Imutável: se as formas não podem mudar uma vez construídas, então um objeto quadrado também deve sempre preencher o contrato retângulo. Um quadrado pode ter uma relação é com um retângulo.

Em ambos os casos, é possível pedir a um quadrado para produzir uma nova forma com base em seu estado com uma ou mais alterações. Por exemplo, pode-se dizer "crie um novo retângulo baseado neste quadrado, exceto que os lados opostos A e C são duas vezes mais longos". Desde que um novo objeto está sendo construído, o quadrado original continua a aderir aos seus contratos.

    
por 07.05.2014 / 08:00
fonte
13

And why would Square be usable anywhere you expect a rectangle?

Porque isso é parte do que significa ser um subtipo (veja também: Princípio da substituição de Liskov). Você pode fazer, precisa ser capaz de fazer isso:

Square s = new Square(5);
Rect r = s;
doSomethingWith(r); // written assuming a Rect, actually calls Square methods

Você realmente faz isso o tempo todo (às vezes de forma ainda mais implícita) ao usar o OOP.

and if we over-ride the SetWidth and SetHeight methods for Square than why would there be any issue?

Porque você não pode substituí-los sensivelmente por Square . Porque um quadrado não pode "ser redimensionado como qualquer retângulo". Quando a altura de um retângulo muda, a largura permanece a mesma. Mas quando a altura de um quadrado muda, a largura deve mudar de acordo. O problema não está apenas sendo redimensionável, está sendo redimensionável em ambas as dimensões de forma independente.

    
por 07.05.2014 / 07:31
fonte
9

O que você está descrevendo entra em conflito com o que é chamado de Princípio de Substituição de Liskov . A idéia básica do LSP é que sempre que você usar uma instância de uma classe em particular, você deve sempre ser capaz de trocar em uma instância de qualquer subclasse daquela classe, sem introduzir erros.

O problema do Retângulo-Quadrado não é realmente uma boa maneira de apresentar Liskov. Ele tenta explicar um princípio amplo usando um exemplo que é na verdade bastante sutil, e entra em conflito com um dos definições intuitivas mais comuns em toda a matemática. Alguns o chamam de problema Elipse-Círculo por esse motivo, mas é apenas um pouco melhor na medida em que isso acontece. Uma abordagem melhor é dar um pequeno passo para trás, usando o que chamo de problema Paralelogramo-Retângulo. Isso torna as coisas muito mais fáceis de entender.

Um paralelogramo é um quadrilátero com dois pares de lados paralelos. Também possui dois pares de ângulos congruentes. Não é difícil imaginar um objeto Parallelogram ao longo destas linhas:

class Parallelogram {
    function getSideA() {};
    function getSideB() {};
    function getAngleA() {};
    function getAngleB() {};
    function setSideA(newLength) {};
    function setSideB(newLength) {};
    function setAngleA(newAngle) {};
    function setAngleB(newAngle) {};
}

Uma maneira comum de pensar em um retângulo é como um paralelogramo com ângulos retos. À primeira vista, isso pode fazer do Rectangle um bom candidato para herdar do Parallelogram , para que você possa reutilizar todo o código. No entanto:

class Rectangle extends Parallelogram {
    function getSideA() {};
    function getSideB() {};
    function getAngleA() {};
    function getAngleB() {};
    function setSideA(newLength) {};
    function setSideB(newLength) {};

    /* BUG: Liskov violations ahead */
    function setAngleA(newAngle) {};
    function setAngleB(newAngle) {};
}

Por que essas duas funções introduzem bugs no Rectangle? O problema é que você não pode alterar os ângulos em um retângulo : eles são definidos como sempre sendo 90 graus e, portanto, essa interface não funciona realmente para o Retângulo herdar do Paralelogramo. Se eu trocar um retângulo em código que espera um paralelogramo, e esse código tentar alterar o ângulo, quase certamente haverá bugs. Pegamos algo que foi gravável na subclasse e o transformou em somente leitura, e isso é uma violação Liskov.

Agora, como isso se aplica a praças e retângulos?

Quando dizemos que você pode definir um valor, geralmente queremos dizer algo um pouco mais strong do que apenas ser capaz de escrever um valor para ele. Nós sugerimos um certo grau de exclusividade: se você definir um valor e, em seguida, bloquear algumas circunstâncias extraordinárias, ele permanecerá nesse valor até que você o defina novamente. Há muitos usos para valores que podem ser Escrito para, mas não permanece definido, mas há também muitos casos que dependem de um valor ficar onde está quando você defini-lo. E é aí que nos deparamos com outro problema.

class Square extends Rectangle {
    function getSideA() {};
    function getSideB() {};
    function getAngleA() {};
    function getAngleB() {};

    /* BUG: More Liskov violations */
    function setSideA(newLength) {};
    function setSideB(newLength) {};

    /* Liskov violations inherited from Rectangle */
    function setAngleA(newAngle) {};
    function setAngleB(newAngle) {};
}

Nossa classe Square herdou bugs do Rectangle, mas tem alguns novos. O problema com setSideA e setSideB é que nenhum destes é verdadeiramente configurável: você ainda pode escrever um valor em qualquer um deles, mas ele mudará de dentro de você se o outro for gravado. Se eu trocar isso por um paralelogramo no código que depende de ser capaz de definir os lados independentemente um do outro, vai surtar.

Esse é o problema, e é por isso que há um problema em usar o Rectangle-Square como uma introdução a Liskov. Retângulo-Quadrado depende da diferença entre ser capaz de escrever para algo e ser capaz de definir , e isso é uma diferença muito mais sutil do que ser capaz de definir algo versus ter seja somente leitura. O Retângulo-Quadrado ainda tem valor como exemplo, porque ele documenta uma pegadinha bastante comum que deve ser observada, mas não deve ser usada como um exemplo introdutório . Deixe o aprendiz se basear primeiro no básico, e então jogue algo mais strong neles.

    
por 08.05.2014 / 19:04
fonte
8

A subtipagem é sobre o comportamento.

Para o tipo B ser um subtipo do tipo A , ele deve suportar todas as operações que o tipo A suporta com a mesma semântica (conversa fiada para "comportamento"). Usando o raciocínio de que todo B é um A , o não funciona - a compatibilidade de comportamento tem a palavra final. Na maior parte do tempo, "B é um tipo de A" se sobrepõe a "B se comporta como A", mas nem sempre .

Um exemplo:

Considere o conjunto de números reais. Em qualquer idioma, podemos esperar que eles ofereçam suporte às operações + , - , * e / . Agora considere o conjunto de inteiros positivos ({1, 2, 3, ...}). Claramente, todo inteiro positivo também é um número real. Mas é o tipo de inteiros positivos um subtipo do tipo de números reais? Vamos examinar as quatro operações e ver se números inteiros positivos se comportam da mesma maneira que números reais:

  • + : Podemos adicionar inteiros positivos sem problemas.
  • - : nem todas as subtrações de inteiros positivos resultam em inteiros positivos. Por exemplo. 3 - 5 .
  • * : Podemos multiplicar inteiros positivos sem problemas.
  • / : Nem sempre podemos dividir inteiros positivos e obter um inteiro positivo. Por exemplo. 5 / 3 .

Portanto, apesar de inteiros positivos serem um subconjunto de números reais, eles não são um subtipo. Um argumento similar pode ser feito para números inteiros de tamanho finito. Claramente, todo número inteiro de 32 bits também é um inteiro de 64 bits, mas 32_BIT_MAX + 1 fornecerá resultados diferentes para cada tipo. Então, se eu te dei algum programa e você mudou o tipo de toda a variável inteira de 32 bits para inteiros de 64 bits, há uma boa chance de o programa se comportar de maneira diferente (o que quase sempre significa erradamente ). / p>

Naturalmente, você pode definir + para ints de 32 bits para que o resultado seja um inteiro de 64 bits, mas agora você terá que reservar 64 bits de espaço toda vez que adicionar dois números de 32 bits. Isso pode ou não ser aceitável para você, dependendo das suas necessidades de memória.

Por que isso importa?

É importante que os programas estejam corretos. É indiscutivelmente a propriedade mais importante para um programa. Se um programa estiver correto para algum tipo A , a única maneira de garantir que o programa continuará a ser correto para algum subtipo B é se B se comportar como A em todos os sentidos.

Então você tem o tipo de Rectangles , cuja especificação diz que seus lados podem ser alterados independentemente. Você escreveu alguns programas que usam Rectangles e assume que a implementação segue a especificação. Então você introduziu um subtipo chamado Square cujos lados não podem ser redimensionados independentemente. Como resultado, a maioria dos programas que redimensionam retângulos agora estará errada.

    
por 07.05.2014 / 14:18
fonte
6

If a Square is a type of Rectangle than why cant a Square inherit from a Rectangle? Or why is it a bad design?

Antes de tudo, pergunte-se por que você acha que um quadrado é um retângulo.

É claro que a maioria das pessoas aprendeu isso na escola primária, e isso parece óbvio. Um retângulo é uma forma de 4 faces com ângulos de 90 graus e um quadrado preenche todas essas propriedades. Então não é um quadrado um retângulo?

O problema é que tudo depende de qual é o seu critério inicial para agrupar objetos, que contexto você está olhando para esses objetos. Em geometria, as formas são classificadas com base nas propriedades de seus pontos, linhas e anjos.

Portanto, antes mesmo de dizer "um quadrado é um tipo de retângulo", primeiro você precisa se perguntar, é baseado em critérios que me interessam .

Na grande maioria dos casos, não será com o que você se importa. A maioria dos sistemas que modelam formas, como GUIs, gráficos e videogames, não se preocupam principalmente com o agrupamento geométrico de um objeto, mas com o comportamento. Você já trabalhou em um sistema que importava que um quadrado fosse um tipo de retângulo no sentido geométrico? O que isso lhe daria, sabendo que tem 4 lados e ângulos de 90 graus?

Você não está modelando um sistema estático, está modelando um sistema dinâmico onde as coisas vão acontecer (as formas serão criadas, destruídas, alteradas, desenhadas, etc.). Neste contexto, você se preocupa com o comportamento compartilhado entre os objetos, porque sua principal preocupação é o que você pode fazer com uma forma, quais regras devem ser mantidas para ainda ter um sistema coerente.

Neste contexto, um quadrado definitivamente não é um retângulo , porque as regras que governam como o quadrado pode ser alterado não são iguais ao retângulo. Então eles não são o mesmo tipo de coisa.

Nesse caso, não os modele como tal. Por que você? Não ganha nada além de uma restrição desnecessária.

It would only be usable if we create the Square object, and if we override the SetWidth and SetHeight methods for Square than why would there be any issue?

Se você fizer isso, você está praticamente declarando no código que eles não são a mesma coisa. Seu código estaria dizendo que um quadrado se comporta dessa maneira e um retângulo se comporta dessa maneira, mas eles ainda são os mesmos.

Eles claramente não são os mesmos no contexto que você se importa, porque você acabou de definir dois comportamentos diferentes. Então, por que fingir que são iguais se eles são semelhantes em um contexto que você não se importa?

Isso destaca um problema significativo quando os desenvolvedores acessam um domínio que desejam modelar. É tão importante esclarecer em qual contexto você está interessado antes de começar a pensar sobre os objetos no domínio. Em que aspecto você está interessado? Milhares de anos atrás, os gregos se preocuparam com as propriedades compartilhadas das linhas e anjos das formas, e as agruparam com base nelas. Isso não significa que você é forçado a continuar esse agrupamento se não for com o que você se importa (o que, em 99% das vezes, modelará em software com o qual você não se importará).

Muitas das respostas a esta questão centram-se na sub-digitação, referindo-se ao agrupamento de comportamentos , "causando-lhes as regras" .

Mas é tão importante entender que você não está fazendo isso apenas para seguir as regras. Você está fazendo isso porque na grande maioria dos casos isso é o que você realmente se importa também. Você não se importa se um quadrado e um retângulo compartilham os mesmos anjos internos. Você se preocupa com o que eles podem fazer enquanto ainda são quadrados e retângulos. Você se preocupa com o comportamento dos objetos porque está modelando um sistema que está focado em mudar o sistema com base nas regras do comportamento dos objetos.

    
por 08.05.2014 / 10:56
fonte
5

If a Square is a type of Rectangle than why cant a Square inherit from a Rectangle?

O problema está em pensar que, se as coisas estão relacionadas de alguma forma na realidade, elas devem ser relacionadas exatamente da mesma maneira depois da modelagem.

O mais importante na modelagem é identificar os atributos comuns e os comportamentos comuns, defini-los na classe básica e adicionar atributos adicionais nas classes filhas.

O problema com o seu exemplo é completamente abstrato. Contanto que ninguém saiba, para o que você planeja usar essas aulas, é difícil adivinhar qual projeto você deve fazer. Mas se você realmente quiser ter apenas altura, largura e redimensionamento, seria mais lógico:

  • define Square como classe base, com o parâmetro width e resize(double factor) redimensionando a largura pelo fator especificado
  • define a classe Rectangle e a subclasse de Square, porque adiciona outro atributo, height , e substitui a função resize , que chama super.resize e, em seguida, redimensiona a altura pelo fator especificado

Do ponto de vista da programação, não há nada no Square, que o Rectangle não tenha. Não há sentido em fazer um Square como a subclasse de Rectangle.

    
por 07.05.2014 / 16:03
fonte
4

Porque pelo LSP, criar uma relação de herança entre os dois e substituir setWidth e setHeight para garantir que o square tenha ambos como iguais introduz um comportamento confuso e não intuitivo. Vamos dizer que temos um código:

Rectangle r = createRectangle(); // create rectangle or square here
r.setWidth(10);
r.setHeight(20);
print(r.getWidth()); // expect to print 10
print(r.getHeight()); // expect to print 20

Mas se o método createRectangle retornou Square , porque é possível graças a Square herdando de Rectange . Então as expectativas são quebradas. Aqui, com este código, esperamos que a largura ou a altura da configuração apenas altere a largura ou a altura, respectivamente. O ponto de OOP é que quando você trabalha com a superclasse, você não tem nenhum conhecimento de nenhuma subclasse sob ela. E se a subclasse mudar o comportamento de modo que vá contra as expectativas que temos sobre a superclasse, então há uma grande chance de que erros ocorram. E esses tipos de bugs são difíceis de depurar e corrigir.

Uma das principais idéias sobre a OOP é que é o comportamento, não os dados herdados (o que também é um dos principais equívocos da IMO). E se você olhar para o quadrado e o retângulo, eles não têm nenhum comportamento em si que possamos relacionar na relação de herança.

    
por 07.05.2014 / 08:37
fonte
2

O que o LSP diz é que qualquer coisa que herda de Rectangle deve ser Rectangle . Ou seja, ele deve fazer o que for que Rectangle faça.

Provavelmente, a documentação de Rectangle foi escrita para informar que o comportamento de um Rectangle denominado r é o seguinte:

r.setWidth(10);
r.setHeight(20);
print(r.getWidth());  // prints 10

Se o seu Square não tiver o mesmo comportamento, ele não se comportará como Rectangle . Então, o LSP diz que não deve herdar de Rectangle . A linguagem não pode impor essa regra, porque ela não pode impedi-lo de fazer algo errado em uma substituição de método, mas isso não significa "está tudo bem, porque a linguagem me permite substituir os métodos" é um argumento convincente para fazer isso!

Agora, seria possível escrever a documentação de Rectangle de tal forma que não implique que o código acima imprima 10, caso em que talvez seu Square possa seja um Rectangle . Você pode ver a documentação que diz algo como "isso faz X. Além disso, a implementação nesta classe faz Y". Se sim, então você tem um bom argumento para extrair uma interface da classe e distinguir entre o que a interface garante e o que a classe garante além disso. Mas quando as pessoas dizem que "um quadrado mutável não é um retângulo mutável, enquanto um quadrado imutável é um retângulo imutável", elas estão basicamente assumindo que o acima é de fato parte da definição razoável de um retângulo mutável.

    
por 07.05.2014 / 18:25
fonte
1

Subtipos e, por extensão, programação OO, freqüentemente dependem do Princípio de Substituição de Liskov, que qualquer valor do tipo A pode ser usado onde um B é requerido, se A < = B. Isto é basicamente um axioma em OO. arquitetura, ie. presume-se que todas as subclasses terão essa propriedade (e, se não, os subtipos têm bugs e precisam ser corrigidos).

No entanto, verifica-se que este princípio é irrealista / não representativo da maioria dos códigos, ou mesmo impossível de satisfazer (em casos não triviais)! Esse problema, conhecido como o problema de retângulo quadrado ou o problema de círculo-elipse ( link ) é um exemplo famoso de quão difícil é cumprir.

Note que nós poderíamos implementar Quadrados e Retângulos cada vez mais equivalentes a observacionais, mas apenas jogando fora mais e mais funcionalidades até que a distinção seja inútil.

Como exemplo, consulte o link

    
por 08.05.2014 / 18:10
fonte