¿Por qué sería problemático heredar Square de Rectangle si reemplazamos los métodos SetWidth y SetHeight?

103

Si un cuadrado es un tipo de rectángulo, ¿por qué no puede un cuadrado heredar de un rectángulo? ¿O por qué es un mal diseño?

He oído a la gente decir:

  

Si hiciste una derivación cuadrada del rectángulo, entonces una cuadrada debería ser   utilizable en cualquier lugar que espere un rectángulo

¿Cuál es el problema aquí? ¿Y por qué Square sería utilizable en cualquier lugar que esperas un rectángulo? Solo sería utilizable si creamos el objeto Cuadrado y si reemplazamos los métodos SetWidth y SetHeight para Cuadrado, ¿por qué habría algún problema?

  

Si tenía los métodos SetWidth y SetHeight en su clase base Rectangle   y si la referencia de su rectángulo apuntaba a un cuadrado, entonces SetWidth y   SetHeight no tiene sentido porque configurar uno cambiaría el   otro para que coincida. En este caso la plaza falla la sustitución de Liskov.   Prueba con Rectángulo y la abstracción de tener Cuadrado heredado de   El rectángulo es malo.

¿Puede alguien explicar los argumentos anteriores? Nuevamente, si superamos los métodos SetWidth y SetHeight en Square, ¿no resolvería este problema?

También he escuchado / leído:

  

El problema real es que no estamos modelando rectángulos, sino que   "rectángulos adaptables", es decir, rectángulos cuyo ancho o alto pueden ser   modificado después de la creación (y todavía lo consideramos igual   objeto). Si miramos la clase de rectángulo de esta manera, está claro   que un cuadrado no es un "rectángulo adaptable", porque un cuadrado no puede   ser remodelado y aún ser un cuadrado (en general). Matemáticamente, nosotros   No veo el problema porque la mutabilidad ni siquiera tiene sentido en una   contexto matemático

Aquí creo que "re-considerable" es el término correcto. Los rectángulos son "redimensionables" y también lo son los cuadrados. ¿Me estoy perdiendo algo en el argumento anterior? Un cuadrado puede redimensionarse como cualquier rectángulo.

    
pregunta user793468 07.05.2014 - 07:21
fuente

12 respuestas

185

Básicamente queremos que las cosas se comporten con sensatez.

Considere el siguiente problema:

Me dan un grupo de rectángulos y quiero aumentar su área en un 10%. Entonces, lo que hago es establecer la longitud del rectángulo en 1.1 veces lo que era antes.

public void IncreaseRectangleSizeByTenPercent(IEnumerable<Rectangle> rectangles)
{
  foreach(var rectangle in rectangles)
  {
    rectangle.Length = rectangle.Length * 1.1;
  }
}

Ahora, en este caso, todos mis rectángulos ahora han aumentado su longitud en un 10%, lo que aumentará su área en un 10%. Desafortunadamente, alguien realmente me ha pasado una mezcla de cuadrados y rectángulos, y cuando se cambió la longitud del rectángulo, también lo fue el ancho.

Mis pruebas de unidad pasan porque escribí todas mis pruebas de unidad para usar una colección de rectángulos. Ahora he introducido un error sutil en mi aplicación que puede pasar desapercibido durante meses.

Peor aún, Jim de contabilidad ve mi método y escribe otro código que usa el hecho de que si pasa cuadrados a mi método, obtiene un muy buen aumento de tamaño del 21%. Jim es feliz y nadie es más sabio.

Jim es promovido por un trabajo excelente a una división diferente. Alfred se une a la empresa como junior. En su primer informe de errores, Jill, de Advertising, informó que pasar cuadrados a este método da como resultado un aumento del 21% y quiere que se solucione el error. Alfred ve que los cuadrados y los rectángulos se utilizan en todas partes del código y se da cuenta de que romper la cadena de herencia es imposible. Tampoco tiene acceso al código fuente de Accounting. Así que Alfred arregla el error de esta manera:

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á contento con sus habilidades de hacker de uber y Jill firma que el error está solucionado.

