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.
fonte
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.Respostas:
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.
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:
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
IncreaseRectangleSizeByTenPercent
mé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: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
SideLength
propriedade e retângulos ter umLength
eWidth
propriedade 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 classificarseal
sua 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.Id
campo do cliente antigo para o novo, cria um novoCustomer
.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 .
fonte
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
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ê optou por modificar a largura e a altura do quadrado ao usarsetWidth
, não está implementando a especificação dos retângulossetWidth
. 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:
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 ecompareTo(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á.
fonte
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:
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.
fonte
IShape
tipo que inclui uma caixa delimitadora e pode ser desenhado, dimensionado e serializado e ter umIPolygon
subtipo com um método para relatar o número de vértices e um método para retornar umIEnumerable<Point>
. Pode-se, então,IQuadrilateral
subtipo que derivaIPolygon
,IRhombus
eIRectangle
, derivam isso, eISquare
derivam deIRhombus
eIRectangle
. 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.IRhombus
garante que todos osPoint
retornados doEnumerable<Point>
definido porIPolygon
correspondem a arestas de comprimentos iguais? Como a implementação daIRhombus
interface por si só não garante que um objeto concreto seja um losango, a herança não pode ser a resposta.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.
fonte
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.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:
Na verdade, você faz isso o tempo todo (às vezes de forma ainda mais implícita) ao usar o OOP.
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.fonte
Rect r = s;
linha, bastadoSomethingWith(s)
e o tempo de execução usará todas as chamadass
para resolver qualquerSquare
método virtual .setWidth
esetHeight
altere a largura e a altura.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:
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:
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.
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.
fonte
Subtipagem é sobre comportamento.
Para que o tipo
B
seja um subtipo do tipoA
, ele deve suportar todas as operações desse tipoA
com 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 exemplo3 - 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 exemplo5 / 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 + 1
fornecerá 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 subtipoB
é seB
comportar comoA
em 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 usamRectangles
e assumem que a implementação segue a especificação. Em seguida, você introduziu um subtipo chamadoSquare
cujos lados não podem ser redimensionados independentemente. Como resultado, a maioria dos programas que redimensionam retângulos agora estará errada.fonte
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.
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.
fonte
Rectangle
são usadas apenas para representar valores , pode ser possível que uma classeSquare
seja herdadaRectangle
e cumpra completamente seu contrato. Infelizmente, muitos idiomas não fazem distinção entre variáveis que encapsulam valores e aquelas que identificam entidades.Square
tipo imutável que herda de umRectnagle
tipo 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 umReadableMatrix
tipo [tipo base] uma matriz retangular que pode ser armazenada de várias maneiras, inclusive esparsamente] e umComputeDeterminant
método. Pode fazer sentidoComputeDeterminant
trabalhar apenas com umReadableSquareMatrix
tipo derivadoReadableMatrix
, do qual eu consideraria um exemplo deSquare
derivado de aRectangle
.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:
width
parâmetro eresize(double factor)
redimensionando a largura pelo fator especificadoheight
e substitui suaresize
função, que chamasuper.resize
e redimensiona a altura pelo fator especificadoDo 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.
fonte
Porque, pelo LSP, a criação de uma relação de herança entre os dois e a substituição
setWidth
esetHeight
garantir que o quadrado tenha o mesmo introduz um comportamento confuso e não intuitivo. Vamos dizer que temos um código:Mas se o método
createRectangle
retornouSquare
, porque é possível graças àSquare
herança deRectange
. 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.
fonte
O que o LSP diz é que qualquer coisa que herda
Rectangle
deve ser aRectangle
. Ou seja, ele deve fazer o queRectangle
faz.Provavelmente, a documentação para
Rectangle
está escrita para dizer que o comportamento de umRectangle
nomeador
é o seguinte: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 herdarRectangle
. 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
Rectangle
tal maneira que isso não implique que o código acima imprima 10; nesse caso, talvez vocêSquare
possa ser umRectangle
. 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.fonte
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/
fonte