Tener Circle
extendidoEllipse
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 setX
y setY
. Llamar setRadius
a 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.
object-oriented
solid
liskov-substitution
HorusKol
fuente
fuente
Respuestas:
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.
fuente
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 importantegetRadius()
)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()
ysetY()
, no hay violación de LSP. Si es necesario tener un objeto con diferentes dimensiones, crear una nueva instancia es una mejor solución:fuente
Ellipse
yCircle
(comogetArea
) que se abstrae a un tipoShape
- podríaEllipse
yCircle
subtipo separado deShape
y satisfacer LSP?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:
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.
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.
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.
fuente
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.
fuente
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.
En inglés:
(Ejemplo:
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:
(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).
fuente
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.
fuente