El mes próximo no se paga a nadie porque la Contabilidad dependía de poder pasar cuadrados al método IncreaseRectangleSizeByTenPercent y obtener un aumento en el área del 21%. Toda la compañía entra en el modo de "corrección de errores de prioridad 1" para rastrear el origen del problema. Ellos rastrean el problema a la solución de Alfred. Saben que tienen que mantener contentos tanto a la Contabilidad como a la Publicidad. Así que solucionan el problema identificando al usuario con el método llamado así:

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;
    }
  }
}

Y así sucesivamente.

Esta anécdota se basa en situaciones reales que enfrentan los programadores a diario. Las violaciones del principio de sustitución de Liskov pueden introducir errores muy sutiles que solo se detectan años después de que se escriben, momento en el que corregir la infracción romperá un montón de cosas y no arreglar enfadará a tu mayor cliente.

Hay dos formas realistas de solucionar este problema.

La primera forma es hacer que Rectangle sea inmutable. Si el usuario de Rectangle no puede cambiar las propiedades Longitud y Ancho, este problema desaparece. Si desea un rectángulo con una longitud y anchura diferentes, cree uno nuevo. Los cuadrados pueden heredar de los rectángulos alegremente.

La segunda forma es romper la cadena de herencia entre cuadrados y rectángulos. Si un cuadrado se define como tener una sola propiedad SideLength y los rectángulos tienen una propiedad Length y Width y no hay herencia, es imposible romper cosas accidentalmente esperando un rectángulo y obteniendo un cuadrado. En términos de C #, podría seal su clase de rectángulo, lo que garantiza que todos los Rectángulos que obtenga sean realmente Rectángulos.

En este caso, me gusta la forma de "objetos inmutables" de solucionar el problema. La identidad de un rectángulo es su longitud y anchura. Tiene sentido que cuando desee cambiar la identidad de un objeto, lo que realmente desea sea un objeto nuevo . Si pierde un cliente antiguo y obtiene un cliente nuevo, no cambia el campo Customer.Id del cliente anterior al nuevo, crea un nuevo Customer .

Las violaciones del principio de Sustitución de Liskov son comunes en el mundo real, principalmente porque la mayoría de los códigos están escritos por personas que son incompetentes / bajo presión de tiempo / no les importa / cometen errores. Puede y lleva a algunos problemas muy desagradables. En la mayoría de los casos, desea favorecer la composición sobre la herencia en su lugar.

    
respondido por el Stephen 07.05.2014 - 09:19
fuente
29

Si todos sus objetos son inmutables, no hay problema. Cada cuadrado es también un rectángulo. Todas las propiedades de un rectángulo son también propiedades de un cuadrado.

El problema comienza cuando se agrega la capacidad de modificar los objetos. O realmente, cuando comienzas a pasar argumentos al objeto, no solo lees a los captadores de propiedades.

Hay modificaciones que puedes hacer en un Rectángulo que mantiene todas las invariantes de tu clase de Rectángulo, pero no todas las invariantes de Cuadrado, como cambiar el ancho o la altura. De repente, el comportamiento de un rectángulo no es solo sus propiedades, sino también sus posibles modificaciones. No es solo lo que obtienes del Rectángulo, también es lo que puedes poner .

Si su Rectángulo tiene un método setWidth que está documentado como que cambia el ancho y no modifica la altura, entonces Square no puede tener un método compatible. Si cambia el ancho y no la altura, el resultado ya no es un cuadrado válido. Si eligió modificar tanto el ancho como el alto del cuadrado cuando usa setWidth , no está implementando la especificación del setWidth del rectángulo. Simplemente no puedes ganar.

Cuando mire lo que puede "poner" en un Rectángulo y un Cuadrado, qué mensajes puede enviarles, es probable que encuentre que cualquier mensaje que pueda enviar válidamente a un Cuadrado, también puede enviarlo a un Rectángulo.

Es una cuestión de covarianza vs. contravarianza.

Los métodos de una subclase adecuada, uno en el que se pueden usar instancias en todos los casos en que se espera la superclase, requieren que cada método:

  • Devuelve solo los valores que devolvería la superclase, es decir, el tipo de retorno debe ser un subtipo del tipo de retorno del método de superclase. El retorno es co-variante.
  • Acepte todos los valores que el supertipo aceptaría, es decir, los tipos de argumento deben ser supertipos de los tipos de argumento del método de superclase. Los argumentos son contrarios a la variante.

