Por que Square herdar de Rectangle seria problemático se substituíssemos os métodos SetWidth e SetHeight?


105

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

Eu ouvi pessoas dizerem:

Se você fez o quadrado derivar do retângulo, um quadrado deve ser usado em qualquer lugar em que você espera um retângulo

Qual é o problema aqui? E por que o Square seria utilizável em qualquer lugar que você espera um retângulo? Seria útil apenas se criarmos o objeto Square e substituirmos os métodos SetWidth e SetHeight para Square, por que haveria algum problema?

Se você tivesse os métodos SetWidth e SetHeight em sua classe base Rectangle e se sua referência Rectangle apontasse para um Square, SetWidth e SetHeight não fazem sentido, pois definir um alteraria o outro para corresponder a ele. Nesse caso, o Square falha no teste de substituição de Liskov com retângulo e a abstração de herdar o Square do retângulo é ruim.

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

Eu também ouvi / li:

O problema real é que não estamos modelando retângulos, mas sim "retângulos retocáveis", isto é, retângulos cuja largura ou altura podem ser modificados após a criação (e ainda o consideramos o mesmo objeto). Se olharmos para a classe retângulo dessa maneira, fica claro que um quadrado não é um "retângulo reshapable", porque um quadrado não pode ser remodelado e ainda assim ser um quadrado (em geral). Matematicamente, não vemos o problema porque a mutabilidade nem faz sentido em um contexto matemático

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


15
Esta questão parece muito abstrata. Existem várias maneiras de usar classes e herança, se uma classe é herdada ou não de uma classe é uma boa ideia, geralmente depende principalmente de como você deseja usar essas classes. Sem um caso prático, não vejo como essa pergunta pode obter uma resposta relevante.
Aaaaaaaaaaaa

2
Usando algum senso comum, lembre-se de que quadrado é um retângulo; portanto, se o objeto da sua classe quadrada não puder ser usado onde o retângulo é necessário, provavelmente haverá alguma falha no design do aplicativo.
Cthulhu

7
Eu acho que a melhor pergunta é Why do we even need Square? É como ter duas canetas. Uma caneta azul e uma caneta vermelha azul, amarela ou verde. A caneta azul é redundante - ainda mais no caso do quadrado, pois não tem custo benefício.
Gusdor

2
@eBusiness Sua abstração é o que a torna uma boa pergunta de aprendizado. É importante poder reconhecer quais usos de subtipagem são ruins, independentemente de casos de uso específicos.
Doval

5
@Cthulhu Na verdade não. Subtipagem é tudo sobre comportamento e um quadrado mutável não se comporta como um retângulo mutável. É por isso que a metáfora "é um ..." é ruim.
Doval

Respostas:


189

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

Considere o seguinte problema:

Recebi um grupo de retângulos e quero aumentar a área deles em 10%. Então, o que 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 realmente me passou uma mistura de quadrados e retângulos, e quando o comprimento do retângulo foi alterado, o mesmo ocorreu com a largura.

Meus testes de unidade são aprovados porque escrevi todos os meus testes de unidade para usar uma coleção de retângulos. 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 outro código que usa o fato de que, se ele passar quadrados ao meu método, ele obtém um aumento de tamanho de 21% muito bom. Jim está feliz e ninguém é mais sábio.

Jim é promovido por um excelente trabalho a uma divisão diferente. Alfred se junta à empresa como um 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 deseja que o bug seja corrigido. Alfred vê que Squares e retângulos são usados ​​em todo lugar no código e percebe que é impossível quebrar a cadeia de herança. Ele também não tem acesso ao código-fonte da Contabilidade. Então, Alfred corrige o erro da seguinte maneira:

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 hackers e Jill assina que o bug foi corrigido.

No próximo mês, ninguém é pago porque a Contabilidade dependia de poder passar quadrados para o IncreaseRectangleSizeByTenPercentmétodo e obter um aumento na área de 21%. A empresa inteira entra no modo "correção de bug de prioridade 1" para rastrear a origem do problema. Eles rastreiam o problema até a correção de Alfred. Eles sabem que precisam manter a contabilidade e a publicidade felizes. Portanto, eles corrigem o problema identificando o usuário com a chamada de método da seguinte maneira:

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.

