O problema da elipse circular pode ser resolvido revertendo o relacionamento?

13

A Circleextensão deEllipse interrupções quebra o Princípio da Substituição de Liskov , porque modifica uma pós-condição: ou seja, você pode definir X e Y independentemente para desenhar uma elipse, mas X sempre deve ser igual a Y para círculos.

Mas o problema aqui não é causado pelo fato de o Circle ser o subtipo de uma elipse? Não poderíamos reverter o relacionamento?

Portanto, Circle é o supertipo - ele tem um método único setRadius.

Em seguida, o Ellipse estende o Círculo adicionando setXe setY. A chamada setRadiusdo Ellipse definiria X e Y - o que significa que a pós-condição no setRadius é mantida, mas agora você pode definir X e Y independentemente por meio de uma interface estendida.

HorusKol
fonte
1
Você consultou a Wikipedia primeiro ( en.wikipedia.org/wiki/Circle-ellipse_problem )?
Doc Brown
1
sim - eu mesmo vinculá-lo na minha pergunta ...
HorusKol
6
E esse ponto exato é abordado nesse artigo, então não sei ao certo o que você está perguntando?
Philip Kendall
6
"Alguns autores sugeriram reverter a relação entre círculo e elipse, alegando que uma elipse é um círculo com recursos adicionais. Infelizmente, as elipses falham em satisfazer muitos dos invariantes dos círculos; se o Círculo tem um raio de método, a Ellipse agora terá para fornecê-lo também ".
Philip Kendall
3
O que eu achei ser a explicação mais clara sobre por que esse problema tem premissas ruins está na parte inferior do artigo da wikipedia: en.wikipedia.org/wiki/… . Dependendo da situação, há vários designs limpos, mas depende do que você precisa dessas duas classes para fazer , para não ser .
Arthur Havlicek

Respostas:

37

Mas o problema aqui não é causado pelo fato de o Circle ser o subtipo de uma elipse? Não poderíamos reverter o relacionamento?

O problema com isso (e o problema do quadrado / retângulo) está assumindo falsamente que um relacionamento em um domínio (geometria) se mantém em outro (comportamento)

Um círculo e elipse estão relacionados se você os estiver visualizando através do prisma da teoria geométrica. Mas esse não é o único domínio que você pode olhar.

O design orientado a objetos lida com o comportamento .

A característica definidora de um objeto é o comportamento pelo qual o objeto é responsável. E no domínio do comportamento, um círculo e uma elipse têm um comportamento tão diferente que provavelmente é melhor não pensar neles como relacionados. Nesse domínio, uma elipse e um círculo não têm relacionamento significativo.

A lição aqui é escolher o domínio que faz mais sentido para OOD, não tentar calçar um relacionamento simplesmente porque ele existe em um domínio diferente.

O exemplo mais comum desse erro no mundo real é assumir que os objetos estão relacionados (ou até a mesma classe) porque eles têm dados semelhantes, mesmo que seu comportamento seja muito diferente. Esse é um problema comum quando você começa a construir objetos "dados primeiro", definindo para onde vão os dados. Você pode acabar com uma classe relacionada a dados com comportamento completamente diferente. Por exemplo, os objetos de salário e de funcionário podem ter um atributo "salário bruto", mas um funcionário não é um tipo de salário e um salário não é um tipo de empregado.

Cormac Mulhall
fonte
Separar as preocupações do domínio (aplicativo) versus os recursos comportamentais e de responsabilidade do OOD é um ponto muito importante. Por exemplo, em um aplicativo de desenho, talvez você deva transformar um círculo em um quadrado, mas isso não é facilmente modelado usando classes / objetos na maioria dos idiomas (como objetos geralmente não podem mudar de classe). Portanto, o domínio do aplicativo nem sempre é mapeado adequadamente para a hierarquia de herança de uma determinada linguagem OOP e não devemos forçá-lo; em muitos casos, a composição é melhor.
precisa saber é o seguinte
3
Essa resposta é de longe a melhor coisa que eu já vi sobre toda a questão e como o potencial de erros de design pode surgir em casos mais gerais. Obrigado
HorusKol 04/04
1
@ErikEidt O problema de um comportamento de mudança de objeto pode ser resolvido no OOD através da decomposição. Por exemplo, se uma forma transformável se transformar em um círculo, você não precisará alterar a classe. Em vez disso, a classe pega um objeto de comportamento geométrico atual que você pode trocar por outro comportamento quando se transformar. Essa outra classe contém as regras da forma geométrica atualmente sendo modelada e a classe de forma morfável adia a essa classe o comportamento geométrico. Se o objeto se transformar em uma classe diferente, você alterará a classe de comportamento para outra coisa.
Cormac Mulhall
2
@Cormac, certo! Genericamente, eu chamaria isso de uma forma de composição, como mencionei, embora você possa identificar, mais especificamente, um padrão de estratégia ou algo assim. Em essência, você tem uma identidade que não se transforma e outras coisas que podem ser alteradas. No geral, um bom destaque da diferença entre os conceitos de domínio de aplicativo e os detalhes do POO de uma determinada linguagem e a necessidade de mapear entre eles (ou seja, arquitetura, design e programação).
precisa saber é o seguinte
1
Mas um emprego pode ser um salário.
8