Entonces, volvamos al Rectángulo y al Cuadrado: si Cuadrado puede ser una subclase del Rectángulo, depende completamente de los métodos que tenga el Rectángulo.

Si Rectangle tiene configuradores individuales para ancho y alto, Square no será una buena subclase.

Del mismo modo, si hace que algunos métodos sean co-variantes en los argumentos, como tener compareTo(Rectangle) en Rectángulo y compareTo(Square) en Cuadrado, tendrá un problema al usar un cuadrado como Rectángulo.

Si diseñas tu Cuadrado y Rectángulo para que sean compatibles, probablemente funcionará, pero deberían desarrollarse juntos, o apostaré a que no funcionará.

    
respondido por el lrn 07.05.2014 - 13:19
fuente
16

Hay muchas respuestas buenas aquí; La respuesta de Stephen en particular hace un buen trabajo para ilustrar por qué las violaciones del principio de sustitución conducen a conflictos reales entre equipos.

Pensé que podría hablar brevemente sobre el problema específico de los rectángulos y cuadrados, en lugar de usarlo como una metáfora de otras violaciones del LSP.

Hay un problema adicional con square-is-a-special-kind-rectangle que rara vez se menciona, y es: ¿por qué nos detenemos con cuadrados y rectángulos ? Si estamos dispuestos a decir que un cuadrado es un tipo especial de rectángulo, seguramente también deberíamos estar dispuestos a decir:

  • Un cuadrado es un tipo especial de rombo, es un rombo con ángulos cuadrados.
  • Un rombo es un paralelogramo especial, es un paralelogramo con lados iguales.
  • Un rectángulo es un tipo especial de paralelogramo, es un paralelogramo con ángulos cuadrados
  • Un rectángulo, un cuadrado y un paralelogramo son todos un tipo especial de trapecio: son trapezoides con dos conjuntos de lados paralelos
  • Todos los anteriores son tipos especiales de cuadriláteros
  • Todas las anteriores son tipos especiales de formas planas
  • Y así sucesivamente; Podría seguir por un tiempo aquí.

¿Qué demonios deberían estar aquí todas las relaciones? Los lenguajes basados en herencia de clase como C # o Java no fueron diseñados para representar este tipo de relaciones complejas con múltiples tipos diferentes de restricciones. Es mejor simplemente evitar la pregunta por completo al no tratar de representar todas estas cosas como clases con relaciones de subtipo.

    
respondido por el Eric Lippert 09.05.2014 - 18:44
fuente
13

Desde una perspectiva matemática, un cuadrado es un rectángulo. Si un matemático modifica el cuadrado para que ya no se adhiera al contrato cuadrado, se convierte en un rectángulo.

Pero en el diseño OO, esto es un problema. Un objeto es lo que es, y esto incluye comportamientos así como el estado. Si sostengo un objeto cuadrado, pero alguien más lo modifica para que sea un rectángulo, eso viola el contrato de la plaza sin que sea mi culpa. Esto hace que ocurran todo tipo de cosas malas.

El factor clave aquí es mutability . ¿Puede una forma cambiar una vez que está construida?

  • Mutable: si se permite que las formas cambien una vez construidas, un cuadrado no puede tener una relación is-a con un rectángulo. El contrato de un rectángulo incluye la restricción de que los lados opuestos deben tener la misma longitud, pero los lados adyacentes no tienen que serlo. El cuadrado debe tener cuatro lados iguales. Modificar un cuadrado a través de una interfaz de rectángulo puede violar el contrato cuadrado.

  • Inmutable: si las formas no pueden cambiar una vez construidas, entonces un objeto cuadrado también debe cumplir siempre el contrato de rectángulo. Un cuadrado puede tener una relación is-a con un rectángulo.