Essa anedota é baseada em situações do mundo real que enfrentam os programadores diariamente. Violações do princípio da substituição de Liskov podem introduzir bugs muito sutis que só são detectados anos depois de serem escritos, quando a correção da violação quebrará um monte de coisas e não a fixação , irritará o seu maior cliente.

Existem duas maneiras realistas de corrigir esse problema.

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

A segunda maneira é quebrar a cadeia de herança entre quadrados e retângulos. Se um quadrado é definido como tendo uma única SideLengthpropriedade e retângulos ter um Lengthe Widthpropriedade e não há herança, é impossível quebrar acidentalmente coisas, esperando que um retângulo e obter um quadrado. Em termos de C #, você pode classificar sealsua classe de retângulo, o que garante que todos os retângulos que você obtém sejam retângulos.

Nesse caso, gosto da maneira "objetos imutáveis" de corrigir o problema. A identidade de um retângulo é seu comprimento e largura. Faz sentido que, quando você deseja alterar a identidade de um objeto, o que você realmente deseja é um novo objeto. Se você perde um cliente antigo e ganha um novo cliente, não altera o Customer.Idcampo do cliente antigo para o novo, cria um novo Customer.

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


7
Liskov é uma coisa, e armazenamento é outra questão. Na maioria das implementações, uma instância Square herdada de Rectangle exige espaço para armazenar duas dimensões, mesmo que apenas uma seja necessária.
7898 el.pescado

29
Uso brilhante de uma história para ilustrar o ponto
Rory Hunter

29
Bela história, mas eu não concordo. O caso de uso foi: altere a área de um retângulo. A correção deve ser adicionar um método substituível 'ChangeArea' ao retângulo especializado em Square. Isso não quebraria a cadeia de herança, tornaria explícito o que o usuário queria fazer e não causaria o bug introduzido pela correção mencionada (que seria capturada em uma área de armazenamento temporário adequada).
Roy T.

33
@RoyT .: Por que um retângulo sabe como definir sua área? Essa é uma propriedade derivada inteiramente do comprimento e largura. E, mais precisamente, qual dimensão ela deve mudar - o comprimento, a largura ou ambas?
cHao

32
@ Roy T. É muito bom dizer que você teria resolvido o problema de maneira diferente, mas o fato é que este é um exemplo - embora simplificado - de situações do mundo real que os desenvolvedores enfrentam diariamente quando mantêm produtos herdados. E mesmo se você implementou esse método, isso não impedirá que os herdeiros violem o LSP e introduzam erros semelhantes a este. É por isso que praticamente todas as classes no .NET framework são seladas.
7304 Stephen

30

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 todos os invariantes da sua classe Rectangle, mas nem todos os invariantes quadrados - 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 inserir .

Se o seu retângulo tiver um método setWidthdocumentado 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ê optou por modificar a largura e a altura do quadrado ao usar setWidth, não está implementando a especificação dos retângulos setWidth. Você simplesmente não pode vencer.

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

É uma questão de co-variação versus contra-variação.

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

  • Retorne apenas valores que a superclasse retornaria - ou seja, 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 - ou seja, os tipos de argumento devem ser supertipos dos tipos de argumento do método da superclasse. Os argumentos são contra-variantes.

Então, voltando ao retângulo e ao quadrado: se o quadrado pode ser uma subclasse de retângulo depende inteiramente de quais métodos o retângulo possui.

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

Da mesma forma, se você faz com que alguns métodos sejam co-variantes nos argumentos, como compareTo(Rectangle)em Retângulo e compareTo(Square)Quadrado, você terá um problema ao usar um Quadrado como Retângulo.

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


"Se todos os seus objetos são imutáveis, não há nenhum problema" - esta é a afirmação aparentemente irrelevante no contexto desta questão, que menciona explicitamente setters para a largura e altura
mosquito

11
Achei isso interessante, mesmo quando é "aparentemente irrelevante"
Jesvin Jose

14
@gnat Eu diria que é relevante porque o valor real da pergunta é reconhecer quando há uma relação de subtipagem válida entre dois tipos. Isso depende de quais operações o supertipo declara; portanto, vale ressaltar que o problema desaparece se os métodos do mutador desaparecerem.
Doval

1
@gnat também, setters são mutadores , então o lrn está basicamente dizendo: "Não faça isso e isso não é um problema". Por acaso, concordo com a imutabilidade para tipos simples, mas você faz uma boa observação: para objetos complexos, o problema não é tão simples.
Patrick M

