¿Se puede resolver el problema círculo-elipse invirtiendo la relación?

13

Tener CircleextendidoEllipse rompe el Principio de Substición de Liskov , porque modifica una condición posterior: es decir, puede establecer X e Y independientemente para dibujar una elipse, pero X siempre debe ser igual a Y para los círculos.

¿Pero no es el problema aquí causado por hacer que Circle sea el subtipo de una Elipse? ¿No podríamos revertir la relación?

Entonces, Circle es el supertipo: tiene un método único setRadius.

Luego, Ellipse extiende Circle sumando setXy setY. Llamar setRadiusa Ellipse establecería X e Y, lo que significa que se mantiene la condición posterior en setRadius, pero ahora puede configurar X e Y de forma independiente a través de una interfaz extendida.

HorusKol
fuente
1
¿Buscó primero en Wikipedia ( en.wikipedia.org/wiki/Circle-ellipse_problem )?
Doc Brown
1
sí, incluso lo vinculo en mi pregunta ...
HorusKol
66
Y este punto exacto está cubierto en ese artículo, así que no tengo claro lo que estás preguntando.
Philip Kendall
66
"Algunos autores han sugerido invertir la relación entre círculo y elipse, con el argumento de que una elipse es un círculo con capacidades adicionales. Desafortunadamente, las elipses no satisfacen a muchos de los invariantes de los círculos; si Circle tiene un radio de método, Ellipse ahora tendrá para proporcionarlo también ".
Philip Kendall
3
Lo que encontré como la explicación más clara sobre por qué este problema tiene malas premisas se encuentra en la parte inferior del artículo de wikipedia: en.wikipedia.org/wiki/… . Dependiendo de la situación, hay varios diseños limpios, pero depende de lo que necesita de estas dos clases para hacer , no ser .
Arthur Havlicek

Respuestas:

37

¿Pero no es el problema aquí causado por hacer que Circle sea el subtipo de una Elipse? ¿No podríamos revertir la relación?

El problema con esto (y el problema del cuadrado / rectángulo) es asumir falsamente que una relación en un dominio (geometría) se cumple en otro (comportamiento)

Un círculo y una elipse están relacionados si los está viendo a través del prisma de la teoría geométrica. Pero ese no es el único dominio que puedes mirar.

El diseño orientado a objetos se ocupa del comportamiento .

La característica definitoria de un objeto es el comportamiento del que es responsable el objeto. Y en el dominio del comportamiento, un círculo y una elipse tienen un comportamiento tan diferente que probablemente sea mejor no pensar en ellos como relacionados. En este dominio, una elipse y un círculo no tienen una relación significativa.

La lección aquí es elegir el dominio que tenga más sentido para OOD, no tratar de calzar una relación simplemente porque existe en un dominio diferente.

El ejemplo más común en el mundo real de este error es asumir que los objetos están relacionados (o incluso la misma clase) porque tienen datos similares , incluso si su comportamiento es muy diferente. Este es un problema común cuando comienza a construir objetos "datos primero" definiendo a dónde van los datos. Puede terminar con una clase que está relacionada a través de datos que tienen un comportamiento completamente diferente. Por ejemplo, tanto el recibo de sueldo como los objetos de empleado pueden tener un atributo de "salario bruto", pero un empleado no es un tipo de recibo de sueldo y un recibo de sueldo no es un tipo de empleado.

Cormac Mulhall
fuente
Separar las preocupaciones del dominio (aplicación) frente a las capacidades de comportamiento y responsabilidad de OOD es un punto muy importante. Por ejemplo, en una aplicación de dibujo, quizás debería poder transformar un círculo en un cuadrado, pero esto no se modela fácilmente usando clases / objetos en la mayoría de los idiomas (ya que los objetos generalmente no pueden cambiar de clase). Por lo tanto, el dominio de la aplicación no siempre se correlaciona muy bien con la jerarquía de herencia de un lenguaje OOP dado y no debemos tratar de forzarlo; En muchos casos, la composición es mejor.
Erik Eidt
3
Esta respuesta es, con mucho, lo mejor que he visto sobre todo el problema, y ​​cómo puede surgir el potencial de errores de diseño en casos más generales. Gracias
HorusKol
1
@ErikEidt El problema de un comportamiento de cambio de objeto se puede resolver en OOD mediante descomposición. Por ejemplo, si una forma transformable se transforma en un círculo, no tiene que cambiar la clase. En cambio, la clase toma un objeto de comportamiento geométrico actual que puede cambiar por otro comportamiento cuando se transforma. Esta otra clase contiene las reglas de la forma geométrica que se está modelando actualmente, y la clase de forma morphable difiere a esta clase por comportamiento geométrico. Si el objeto se transforma en una clase diferente, cambia la clase de comportamiento a otra.
Cormac Mulhall
2
@ Cormac, cierto! Genéricamente lo llamaría una forma de composición, como mencioné, aunque podría identificar, más específicamente, un patrón de estrategia o algo así. En esencia, tienes una identidad que no se transforma y otras cosas que luego se pueden cambiar. En general, un buen resaltado de la diferencia entre los conceptos de dominio de aplicación y los detalles de la POO de un lenguaje dado, y la necesidad de mapear entre ellos (es decir, arquitectura, diseño y programación).
Erik Eidt
1
Pero un trabajo puede ser un sueldo.
8