En ambos casos, es posible pedir a un cuadrado que produzca una nueva forma en función de su estado con uno o más cambios. Por ejemplo, uno podría decir "cree un nuevo rectángulo basado en este cuadrado, excepto que los lados opuestos A y C son dos veces más largos". Dado que se está construyendo un nuevo objeto, el cuadrado original continúa cumpliendo con sus contratos.

    
respondido por el user22815 07.05.2014 - 08:00
fuente
13
  

¿Y por qué Square se puede usar en cualquier lugar donde esperas un rectángulo?

Porque eso es parte de lo que significa ser un subtipo (vea también: Principio de sustitución de Liskov). Usted puede hacer, necesita poder hacer esto:

Square s = new Square(5);
Rect r = s;
doSomethingWith(r); // written assuming a Rect, actually calls Square methods

En realidad, haces esto todo el tiempo (a veces incluso de manera más implícita) cuando usas la POO.

  

y si superamos los métodos SetWidth y SetHeight para Square, ¿por qué habría algún problema?

Porque no puedes anular sensiblemente esos para Square . Debido a que un cuadrado no puede "redimensionarse como cualquier rectángulo". Cuando cambia la altura de un rectángulo, el ancho permanece igual. Pero cuando la altura de un cuadrado cambia, el ancho debe cambiar en consecuencia. El problema no es solo ser redimensionable, se está redimensionando en ambas dimensiones de forma independiente.

    
respondido por el user7043 07.05.2014 - 07:31
fuente
9

Lo que estás describiendo entra en conflicto con lo que se llama el Principio de Sustitución de Liskov . La idea básica del LSP es que siempre que uses una instancia de una clase en particular, siempre deberías poder intercambiar una instancia de cualquier subclase de esa clase, sin introducir errores.

El problema de Rectangle-Square no es realmente una buena manera de presentar a Liskov. Intenta explicar un principio amplio usando un ejemplo que es bastante sutil, y entra en conflicto con uno de los Definiciones intuitivas más comunes en todas las matemáticas. Algunos lo llaman el problema del Círculo de la Elipse por esa razón, pero solo es un poco mejor en lo que se refiere a esto. Un enfoque mejor es dar un paso atrás hacia atrás, utilizando lo que yo llamo el problema Paralelogramo-Rectángulo. Esto hace que las cosas sean mucho más fáciles de entender.

Un paralelogramo es un cuadrilátero con dos pares de lados paralelos. También tiene dos pares de ángulos congruentes. No es difícil imaginar un objeto de paralelogramo en estas líneas:

class Parallelogram {
    function getSideA() {};
    function getSideB() {};
    function getAngleA() {};
    function getAngleB() {};
    function setSideA(newLength) {};
    function setSideB(newLength) {};
    function setAngleA(newAngle) {};
    function setAngleB(newAngle) {};
}

Una forma común de pensar en un rectángulo es como un paralelogramo con ángulos rectos. A primera vista, esto podría hacer que Rectangle sea un buen candidato para heredar de un paralelogramo , de modo que pueda reutilizar todo ese código delicioso. Sin embargo:

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 qué estas dos funciones introducen errores en el rectángulo? El problema es que no se pueden cambiar los ángulos en un rectángulo : se definen como siempre de 90 grados, por lo que esta interfaz no funciona para el Rectángulo que se hereda del Paralelogramo. Si cambio un rectángulo en el código que espera un paralelogramo, y ese código intenta cambiar el ángulo, es casi seguro que habrá errores. Tomamos algo que se podía escribir en la subclase y lo hicimos de solo lectura, y eso es una violación de Liskov.

Ahora, ¿cómo se aplica esto a los cuadrados y rectángulos?

Cuando decimos que puedes establecer un valor, generalmente queremos decir algo un poco más fuerte que simplemente poder escribir un valor en él. Implicamos un cierto grado de exclusividad: si establece un valor, y salvo circunstancias extraordinarias, permanecerá en ese valor hasta que lo establezca nuevamente. Hay muchos usos para los valores que pueden ser escrito para, pero no te quedes establecido, pero también hay muchos casos que dependen de un valor que permanece donde está una vez que lo configuras. Y ahí es donde nos encontramos con otro 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) {};
}