1
Considere desta forma, qual é o comportamento garantido pela classe 'Rectangle'? Que você pode alterar a largura e a altura INDEPENDENTES um do outro. (ou seja, setWidth e setHeight). Agora, se o Square for derivado do Retângulo, o Square precisará garantir esse comportamento. Como o square não pode garantir esse comportamento, é uma herança ruim. No entanto, se os métodos setWidth / setHeight forem removidos da classe Rectangle, esse comportamento não existe e, portanto, você poderá derivar a classe Square do Rectangle.
Nitin Bhide

17

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

Pensei em 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.

Há um problema adicional com o quadrado é um tipo especial de retângulo que raramente é mencionado, e é o seguinte: por que estamos parando com quadrados e retângulos ? Se estivermos dispostos a dizer que um quadrado é um tipo especial de retângulo, certamente também deveríamos 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, quadrado e paralelogramo são todos um tipo especial de trapézio - são trapézios com dois conjuntos de lados paralelos
  • Todos os itens acima são tipos especiais de quadriláteros
  • Todos os itens acima são tipos especiais de formas planas
  • E assim por diante; Eu poderia continuar por algum tempo aqui.

O que diabos todos os relacionamentos deveriam ter aqui? Linguagens baseadas em herança de classe 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 completamente, não tentando representar todas essas coisas como classes com relacionamentos de subtipagem.


3
Se objetos de formas são imutáveis, pode-se ter um IShapetipo que inclui uma caixa delimitadora e pode ser desenhado, dimensionado e serializado e ter um IPolygonsubtipo com um método para relatar o número de vértices e um método para retornar um IEnumerable<Point>. Pode-se, então, IQuadrilateralsubtipo que deriva IPolygon, IRhombuse IRectangle, derivam isso, e ISquarederivam de IRhombuse IRectangle. A mutabilidade jogaria tudo pela janela e a herança múltipla não funciona com classes, mas acho que é bom com interfaces imutáveis.
Supercat

Eu discordo efetivamente de Eric aqui (embora não seja suficiente para -1!). Todos esses relacionamentos são (possivelmente) relevantes, como o @supercat menciona; é apenas uma questão do YAGNI: você não o implementa até precisar.
Mark Hurd

Resposta muito boa! Deve ser muito mais alto.
precisa saber é o seguinte

1
@ MarkHurd - não é um problema do YAGNI: a hierarquia baseada em herança proposta tem a forma da taxonomia descrita, mas não pode ser escrita para garantir os relacionamentos que a definem. Como IRhombusgarante que todos os Pointretornados do Enumerable<Point>definido por IPolygoncorrespondem a arestas de comprimentos iguais? Como a implementação da IRhombusinterface por si só não garante que um objeto concreto seja um losango, a herança não pode ser a resposta.
A. Rager

14

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

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

O fator chave aqui é a mutabilidade . Uma forma pode mudar uma vez construída?

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

  • Imutável: se as formas não podem mudar depois de construídas, um objeto quadrado também deve sempre cumprir o contrato do retângulo. Um quadrado pode ter um relacionamento é-um com retângulo.

Nos dois casos, é possível solicitar que um quadrado produza uma nova forma com base em seu estado com uma ou mais alterações. Por exemplo, alguém poderia dizer "crie um novo retângulo com base nesse quadrado, exceto que os lados opostos A e C têm o dobro do comprimento". Como um novo objeto está sendo construído, o quadrado original continua aderindo aos seus contratos.


1
This is one of those cases where the real world is not able to be modeled in a computer 100%. Por quê então? Ainda podemos ter um modelo funcional de um quadrado e um retângulo. A única consequência é que precisamos procurar uma construção mais simples para abstrair sobre esses dois objetos.
Simon Bergot

6
Há mais em comum entre retângulos e quadrados do que isso. O problema é que a identidade de um retângulo e a identidade de um quadrado são seus comprimentos laterais (e o ângulo em cada interseção). A melhor solução aqui é fazer os quadrados herdarem dos retângulos, mas tornar os dois imutáveis.
7774 Stephen