Os círculos são um caso especial de elipse, a saber, que os dois eixos da elipse são iguais. É fundamentalmente falso no domínio do problema (geometria) afirmar que as elipses podem ser um tipo de círculo. O uso desse modelo defeituoso violaria muitas garantias de um círculo, por exemplo "todos os pontos do círculo têm a mesma distância do centro". Isso também seria uma violação do Princípio de Substituição de Liskov. Como uma elipse teria um único raio? (Não é setRadius()mais importante getRadius())

Embora a modelagem de círculos como um subtipo de elipses não esteja fundamentalmente errada, é a introdução da mutabilidade que quebra esse modelo. Sem os métodos setX()e setY(), não há violação do LSP. Se for necessário ter um objeto com dimensões diferentes, criar uma nova instância é uma solução melhor:

class Ellipse {
  final double x;
  final double y;
  ...
  Ellipse withX(double newX) {
    return new Ellipse(x: newX, y: y);
  }
}
amon
fonte
1
ok - então, se houvesse alguma interface comum entre Ellipsee Circle(como getArea) que seria abstraída para um tipo Shape- poderia Ellipsee Circleseparadamente subtipo Shapee satisfazia o LSP?
HorusKol
1
@HorusKol Sim. Duas classes que herdam uma interface que elas realmente implementam corretamente estão completamente bem.
Ixrec # 4/16
6

É um erro desde o início insistir em ter uma classe "Elipse" e "Círculo", onde uma é uma subclasse da outra. Você tem duas opções realistas: uma é ter aulas separadas. Eles podem ter uma superclasse comum, para coisas como cor, se o objeto está preenchido, largura da linha para desenhar etc.

A outra é ter apenas uma classe chamada "Ellipse". Se você tem essa classe, é fácil usá-la para representar círculos (pode haver armadilhas dependendo dos detalhes da implementação; um Ellipse terá algum ângulo e o cálculo desse ângulo não deve causar problemas para uma elipse em forma de círculo). Você poderia até ter métodos especializados para elipses circulares, mas essas "elipses circulares" ainda seriam objetos completos de "Elipse".

gnasher729
fonte
Poderia haver um método IsCircle que verificaria se um objeto específico da classe Ellipse realmente tem os dois eixos iguais. Você apontou a questão do ângulo também. Os círculos não podem ser 'rotacionados'.
6

Cormac tem uma resposta realmente ótima, mas eu só quero elaborar um pouco sobre o motivo da confusão em primeiro lugar.

A herança em OO geralmente é ensinada usando metáforas do mundo real, como "maçãs e laranjas são subclasses de frutas". Infelizmente, isso leva à crença equivocada de que os tipos de OO devem ser modelados de acordo com algumas hierarquias taxonômicas existentes independentemente do programa.

Porém, no design de software, os tipos devem ser modelados de acordo com os requisitos do aplicativo. Classificações em outros domínios são geralmente irrelevantes. Em uma aplicação real com objetos "Apple" e "Orange" - digamos, um sistema de gerenciamento de inventário para um supermercado - eles provavelmente não serão classes distintas, e categorias como "Frutas" serão atributos e não supertipos.

O problema da elipse circular é um arenque vermelho. Na geometria, um círculo é uma especialização de uma elipse, mas as classes no seu exemplo não são figuras geométricas. Fundamentalmente, figuras geométricas não são mutáveis. Eles podem ser transformados , mas um círculo pode ser transformado em uma elipse. Portanto, um modelo em que os círculos podem alterar o raio, mas não as reticências, não corresponde à geometria. Esse modelo pode fazer sentido em um aplicativo específico (por exemplo, uma ferramenta de desenho), mas a classificação geométrica é irrelevante para a maneira como você projeta a hierarquia de classes.