Los errores heredados de nuestra clase Square de Rectangle, pero tiene algunos nuevos. El problema con setSideA y setSideB es que ninguno de estos es realmente configurable: aún puede escribir un valor en cualquiera de los dos, pero cambiará de debajo de usted si se escribe el otro. Si cambio esto por un paralelogramo en el código que depende de poder establecer lados de forma independiente, se volverá loco.

Ese es el problema, y es por eso que hay un problema con el uso de Rectangle-Square como introducción a Liskov. Rectangle-Square depende de la diferencia entre poder escribir en algo y ser capaz de establecer , y esa es una diferencia mucho más sutil que ser capaz de configurar algo en lugar de tener ser de solo lectura Rectangle-Square todavía tiene valor como ejemplo, ya que documenta un problema bastante común que debe tenerse en cuenta, pero no debe usarse como ejemplo introductorio . Deje que el aprendiz se base primero en lo básico, y luego lance algo más duro.

    
respondido por el The Spooniest 08.05.2014 - 19:04
fuente
8

Subtipo se trata de comportamiento.

Para que el tipo B sea un subtipo del tipo A , debe admitir todas las operaciones que el tipo A admite con la misma semántica (es decir, "comportamiento"). Usando la razón de que cada B es una A no funciona , la compatibilidad de comportamiento tiene la última palabra. La mayoría de las veces "B es un tipo de A" se superpone con "B se comporta como A", pero no siempre .

Un ejemplo:

Considera el conjunto de números reales. En cualquier idioma, podemos esperar que admitan las operaciones + , - , * y / . Ahora considere el conjunto de enteros positivos ({1, 2, 3, ...}). Claramente, cada entero positivo es también un número real. Pero, ¿es el tipo de enteros positivos un subtipo del tipo de números reales? Veamos las cuatro operaciones y veamos si los enteros positivos se comportan de la misma manera que los números reales:

  • + : podemos agregar enteros positivos sin problemas.
  • - : no todas las restas de enteros positivos dan como resultado enteros positivos. P.ej. 3 - 5 .
  • * : podemos multiplicar enteros positivos sin problemas.
  • / : no siempre podemos dividir enteros positivos y obtener un entero positivo. P.ej. 5 / 3 .

Entonces, a pesar de que los enteros positivos son un subconjunto de números reales, no son un subtipo. Se puede hacer un argumento similar para los enteros de tamaño finito. Claramente, cada entero de 32 bits también es un entero de 64 bits, pero 32_BIT_MAX + 1 le dará resultados diferentes para cada tipo. Por lo tanto, si le proporcioné un programa y usted cambió el tipo de cada variable de entero de 32 bits a entero de 64 bits, existe la posibilidad de que el programa se comporte de manera diferente (lo que casi siempre significa incorrectamente ).

Por supuesto, podría definir + para ints de 32 bits de modo que el resultado sea un entero de 64 bits, pero ahora tendrá que reservar 64 bits de espacio cada vez que agregue dos números de 32 bits. Esto puede o no ser aceptable para usted dependiendo de sus necesidades de memoria.

¿Por qué importa esto?

Es importante que los programas sean correctos. Podría decirse que es la propiedad más importante para un programa. Si un programa es correcto para algún tipo A , la única manera de garantizar que el programa continuará siendo correcto para algún subtipo B es si B se comporta como A en todos los aspectos.

Así que tienes el tipo de Rectangles , cuya especificación dice que sus lados se pueden cambiar de forma independiente. Usted escribió algunos programas que usan Rectangles y asume que la implementación sigue la especificación. Luego introdujo un subtipo llamado Square cuyos lados no pueden cambiar de tamaño de forma independiente. Como resultado, la mayoría de los programas que redimensionan rectángulos ahora estarán equivocados.

    
respondido por el Doval 07.05.2014 - 14:18
fuente
6
  

Si un cuadrado es un tipo de rectángulo, ¿por qué no se puede heredar un cuadrado de un rectángulo? ¿O por qué es un mal diseño?

Lo primero es lo primero, pregúntate por qué crees que un cuadrado es un rectángulo.

Por supuesto, la mayoría de la gente aprendió eso en la escuela primaria, y parece obvio. Un rectángulo es una forma de 4 lados con ángulos de 90 grados, y un cuadrado cumple con todas esas propiedades. Entonces, ¿no es un cuadrado un rectángulo?

