¿Es hereditaria la herencia que agrega reglas? [duplicar]

7

Me metí en un debate sobre esta pregunta que se destila si es una buena idea que una especialización de una clase agregue reglas de negocios. Desafortunadamente, este punto quedó pisoteado en los comentarios, así que lo vuelvo a hacer como una pregunta aparte.

Creo dos cosas:

  • Un objeto es responsable de su consistencia interna
  • Una clase especial / secundaria tiene reglas más específicas que la súper clase, que puede considerarse como el caso general.

El resultado lógico de esto es que una especialización puede aceptar solo algunos valores de entrada para su método o puede cambiar algunos valores para mantener la coherencia. ¿Pero no está bien, ya que proteger su consistencia interna es lo que debe hacer un objeto?

Un punto que muchas personas estaban haciendo es que algunos códigos podrían romperse si se hicieran suposiciones. Por ejemplo, establecer el ancho no cambiaría la altura de un cuadrado. Sin embargo, ¿no sería eso un código malo? ¿Ya que hace suposiciones sobre cómo el objeto hace algo en lugar de simplemente decirle qué hacer y no preocuparse por ello?

Si no escribiéramos código como ese, casi todas las sobrecargas tendrían problemas. ¿Con qué frecuencia la sobrecarga no agrega una condición de falla adicional o más lógica interna que puede verse a través de otras partes de la interfaz? Tal vez el punto que un viejo profesor de mí hizo una vez es correcto: "solo debes usar la herencia para sobrecargar al constructor". En ese momento parecía un poco estricto, pero ahora parece que la única forma de garantizar que este tipo de problema nunca ocurra. Para usar el cuadrado viejo: analogía de rectángulo otra vez:

public class Rectangle
{
    private int width, height;

    public Rectangle(int width, int height){this.width = width; this.height = height;}

    public void SetWidth... SetHeight...
}

public class Square : Rectangle
{
    public Square(int diameter) : base(diameter, diameter) {}

    public void SetDiameter...
}

Nota: espero que podamos jugar esta pregunta un poco menos 'sobre el hombre' que la pregunta que la inspiró. He estado en Stack Exchange durante más de tres años, pero me sentí bastante intimidado por el tipo de respuestas aquí.

    
pregunta Roy T. 08.05.2014 - 14:41

4 respuestas

7

La trampa en la que cae mucha gente es ver la herencia como un medio para codificar la relación o similitud de cualquier entre dos clases. Ese no es el caso. La herencia es útil para ciertos tipos limitados de relaciones y en realidad es dañina cuando se usa fuera de esos contextos. La falta de sustituibilidad es una de las razones por las que.

El punto crucial que mucha gente se pierde en el ejemplo del cuadrado-rectángulo es que es perfectamente sustituible si se revierte la relación. En el diseño orientado a objetos, un rectángulo es una forma especializada de un cuadrado. La razón por la que es difícil de ver es que la gente quiere organizar las clases por las similitudes entre las clases, tal vez siguiendo taxonomías del mundo real, cuando realmente deberían preocuparse por organizar las clases según los métodos que utiliza el código de llamada Tendrá que usar en una colección mixta de esas clases. Ahí es donde aparece la idea de la sustituibilidad.

Piénsalo de esta manera. Tienes un montón de código que establece el ancho de los cuadrados y quieres lanzar algunos rectángulos en la mezcla. Puede establecer el ancho en un cuadrado o en un rectángulo todo el día sin violar la posibilidad de sustitución, pero la configuración independiente de altura solo se aplica al rectángulo, por lo que no debe formar parte de la clase base. Usted está agregando a la clase especializada, no cambiando el comportamiento común.

En otras palabras, no lo conviertas en una elección falsa y di a ti mismo: "No tengo más remedio que violar la sustituibilidad". Si no puede hacer algo sin violar la sustituibilidad, entonces necesita cambiar su relación de herencia o no usar la herencia en absoluto.

    
respondido por el Karl Bielefeldt 08.05.2014 - 15:26
3
  

Un punto que muchas personas estaban haciendo es que algunos códigos podrían romperse si se hicieran suposiciones. Por ejemplo, establecer el ancho no cambiaría la altura de un cuadrado. Sin embargo, ¿no sería eso un código malo? ¿Ya que hace suposiciones sobre cómo el objeto hace algo en lugar de simplemente decirle qué hacer y no preocuparse por ello?

Se me permite asumir que el objeto sigue sus especificaciones. Si la especificación para Rectángulos dice que el ancho y la altura son modificables independientemente, entonces cualquier implementación debe cumplir. (Si no requiere conformidad, es imposible razonar acerca de su programa). Ahora, podría argumentar que la especificación para Rectangles nunca dijo que setWidth no puede cambiar la altura, pero si intenta enumerar todos Las cosas que algo no debe hacer encontrarán que la lista es infinita:

  • setWidth no debe reformatear mi disco duro
  • setWidth no debe eliminar archivos en mi carpeta de inicio
  • setWidth no debe realizar cambios en el Registro de Windows
  • setWidth no debe cambiar el estado de otro objeto
  • setWidth no debe entrar en un bucle infinito
  • setWidth no debe publicar en Twitter en mi nombre
  • ...

