De: http://www.artima.com/lejava/articles/designprinciples4.html
Erich Gamma: Sigo pensando que es verdad incluso después de diez años. La herencia es una forma genial de cambiar el comportamiento. Pero sabemos que es frágil, porque la subclase puede hacer fácilmente suposiciones sobre el contexto en el que se llama a un método que anula. Hay un acoplamiento estrecho entre la clase base y la subclase, debido al contexto implícito en el que se llamará al código de subclase que conecto. La composición tiene una mejor propiedad. El acoplamiento se reduce con solo tener algunas cosas más pequeñas que se conectan a algo más grande, y el objeto más grande simplemente llama al objeto más pequeño. Desde el punto de vista de la API, definir que un método se puede anular es un compromiso más fuerte que definir que se puede llamar a un método.
No entiendo lo que quiere decir. ¿Alguien podría explicarlo?
Si publica una función normal, otorga un contrato unilateral:
¿Qué hace la función si se llama?
Si publica una devolución de llamada, también otorga un contrato unilateral: ¿
Cuándo y cómo se llamará?
Y si publica una función reemplazable, ambas son a la vez, por lo que otorga un contrato a dos caras: ¿
cuándo se llamará y qué debe hacer si se llama?
Incluso si sus usuarios no abusan de su API (al romper su parte del contrato, que puede ser prohibitivamente costoso de detectar), puede ver fácilmente que este último necesita mucha más documentación, y todo lo que documenta es un compromiso, lo que limita Sus otras opciones.
Un ejemplo de incumplir un contrato de este tipo a doble cara es el movimiento desde
show
yhide
haciasetVisible(boolean)
en java.awt.Component .fuente
show
yhide
aún existen, son solo@Deprecated
. Entonces, el cambio no rompe ningún código que simplemente los invoca. Pero si los ha anulado, los clientes que migren al nuevo 'setVisible' no llamarán a sus anulaciones. (Nunca he usado Swing, así que no sé qué tan común es anularlos; pero como sucedió hace mucho tiempo, imagino que la razón por la que Deduplicator lo recuerda es porque lo mordió dolorosamente)La respuesta de Kilian Foth es excelente. Solo me gustaría agregar el ejemplo canónico * de por qué esto es un problema. Imagine una clase de punto entero:
Ahora subclasifiquemos para que sea un punto 3D.
Súper simple! Usemos nuestros puntos:
Probablemente se esté preguntando por qué estoy publicando un ejemplo tan fácil. Aquí está el truco:
Cuando comparamos el punto 2D con el punto 3D equivalente, obtenemos verdadero, pero cuando invertimos la comparación, obtenemos falso (porque p2a falla
instanceof Point3D
).Conclusión
Por lo general, es posible implementar un método en una subclase de tal manera que ya no sea compatible con la forma en que la superclase espera que funcione.
En general, es imposible implementar equals () en una subclase significativamente diferente de una manera que sea compatible con su clase padre.
Cuando escribe una clase que pretende permitir que las personas subclasifiquen, es una muy buena idea redactar un contrato sobre cómo debe comportarse cada método. Aún mejor sería un conjunto de pruebas unitarias que las personas podrían ejecutar contra sus implementaciones de métodos anulados para demostrar que no violan el contrato. Casi nadie hace eso porque es demasiado trabajo. Pero si te importa, eso es lo que debes hacer.
Un gran ejemplo de un contrato bien enunciado es Comparator . Simplemente ignore lo que dice
.equals()
por las razones descritas anteriormente. Aquí hay un ejemplo de cómo Comparator puede hacer cosas.equals()
que no puede .Notas
El elemento 8 "Java efectivo" de Josh Bloch fue la fuente de este ejemplo, pero Bloch usa un ColorPoint que agrega un color en lugar de un tercer eje y usa dobles en lugar de ints. El ejemplo de Bloch en Java está básicamente duplicado por Odersky / Spoon / Venners que hicieron que su ejemplo estuviera disponible en línea.
Varias personas se han opuesto a este ejemplo porque si le informa a la clase principal sobre la subclase, puede solucionar este problema. Eso es cierto si hay un número suficientemente pequeño de subclases y si el padre las conoce todas. Pero la pregunta original era sobre hacer una API para la cual alguien más escribirá subclases. En ese caso, generalmente no puede actualizar la implementación principal para que sea compatible con las subclases.
Prima
Comparator también es interesante porque soluciona el problema de implementar equals () correctamente. Mejor aún, sigue un patrón para solucionar este tipo de problema de herencia: el patrón de diseño de la Estrategia. Las clases de tipo que entusiasman a la gente de Haskell y Scala también son el patrón de estrategia. La herencia no es mala o incorrecta, solo es complicada. Para leer más, consulte el artículo de Philip Wadler Cómo hacer que el polimorfismo ad-hoc sea menos ad-hoc.
fuente
equals
cómo Map y Set lo definen. Igualdad ignora por completo el orden, con el efecto de que, por ejemplo, dos SortedSets con los mismos elementos pero diferentes órdenes de clasificación todavía se comparan igual.B
es hija de una claseA
y debe crear una instancia de un objeto de una claseB
, debe poder lanzar elB
objeto de clase a su padre y usar la API de la variable convertida sin perder ningún detalle de implementación de el niño.Rompiste la regla al proporcionar la tercera propiedad. ¿Cómo planea acceder a laz
coordenada después de convertir laPoint3D
variablePoint2D
, cuando la clase base no tiene idea de que existe tal propiedad? Si al lanzar una clase secundaria a su base, se rompe la API pública, su abstracción es incorrecta.La herencia debilita la encapsulación
Cuando publica una interfaz con la herencia permitida, aumenta sustancialmente el tamaño de su interfaz. Cada método reemplazable podría ser reemplazado y, por lo tanto, debe considerarse como una devolución de llamada proporcionada al constructor. La implementación proporcionada por su clase es simplemente el valor predeterminado de la devolución de llamada. Por lo tanto, se debe proporcionar algún tipo de contrato que indique cuáles son las expectativas sobre el método. Esto rara vez ocurre y es una de las principales razones por las cuales el código orientado a objetos se llama quebradizo.
A continuación se muestra un ejemplo real (simplificado) del marco de colecciones de Java, cortesía de Peter Norvig ( http://norvig.com/java-iaq.html ).
Entonces, ¿qué sucede si subclasificamos esto?
Tenemos un error: ocasionalmente agregamos "dog" y la tabla hash obtiene una entrada para "dogss". La causa fue alguien que proporcionó una implementación de put que la persona que diseñó la clase Hashtable no esperaba.
La herencia rompe la extensibilidad
Si permite que su clase se subclasifique, se compromete a no agregar ningún método a su clase. De lo contrario, esto podría hacerse sin romper nada.
Cuando agrega nuevos métodos a una interfaz, cualquier persona que haya heredado de su clase deberá implementar esos métodos.
fuente
Si se pretende llamar a un método, solo debe asegurarse de que funcione correctamente. Eso es. Hecho.
Si un método está diseñado para ser anulado, también debe pensar cuidadosamente sobre el alcance del método: si el alcance es demasiado grande, la clase secundaria a menudo necesitará incluir código copiado del método principal; Si es demasiado pequeño, será necesario anular muchos métodos para tener la nueva funcionalidad deseada; esto agrega complejidad y un recuento de líneas innecesario.
Por lo tanto, el creador del método padre debe hacer suposiciones sobre cómo la clase y sus métodos podrían anularse en el futuro.
Sin embargo, el autor habla sobre un tema diferente en el texto citado:
Considere el método
a
que normalmente se llama desde el métodob
, pero en algunos casos raros y no obvios desde el métodoc
. Si el autor del método de anulación pasa por alto elc
método y sus expectativas sobrea
, es obvio cómo las cosas pueden salir mal.Por lo tanto, es más importante que
a
se defina clara e inequívocamente, bien documentado, "hace una cosa y lo hace bien", más que si fuera un método diseñado únicamente para ser llamado.fuente