Los círculos son un caso especial de elipses, es decir, que ambos ejes de la elipsis son iguales. Es fundamentalmente falso en el dominio del problema (geometría) afirmar que las elipses podrían ser una especie de círculo. El uso de este modelo defectuoso violaría muchas garantías de un círculo, por ejemplo, "todos los puntos en el círculo tienen la misma distancia al centro". Eso también sería una violación del Principio de sustitución de Liskov. ¿Cómo tendría una elipse un radio único? (No setRadius()pero más importante getRadius())

Si bien el modelado de círculos como un subtipo de elipses no es fundamentalmente incorrecto, es la introducción de la mutabilidad lo que rompe este modelo. Sin los métodos setX()y setY(), no hay violación de LSP. Si es necesario tener un objeto con diferentes dimensiones, crear una nueva instancia es una mejor solución:

class Ellipse {
  final double x;
  final double y;
  ...
  Ellipse withX(double newX) {
    return new Ellipse(x: newX, y: y);
  }
}
amon
fuente
1
bien - por lo que, si hubiera alguna interfaz común entre Ellipsey Circle(como getArea) que se abstrae a un tipo Shape- podría Ellipsey Circlesubtipo separado de Shapey satisfacer LSP?
HorusKol
1
@HorusKol Sí. Dos clases que heredan una interfaz que ambas realmente implementan correctamente está completamente bien.
Ixrec
7

Cormac tiene una respuesta realmente excelente, pero solo quiero explicar un poco sobre la razón de la confusión en primer lugar.

La herencia en OO a menudo se enseña utilizando metáforas del mundo real, como "las manzanas y las naranjas son subclases de fruta". Desafortunadamente, esto lleva a la creencia errónea de que los tipos en OO deben modelarse de acuerdo con algunas jerarquías taxonómicas existentes independientemente del programa.

Pero en el diseño de software, los tipos deben modelarse de acuerdo con los requisitos de la aplicación. Las clasificaciones en otros dominios suelen ser irrelevantes. En una aplicación real con objetos "Apple" y "Orange", digamos un sistema de gestión de inventario para un supermercado, probablemente no serán clases distintas, y categorías como "Fruta" serán atributos en lugar de supertipos.

El problema del círculo-elipse es un arenque rojo. En geometría, un círculo es una especialización de una elipse, pero las clases en su ejemplo no son figuras geométricas. Crucialmente, las figuras geométricas no son mutables. Sin embargo, pueden transformarse , pero luego un círculo puede transformarse en puntos suspensivos. Por lo tanto, un modelo donde los círculos pueden cambiar el radio pero no cambiar a puntos suspensivos no corresponde a la geometría. Tal modelo podría tener sentido en una aplicación particular (digamos una herramienta de dibujo) pero la clasificación geométrica es irrelevante para la forma en que diseña la jerarquía de clases.

Entonces, ¿debería ser Circle una subclase de Ellipse o viceversa? Depende totalmente de los requisitos de la aplicación particular que utiliza estos objetos. Una aplicación de dibujo podría tener diferentes opciones sobre cómo tratar círculos y elipses:

  1. Trate los círculos y las elipses como distintos tipos de formas con diferentes IU (p. Ej., Dos controles de cambio de tamaño en puntos suspensivos, un control en un círculo). Esto significa que puede tener una elipse que es geométricamente un círculo pero no un círculo desde la perspectiva de la aplicación.

  2. Trate todas las elipses, incluidos los círculos, de la misma manera, pero tenga la opción de "bloquear" x e y con el mismo valor.

  3. Las elipses son solo círculos donde se aplica una transformación de escala.

Cada posible diseño conducirá a un modelo de objeto diferente:

En el primer caso, Circle y Ellipses serán clases entre hermanos.

En la segunda, no habrá una clase de círculo distinta.

En el tercero, no habrá una clase de Elipse distinta. Entonces, el llamado problema círculo-elipse no entra en la imagen en ninguno de estos.

Entonces, para responder la pregunta planteada: ¿Debería el círculo extender la elipse? La respuesta es: depende de lo que quieras hacer con él. Pero probablemente no.

JacquesB
fuente
1
Una muy buena respuesta!
Utsav T
6

Es un error desde el principio insistir en tener una clase "Elipse" y una clase "Círculo" donde una es una subclase de la otra. Tienes dos opciones realistas: una es tener clases separadas. Pueden tener una superclase común, para cosas como el color, si el objeto está lleno, el ancho de línea para dibujar, etc.

La otra es tener una clase llamada "Elipse" solamente. Si tiene esa clase, es bastante fácil usarla para representar círculos (puede haber trampas dependiendo de los detalles de implementación; una Elipse tendrá algún ángulo y el cálculo de ese ángulo no debe tener problemas para una elipse en forma de círculo). Incluso podría tener métodos especializados para elipses circulares, pero estas "elipses circulares" seguirían siendo objetos "Elipse" completos.

gnasher729
fuente
Podría haber un método IsCircle que verificaría si un objeto particular de la clase Ellipse, de hecho, tiene ambos ejes iguales. También señaló el problema del ángulo. Los círculos no se pueden 'rotar'.
3

Siguiendo los puntos LSP, una solución 'adecuada' para este problema es cuando se encontraron @HorusKol y @Ixrec, derivando ambos tipos de Shape. Pero depende del modelo con el que esté trabajando, por lo que siempre debe volver a eso.

Lo que me enseñaron es:

Si el subtipo no puede realizar el mismo comportamiento que el supertipo, la relación no se cumple en la premisa IS-A, debe modificarse.

  • Un subtipo es un SUPERSET del supertipo.
  • Un supertipo es un SUBSET del subtipo.

En inglés:

  • Un tipo derivado es un SUPERSET del tipo base.
  • Un tipo base es un SUBSET del tipo derivado.

(Ejemplo:

  • Un automóvil con escape de chico malo sigue siendo un automóvil (según algunos).
  • Un automóvil sin motor, ruedas, cremallera de dirección, transmisión y solo el casco restante, no es un 'automóvil', es solo un casco).

Así es como funciona la clasificación (es decir, en el mundo animal) y, en principio, en OO.

Usando esto como la definición de herencia y polimorfismo (que siempre se escriben juntos), si este principio se rompe, debería intentar repensar los tipos que está tratando de modelar.

Como lo mencionaron @HorusKul y @Ixrec, en matemáticas tienes tipos claramente definidos. Pero en matemáticas, un círculo es una elipse porque es un SUBSET de la elipse. Pero en OOP no es así como funciona la herencia. Una clase solo debe heredar si es un SUPERSET (una extensión) de una clase existente, es decir, sigue siendo la clase base en todos los contextos.

Basándome en eso, creo que la solución debería reformularse ligeramente.

Tiene un tipo de base Shape, luego RoundedShape (efectivamente un círculo, pero he usado un nombre diferente aquí DELIBERADAMENTE ...)

... entonces Elipse.

De esa manera:

  • RoundedShape es una forma.
  • Ellipse es una forma redondeada.

(Esto ahora tiene sentido para las personas en el lenguaje. Ya tenemos un concepto claramente definido de un "círculo" en nuestras mentes, y lo que estamos tratando de hacer aquí generalizando (agregación) rompe ese concepto).

Andy bueno
fuente
Nuestros conceptos claramente definidos no siempre funcionan en la práctica.
-1

Desde una perspectiva OO, la elipse extiende el círculo, se especializa agregando algunas propiedades. Las propiedades existentes del círculo aún se mantienen en elipse, simplemente se vuelve más complejo y más específico. No veo ningún problema con el comportamiento en este caso como lo hace Cormac, las formas no tienen comportamiento. El único problema es que, en un sentido matemático o liguista, no parece correcto decir "una elipse ES un círculo". Debido a que el objetivo del ejercicio que no se menciona pero está implícito, era clasificar las formas geométricas. Esa puede ser una buena razón para considerar el círculo y la elipse como pares, no vincularlos por herencia y aceptar que simplemente tienen algunas de las mismas propiedades y NO dejar que su retorcida mente OO siga su camino con esa observación.

Martin Maat
fuente