La única forma sensata de especificar algo es enumerar las cosas que debe y puede hacer y asumir que todo lo que no esté en la lista está prohibido. Entonces, si la especificación para setWidth dice que cambia el ancho del rectángulo, asumo que no cambia la altura.

  

¿Con qué frecuencia la sobrecarga no agrega una condición de error adicional ...

Hacer esto definitivamente te traerá dolor y desdicha. Cualquier programa escrito de acuerdo con las especificaciones asume que una determinada operación solo puede fallar debido a A , B y C . Si introduce una nueva condición de falla D , posiblemente nadie pueda manejarlo.

Una palabra para los sabios, sin embargo, si necesitas este tipo de sustituibilidad, la herencia probablemente no sea lo que quieres. Comienzas con algún tipo Foo y luego te das cuenta de que quieres un ShinyFoo . Más tarde quieres un TransparentFoo . Eventualmente querrá un ShinyTransparentFoo y luego estará en problemas. No se encuentra con este tipo de problema si usa una interfaz y confía en la composición para reutilizar el comportamiento.

    
respondido por el Doval 08.05.2014 - 16:35
0

Tome el ejemplo clásico de herencia: Dog: Animal donde Dog es una clase que hereda de Animal. Mientras que el animal puede comer, un perro puede ladrar, saltar y nadar. Así que le agregas estos métodos a Dog.

Tiene sentido desde el punto de vista de una relación. ¿Por qué debería poder ladrar Animal? ¿Por qué el perro no debería poder ladrar? De hecho, nadie está diciendo lo contrario. Usted ve este tipo de relación a menudo en el código, sin embargo, cada vez que necesite usar la corteza, simultáneamente debe saber si se trata de un Perro. Por lo tanto, cualquier uso de la corteza automáticamente agrega una diferencia directa en el Perro de cualquier manera que lo cortes. Incluso si tomas un animal y verificas si es un perro, y luego actúas en consecuencia, ya no estás actuando en el caso general de manejo de animales. No hay nada que lo detenga de diong esto, aunque ha perdido cualquier ventaja que tenía con la herencia.

Para aprovechar realmente la herencia, debe dejar que las clases heredadas representen varias implementaciones de la súper clase. De esta manera, no es suficiente que una clase "sea un tipo de" otra clase. También debe jugar la parte de la súper clase con pequeñas excepciones, como durante su creación, que es el único punto de su programa que debe conocer la implementación específica que se está utilizando.

Entonces, tal vez, un mejor ejemplo no sería Dog: Animal sino Square: Drawable, donde Drawable es un objeto al que se puede llamar para dibujarse a sí mismo independientemente de cómo.

    
respondido por el Neil 08.05.2014 - 15:52
0
  

El resultado lógico de esto es que una especialización puede aceptar solo algunos valores de entrada para su método o puede cambiar algunos valores para mantener la coherencia. ¿Pero no está bien, ya que proteger su consistencia interna es lo que debe hacer un objeto?

El problema no es realmente lo que el objeto hace internamente para implementar el mensaje que se le pide que realice (por ejemplo, setWidth). El problema es lo que la persona que llama entiende el resultado final del mensaje.

Tanto un método setWidth en un rectángulo que solo establece el ancho, como un método setWidth en un cuadrado que establece el ancho y la altura, son perfectamente válidos siempre que sea claro para cualquiera que haga las llamadas lo que estos métodos hacen.

El problema es cuando dices que un cuadrado es un rectángulo , y luego miras el método setWidth de un rectángulo. Ese método pone el objeto en un estado particular. Todos los que llaman al método entienden que actualizará el ancho y solo el ancho.

Cuando dices que un cuadrado es un rectángulo , le estás diciendo a cualquiera que trabaje con el cuadrado que todo lo que sabías sobre los rectángulos todavía se mantiene cuando trabajas con cuadrados . Y una de las cosas que el programador sabía acerca de los rectángulos era que setWidth actualizaba solo el ancho. Ese es el comportamiento conocido.

Así que el programador sabe lo que hace este método, ella sabe que el cuadrado es un rectángulo, por lo que sabe que setWidth solo actualizará el ancho.

Excepto que no solo actualiza el ancho, ahora también actualiza el alto. El programa acaba de mentir al programador, ha roto su contrato.

Este ejemplo puede parecer trivial, pero se vuelve mucho más importante cuando se toma en cuenta el polimorfismo.

Es posible que no sepa qué forma tiene, o que no sepa si tiene un rectángulo o un cuadrado. Si sabes que un cuadrado es un rectángulo, sabes que cualquier objeto que tengas se comportará de la manera que esperas que se comporte un rectángulo si le aplicas un comportamiento de rectángulo.

Pero, por supuesto, si no es así, tiene un problema grave porque ahora no puede confiar en que sus objetos se comporten de la manera que espera, y por lo tanto no puede confiar en que su código haga lo que espera que haga.

En lugar de eso, lo que debe hacer es interrumpir la conexión, decirle al programador que un cuadrado NO es un rectángulo y que el programador necesita saber si tiene un cuadrado o no, y debe entender qué comportamiento tiene específicamente un cuadrado porque no es lo mismo que un rectángulo.

    
respondido por el Cormac Mulhall 08.05.2014 - 20:10

Lea otras preguntas en las etiquetas