Sin embargo, lo importante es que todo depende de cuáles sean sus criterios iniciales para agrupar objetos, qué contexto está mirando estos objetos. En geometría, las formas se clasifican en función de las propiedades de sus puntos, líneas y ángeles.

Antes de que incluso digas "un cuadrado es un tipo de rectángulo", primero debes preguntarte, esto se basa en los criterios que me interesan .

En la gran mayoría de los casos, no va a ser lo que te importa en absoluto. La mayoría de los sistemas que modelan formas, como las GUI, los gráficos y los videojuegos, no están relacionados principalmente con la agrupación geométrica de un objeto, sino con su comportamiento. ¿Alguna vez has trabajado en un sistema que importaba que un cuadrado fuera un tipo de rectángulo en el sentido geométrico? ¿Qué te daría eso, sabiendo que tiene 4 lados y ángulos de 90 grados?

No estás modelando un sistema estático, estás modelando un sistema dinámico donde sucederán cosas (las formas se crearán, destruirán, alterarán, dibujarán, etc.). En este contexto, le interesa el comportamiento compartido entre objetos, ya que su principal preocupación es qué puede hacer con una forma, qué reglas deben mantenerse para tener un sistema coherente.

En este contexto, un cuadrado definitivamente no es un rectángulo , porque las reglas que gobiernan cómo se puede alterar el cuadrado no son las mismas que el rectángulo. Así que no son el mismo tipo de cosas.

En cuyo caso no los modeles como tales. ¿Por que lo harias? Te gana nada más que una restricción innecesaria.

  

Solo sería útil si creamos el objeto Cuadrado y si reemplazamos los métodos SetWidth y SetHeight para Square, ¿por qué habría algún problema?

Si haces eso, prácticamente estás declarando en un código que no son lo mismo. Su código estaría diciendo que un cuadrado se comporta de esta manera y un rectángulo se comporta de esa manera pero siguen siendo los mismos.

Claramente, no son lo mismo en el contexto que te importa porque acabas de definir dos comportamientos diferentes. Entonces, ¿por qué fingir que son iguales si solo son similares en un contexto que no te interesa?

Esto resalta un problema importante cuando los desarrolladores llegan a un dominio que desean modelar. Es muy importante aclarar qué contexto le interesa antes de comenzar a pensar en los objetos en el dominio. En qué aspecto estás interesado. Hace miles de años, los griegos se preocuparon por las propiedades compartidas de las líneas y los ángeles de las formas, y las agruparon en función de éstas. Eso no significa que esté obligado a continuar esa agrupación si no es lo que le importa (que en el 99% de las veces no le importará el modelado en software).

Muchas de las respuestas a esta pregunta se centran en la subtitulación de la conducta de agrupación 'porque son las reglas .

Pero es muy importante entender que no estás haciendo esto solo para seguir las reglas. Estás haciendo esto porque en la gran mayoría de los casos esto es lo que realmente te importa también. No te importa si un cuadrado y un rectángulo comparten los mismos ángeles internos. Te preocupas por lo que pueden hacer mientras siguen siendo cuadrados y rectángulos. Te importa el comportamiento de los objetos porque estás modelando un sistema que se centra en cambiar el sistema según las reglas del comportamiento de los objetos.

    
respondido por el Cormac Mulhall 08.05.2014 - 10:56
fuente
5
  

Si un cuadrado es un tipo de rectángulo, ¿por qué no se puede heredar un cuadrado de un rectángulo?

El problema radica en pensar que si las cosas están relacionadas de alguna manera en la realidad, deben relacionarse exactamente de la misma manera después del modelado.

Lo más importante en el modelado es identificar los atributos comunes y los comportamientos comunes, definirlos en la clase básica y agregar atributos adicionales en las clases secundarias.

El problema con tu ejemplo es que es completamente abstracto. Mientras nadie sepa, para qué planeas usar esas clases, es difícil adivinar qué diseño debes hacer. Pero si realmente desea tener solo altura, ancho y tamaño, sería más lógico:

  • define Square como clase base, con el parámetro width y resize(double factor) cambiando el ancho por el factor dado
  • define la clase Rectangle y la subclase de Square, porque agrega otro atributo, height , y reemplaza su función resize , que llama a super.resize y luego cambia el tamaño por el factor dado