Então, Circle deve ser uma subclasse de Ellipse ou vice-versa? Depende totalmente dos requisitos do aplicativo em particular que utiliza esses objetos. Um aplicativo de desenho pode ter diferentes opções em como tratar círculos e elipses:

  1. Trate círculos e elipses como tipos distintos de formas com diferentes interfaces de usuário (por exemplo, duas alças de redimensionamento em uma elipse, uma alça em um círculo). Isso significa que você pode ter uma elipse que é geometricamente um círculo, mas não um círculo da perspectiva do aplicativo.

  2. Trate todas as elipses, incluindo círculos da mesma forma, mas tenha a opção de "travar" x e y com o mesmo valor.

  3. Elipses são apenas círculos nos quais uma transformação de escala foi aplicada.

Cada design possível levará a um modelo de objeto diferente -

No 1º caso, Circle e Ellipses serão irmãos classes

No segundo, não haverá nenhuma classe Circle distinta

No terceiro, não haverá uma classe Ellipse distinta. Portanto, o chamado problema da elipse circular não aparece na imagem em nenhum deles.

Então, para responder à pergunta da seguinte maneira: O círculo deve estender a elipse? A resposta é: depende do que você quer fazer com isso. Mas provavelmente não.

JacquesB
fonte
1
Uma resposta muito boa!
precisa
3

Seguindo os pontos do LSP, uma solução 'adequada' para esse problema é que @HorusKol e @Ixrec surgiram - derivando os dois tipos do Shape. Mas depende do modelo do qual você está trabalhando, portanto, você deve sempre voltar a isso.

O que eu fui ensinado é:

Se o subtipo não puder executar o mesmo comportamento que o supertipo, o relacionamento não se mantém na premissa do IS-A - ele deve ser alterado.

  • Um subtipo é um SUPERSET do supertipo.
  • Um supertipo é um SUBSET do subtipo.

Em inglês:

  • Um tipo derivado é um SUPERSET do tipo base.
  • Um tipo de base é um SUBSET do tipo derivado.

(Exemplo:

  • Um carro com escapamento de bad boy ainda é um carro (de acordo com alguns).
  • Um carro sem motor, rodas, rack de direção, trem de força e apenas a concha restante não é um 'carro', é apenas uma concha.)

É assim que a classificação funciona (ou seja, no mundo animal) e, principalmente, no OO.

Usando isso como a definição de herança e polimorfismo (que sempre são escritos juntos), se esse princípio for quebrado, você deve tentar repensar os tipos que está tentando modelar.

Conforme mencionado por @HorusKul e @Ixrec, em matemática você definiu claramente os tipos. Mas, em matemática, um círculo é uma elipse porque é um SUBSET da elipse. Mas no POO não é assim que a herança funciona. Uma classe só deve herdar se for um SUPERSET (uma extensão) de uma classe existente - ou seja, ainda é a classe base em todos os contextos.

Com base nisso, acho que a solução deve ser um pouco reformulada.

Tenha um tipo de base Shape e RoundedShape (efetivamente um círculo, mas eu usei um nome diferente aqui DELIBERATELY ...)

... então Ellipse.

Dessa maneira:

  • RoundedShape é uma forma.
  • A elipse é uma RoundedShape.

(Isso agora faz sentido para as pessoas na linguagem. Já temos um conceito claramente definido de 'círculo' em nossas mentes, e o que estamos tentando fazer aqui ao generalizar (agregar) quebra esse conceito.)

Andy Good
fonte
Nossos conceitos claramente definidos nem sempre funcionam na prática.
-1

De uma perspectiva OO, a elipse estende o círculo, especializando-se adicionando algumas propriedades. As propriedades existentes do círculo ainda se mantêm na elipse, apenas se tornam mais complexas e mais específicas. Não vejo nenhum problema com o comportamento nesse caso, como o Cormac faz, as formas não têm comportamento. O único problema é que, no sentido liguístico ou matemático, não parece correto dizer "uma elipse É UM círculo". Porque todo o objetivo do exercício que não é mencionado, mas está implícito, era classificar formas geométricas. Essa pode ser uma boa razão para considerar círculo e elipse como pares, não vinculá-los por herança e aceitar que eles possuem algumas das mesmas propriedades e NÃO deixar sua mente distorcida de OO seguir essa observação.

Martin Maat
fonte