3
@ Stephen Concordou. Na verdade, torná-los imutáveis ​​é a coisa mais sensata a se fazer, independentemente dos problemas de subtipagem. Não há razão para torná-los mutáveis ​​- não é mais difícil construir um novo quadrado ou retângulo do que transformá-lo, então por que abrir essa lata de minhocas? Agora você não precisa se preocupar com alias / efeitos colaterais e pode usá-los como chaves para mapas / dict, se necessário. Alguns dirão "desempenho", ao qual direi "otimização prematura" até que eles realmente medam e provem que o hot spot está no código de forma.
Doval

Desculpe pessoal, já era tarde e eu estava super cansada quando escrevi a resposta. Eu o reescrevi para dizer o que realmente queria dizer, cujo ponto crucial é a mutabilidade.

13

E por que o Square seria utilizável em qualquer lugar que você espera um retângulo?

Porque isso faz parte do que significa ser um subtipo (veja também: princípio de 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

Na verdade, você faz isso o tempo todo (às vezes de forma ainda mais implícita) ao usar o OOP.

e se substituirmos os métodos SetWidth e SetHeight para Square, por que haveria algum problema?

Porque você não pode substituí-los sensatamente 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 é apenas redimensionável, mas redimensionável em ambas as dimensões independentemente.


Em muitos idiomas, você nem precisa da Rect r = s;linha, basta doSomethingWith(s)e o tempo de execução usará todas as chamadas spara resolver qualquer Squaremétodo virtual .
Patrick M

1
@PatrickM Você não precisa dele em nenhum idioma que possua subtipagem. Eu incluí essa linha para exposição, para ser explícita.

Substitua setWidthe setHeightaltere a largura e a altura.
ApproachingDarknessFish

@ValekHalfHeart Essa é precisamente a opção que estou considerando.

7
@ValekHalfHeart: Essa é precisamente a violação do Princípio da Substituição de Liskov que o assombrará e fará você passar várias noites sem dormir tentando encontrar um bug estranho dois anos depois, quando você esqueceu como o código deveria funcionar.
Jan Hudec

9

O que você está descrevendo está em conflito com o chamado 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 específica, você sempre poderá trocar em uma instância de qualquer subclasse dessa classe, sem introduzir bugs.

O problema do retângulo quadrado não é realmente uma maneira muito boa de apresentar Liskov. Ele tenta explicar um princípio amplo usando um exemplo que é realmente bastante sutil e colide com uma das definições intuitivas mais comuns em toda a matemática. Alguns o chamam de problema do Ellipse-Circle 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 de paralelogramo-retângulo. Isso torna as coisas muito mais fáceis de entender.

Um paralelogramo é um quadrilátero com dois pares de lados paralelos. Ele também tem dois pares de ângulos congruentes. Não é difícil imaginar um objeto Parallelogram nessas 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 parecer fazer do Rectangle um bom candidato para herdar do Parallelogram , para que você possa reutilizar todo esse código gostoso. Contudo:

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 erros no retângulo? 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 na verdade para o retângulo herdado do paralelogramo. Se eu trocar um retângulo por um código que espera um paralelogramo, e esse código tentar alterar o ângulo, certamente haverá erros. Pegamos algo gravável na subclasse e o tornamos somente leitura, e isso é uma violação de Liskov.

Agora, como isso se aplica a quadrados e retângulos?

Quando dizemos que você pode definir um valor, geralmente queremos dizer algo um pouco mais forte do que apenas poder escrever um valor nele. Implicamos um certo grau de exclusividade: se você definir um valor, exceto algumas circunstâncias extraordinárias, ele permanecerá nesse valor até que você o defina novamente. Existem muitos usos para valores que podem ser gravados, mas não permanecem definidos, mas também existem muitos casos que dependem de um valor que permanece onde está quando você o define. E é aí que encontramos 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 possui alguns novos. O problema com setSideA e setSideB é que nenhum deles é realmente mais configurável: você ainda pode escrever um valor em um deles, mas ele mudará de você se o outro for gravado. Se eu trocar isso por um paralelogramo no código que depende de poder definir lados independentemente um do outro, isso vai surtar.

Esse é o problema e é por isso que há um problema ao usar o Rectangle-Square como uma introdução ao Liskov. O retângulo-quadrado depende da diferença entre ser capaz de gravar em algo e ser capaz de defini- lo, e essa é uma diferença muito mais sutil do que ser capaz de definir algo versus permitir que seja somente leitura. O Retângulo-Quadrado ainda tem valor como exemplo, porque documenta uma pegadinha bastante comum que deve ser observada, mas não deve ser usada como um exemplo introdutório . Deixe o aluno se familiarizar com o básico primeiro e depois jogue algo mais duro neles.


8

Subtipagem é sobre comportamento.

Para que o tipo Bseja um subtipo do tipo A, ele deve suportar todas as operações desse tipo Acom a mesma semântica (conversa sofisticada para "comportamento"). Usando a lógica que cada B é um A se não trabalhar - compatibilidade comportamento tem a palavra final. Na maioria das vezes, "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 língua, podemos esperar que eles para apoiar as operações +, -, *, e /. Agora considere o conjunto de números inteiros positivos ({1, 2, 3, ...}). Claramente, todo número inteiro positivo também é um número real. Mas o tipo de números 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 números inteiros positivos sem problemas.
  • -: Nem todas as subtrações de números inteiros positivos resultam em números inteiros positivos. Por exemplo 3 - 5.
  • *: Podemos multiplicar números inteiros positivos sem problemas.
  • /: Nem sempre podemos dividir números inteiros positivos e obter um número inteiro positivo. Por exemplo 5 / 3.

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

Obviamente, você pode definir +entradas de 32 bits para que o resultado seja um número 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 necessidades de memória.

Por que isso importa?

É importante que os programas estejam corretos. É sem dúvida 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á correto para algum subtipo Bé se Bcomportar como Aem todos os aspectos.

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 Rectanglese assumem que a implementação segue a especificação. Em seguida, você introduziu um subtipo chamado Squarecujos lados não podem ser redimensionados independentemente. Como resultado, a maioria dos programas que redimensionam retângulos agora estará errada.


6

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

Para começar, 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 lados com ângulos de 90 graus e um quadrado cumpre todas essas propriedades. Então, um quadrado não é um retângulo?

O fato é que tudo depende de quais são seus critérios iniciais para agrupar objetos, em que contexto você está vendo esses objetos. Na 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ê deve se perguntar: é com base nos critérios que me interessam .

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

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

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

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

Seria útil apenas se criarmos o objeto Square e substituirmos os métodos SetWidth e SetHeight para Square, por que haveria algum problema?

Se você fizer isso, embora esteja 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 em que você se importa, porque acabou de definir dois comportamentos diferentes. Então, por que fingir que são iguais se são semelhantes em um contexto que você não se importa?

Isso destaca um problema significativo quando os desenvolvedores chegam a um domínio que desejam modelar. É muito importante esclarecer em que 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 preocupavam com as propriedades compartilhadas das linhas e dos anjos das formas, e as agrupavam com base nelas. Isso não significa que você é forçado a continuar esse agrupamento se não for o que lhe interessa (em 99% do tempo modelando em software com o qual você não se importa).

Muitas das respostas a essa pergunta se concentram na sub-digitação, sobre comportamento de agrupamento , porque são 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, é com isso que realmente se importa. Você não se importa se um quadrado e um retângulo compartilham os mesmos anjos internos. Você se importa 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 focado em alterar o sistema com base nas regras de comportamento dos objetos.


Se variáveis ​​do tipo Rectanglesão usadas apenas para representar valores , pode ser possível que uma classe Squareseja herdada Rectanglee cumpra completamente seu contrato. Infelizmente, muitos idiomas não fazem distinção entre variáveis ​​que encapsulam valores e aquelas que identificam entidades.
Supercat

Possivelmente, mas então por que se preocupar em primeiro lugar. O ponto do problema do retângulo / quadrado não é tentar descobrir como fazer o relacionamento "um quadrado é um retângulo" funcionar, mas sim perceber que o relacionamento realmente não existe no contexto em que você está usando os objetos. (comportamental) e como um aviso sobre não impor relações irrelevantes ao seu domínio.
Cormac Mulhall

Ou, colocando de outra forma: não tente dobrar a colher. Isso é impossível. Em vez disso, apenas tente perceber a verdade, que não há colher. :-)
Cormac Mulhall