Desde el punto de vista de la programación, no hay nada en Square, que Rectangle no tenga. No tiene sentido hacer un Cuadrado como la subclase de Rectángulo.

    
respondido por el Danubian Sailor 07.05.2014 - 16:03
fuente
4

Porque, por LSP, crear una relación de herencia entre los dos y anular setWidth y setHeight para garantizar que el cuadrado tenga los mismos aspectos introductorios confusos y no intuitivos. Digamos que tenemos un 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

Pero si el método createRectangle devolvió Square , porque es posible gracias a Square que hereda de Rectange . Entonces las expectativas se rompen. Aquí, con este código, esperamos que la configuración de ancho o alto solo cause cambios en ancho o alto respectivamente. El punto de la POO es que cuando trabajas con la superclase, no tienes conocimiento de ninguna subclase debajo de ella. Y si la subclase cambia el comportamiento de modo que vaya en contra de las expectativas que tenemos sobre la superclase, entonces hay una alta probabilidad de que ocurran errores. Y ese tipo de errores son difíciles de corregir y corregir.

Una de las ideas principales sobre la POO es que se trata de un comportamiento, no de datos heredados (que también es uno de los principales conceptos erróneos de la OMI). Y si nos fijamos en el cuadrado y el rectángulo, ellos mismos no tienen ningún comportamiento que podamos relacionar en relación de herencia.

    
respondido por el Euphoric 07.05.2014 - 08:37
fuente
2

Lo que dice LSP es que todo lo que hereda de Rectangle debe ser Rectangle . Es decir, debería hacer lo que hace Rectangle .

Probablemente, la documentación para Rectangle está escrita para indicar que el comportamiento de un Rectangle llamado r es el siguiente:

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

Si tu Cuadrado no tiene el mismo comportamiento, entonces no se comporta como un Rectangle . Entonces, LSP dice que no debe heredar de Rectangle . El lenguaje no puede hacer cumplir esta regla, porque no puede evitar que hagas algo mal en una anulación de un método, pero eso no significa que "está bien porque el lenguaje me permite anular los métodos" ¡es un argumento convincente para hacerlo!

Ahora, sería posible escribir la documentación para Rectangle de tal manera que no implique que el código anterior se imprima con 10, en cuyo caso tal vez su Square podría ser un Rectangle . Es posible que vea documentación que diga algo como "esto hace X. Además, la implementación en esta clase hace S". Si es así, tiene un buen caso para extraer una interfaz de la clase y distinguir entre lo que garantiza la interfaz y lo que la clase garantiza además de eso. Pero cuando la gente dice que "un cuadrado mutable no es un rectángulo mutable, mientras que un cuadrado inmutable es un rectángulo inmutable", básicamente asumen que lo anterior es parte de la definición razonable de un rectángulo mutable.

    
respondido por el Steve Jessop 07.05.2014 - 18:25
fuente
1

Los subtipos y, por extensión, la programación de OO, a menudo se basan en el Principio de Sustitución de Liskov, de que cualquier valor de tipo A puede usarse donde se requiere una B, si A < = B. Esto es prácticamente un axioma en OO La arquitectura, es decir. se asume que todas las subclases tendrán esta propiedad (y, de no ser así, los subtipos tienen errores y deben corregirse).

Sin embargo, resulta que este principio no es realista o no es representativo de la mayoría de los códigos, ¡o de hecho es imposible de satisfacer (en casos no triviales)! Este problema, conocido como el problema del cuadrado-rectángulo o el problema del círculo-elipse ( enlace ) es un ejemplo famoso de lo difícil que es cumplir.

Tenga en cuenta que podríamos implementar más y más cuadrados y rectángulos de observación equivalente, pero solo eliminando más y más funcionalidad hasta que la distinción sea inútil.

Como ejemplo, consulte enlace

    
respondido por el Warbo 08.05.2014 - 18:10
fuente

Lea otras preguntas en las etiquetas