1
Ter um Squaretipo imutável que herda de um Rectnagletipo imutável poderia ser útil se houvesse alguns tipos de operações que só poderiam ser realizadas em quadrados. Como um exemplo realista do conceito, considere um ReadableMatrixtipo [tipo base] uma matriz retangular que pode ser armazenada de várias maneiras, inclusive esparsamente] e um ComputeDeterminantmétodo. Pode fazer sentido ComputeDeterminanttrabalhar apenas com um ReadableSquareMatrixtipo derivado ReadableMatrix, do qual eu consideraria um exemplo de Squarederivado de a Rectangle.
Supercat

5

Se um quadrado é um tipo de retângulo, por que um quadrado não pode herdar de um retângulo?

O problema reside em pensar que, se as coisas estão relacionadas de alguma forma na realidade, elas devem ser relacionadas exatamente da mesma maneira após a 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 filho.

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

  • defina Square como classe base, com widthparâmetro e resize(double factor)redimensionando a largura pelo fator especificado
  • define a classe Rectangle e a subclasse de Square, porque adiciona outro atributo heighte substitui sua resizefunção, que chama super.resizee redimensiona a altura pelo fator especificado

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


+1 Só porque um quadrado é um tipo especial de retângulo em matemática, não significa que é o mesmo em OO.
Lovis

1
Um quadrado é um quadrado e um retângulo é um retângulo. Os relacionamentos entre eles também devem se aplicar à modelagem, ou você tem um modelo bastante ruim. Os problemas reais são: 1) se você os torna mutáveis, não está mais modelando quadrados e retângulos; 2) assumindo que, apenas porque um relacionamento "é um" é válido entre dois tipos de objetos, você pode substituir um pelo outro indiscriminadamente.
Doval

4

Porque, pelo LSP, a criação de uma relação de herança entre os dois e a substituição setWidthe setHeightgarantir que o quadrado tenha o mesmo 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 createRectangleretornou Square, porque é possível graças à Squareherança de Rectange. Então as expectativas se quebram. Aqui, com esse código, esperamos que a configuração de largura ou altura só cause alterações na largura ou altura, respectivamente. O ponto da OOP é que, quando você trabalha com a superclasse, não tem conhecimento de nenhuma subclasse sob ela. E se a subclasse muda o comportamento, de modo a ir contra as expectativas que temos sobre a superclasse, há uma grande chance de que erros ocorram. E esse tipo de erro é difícil de depurar e corrigir.

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


2

O que o LSP diz é que qualquer coisa que herda Rectangledeve ser a Rectangle. Ou seja, ele deve fazer o que Rectanglefaz.

Provavelmente, a documentação para Rectangleestá escrita para dizer que o comportamento de um Rectanglenomeado ré o seguinte:

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

Se o seu quadrado não tiver o mesmo comportamento, ele não se comportará como um Rectangle. Portanto, o LSP diz que não deve herdar Rectangle. A linguagem não pode impor essa regra, porque não pode impedir que você faça 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 isso!

Agora, seria possível escrever a documentação de Rectangletal maneira que isso não implique que o código acima imprima 10; nesse caso, talvez você Squarepossa ser um Rectangle. Você pode ver uma documentação que diz algo como "isso faz X. Além disso, a implementação nesta classe faz Y". Nesse caso, você pode 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", eles estão basicamente assumindo que o exposto acima é realmente parte da definição razoável de um retângulo mutável.


este parece apenas repetir pontos explicou em uma resposta postada 5 horas atrás
mosquito

@gnat: você prefere que eu edite essa outra resposta até aproximadamente essa brevidade? ;-) Eu não acho que posso sem remover pontos que outro respondente presumivelmente considera necessários para responder à pergunta e acho que não.
Steve Jessop


1

Os subtipos e, por extensão, a programação OO, geralmente dependem do Princípio de Substituição de Liskov, de que qualquer valor do tipo A pode ser usado onde um B é necessário, se A <= B. Esse é um axioma na arquitetura OO, ie. supõe-se que todas as subclasses terão essa propriedade (e, caso contrário, os subtipos são de bugs e precisam ser corrigidos).

No entanto, verifica-se que esse 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 do retângulo quadrado ou o problema da elipse circular ( http://en.wikipedia.org/wiki/Circle-ellipse_problem ), é um exemplo famoso de quão difícil é resolver .

Observe que poderíamos implementar quadrados e retângulos cada vez mais equivalentes à observação, mas apenas descartando cada vez mais funcionalidade até que a distinção seja inútil.

Como exemplo, consulte http://okmij.org/ftp/Computation/Subtyping/

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.