java - ¿Por qué insistir en que todas las implementaciones de una interfaz extiendan una clase base?
inheritance interface (7)
Pero si la intención es que todas las clases que implementen Matcher extiendan BaseMatcher, ¿por qué usar una interfaz? ¿Por qué no simplemente hacer de Matcher una clase abstracta en primer lugar y hacer que todos los demás lo extiendan?
Al separar la interfaz y la implementación (la clase abstracta sigue siendo una implementación), usted cumple con el Principio de Inversión de Dependencia . No confundas con la inyección de dependencia, nada en común. Puede notar que, en la interfaz de Hamcrest, se mantiene el paquete hamcrest-api, mientras que la clase abstracta está en hamcrest-core. Esto proporciona un bajo acoplamiento, porque la implementación solo depende de las interfaces, pero no de otras implementaciones. Un buen libro sobre este tema es: Diseño orientado a la interfaz: con patrones .
¿Hay alguna ventaja de hacerlo de la forma en que Hamcrest lo ha hecho? ¿O es este un gran ejemplo de mala práctica?
La solución en este ejemplo parece fea. Creo que el comentario es suficiente. Hacer tales métodos de stub es redundante. No seguiría este enfoque.
Estaba mirando el código de Java Hamcrest en GitHub, y me di cuenta de que empleaban una estrategia que parecía poco intuitiva e incómoda, pero me hizo preguntarme si me estaba perdiendo algo.
Noté en la API de HamCrest que hay una interfaz Matcher y una clase abstracta BaseMatcher . La interfaz de Matcher declara este método, con este javadoc:
/**
* This method simply acts a friendly reminder not to implement Matcher directly and
* instead extend BaseMatcher. It''s easy to ignore JavaDoc, but a bit harder to ignore
* compile errors .
*
* @see Matcher for reasons why.
* @see BaseMatcher
* @deprecated to make
*/
@Deprecated
void _dont_implement_Matcher___instead_extend_BaseMatcher_();
Luego, en BaseMatcher, este método se implementa de la siguiente manera:
/**
* @see Matcher#_dont_implement_Matcher___instead_extend_BaseMatcher_()
*/
@Override
@Deprecated
public final void _dont_implement_Matcher___instead_extend_BaseMatcher_() {
// See Matcher interface for an explanation of this method.
}
Es cierto que esto es efectivo y lindo (e increíblemente incómodo). Pero si la intención es que todas las clases que implementen Matcher extiendan BaseMatcher, ¿por qué usar una interfaz? ¿Por qué no simplemente hacer de Matcher una clase abstracta en primer lugar y hacer que todos los demás lo extiendan? ¿Hay alguna ventaja de hacerlo de la forma en que Hamcrest lo ha hecho? ¿O es este un gran ejemplo de mala práctica?
EDITAR
Algunas buenas respuestas, pero en busca de más detalles estoy ofreciendo una recompensa. Creo que el problema de la compatibilidad con versiones anteriores / binarias es la mejor respuesta. Sin embargo, me gustaría ver el problema de compatibilidad elaborado en más, idealmente con algunos ejemplos de código (preferiblemente en Java). Además, ¿hay algún matiz entre la compatibilidad "hacia atrás" y la compatibilidad "binaria"?
MÁS EDITACIÓN
7 de enero de 2014 - pigroxalot proporcionó una respuesta a continuación, vinculándose a este comentario en Reddit por los autores de HamCrest. Animo a todos a que lo lean, y si les parece informativo, responden mejor que pigroxalot.
INCLUSO MÁS EDITACIÓN
12 de diciembre de 2017 - la respuesta de pigroxalot fue eliminada de alguna manera, no estoy seguro de cómo sucedió eso. Es una lástima ... ese simple enlace fue muy informativo.
Pero si la intención es que todas las clases que implementen Matcher extiendan BaseMatcher, ¿por qué usar una interfaz?
No es exactamente el intento. Las clases base y las interfaces abstractas proporcionan ''contratos'' completamente diferentes desde una perspectiva de POO.
Una interfaz es un contrato de comunicación . Una clase implementa una interfaz para indicar al mundo que se adhiere a ciertos estándares de comunicación, y dará un tipo específico de resultado en respuesta a una llamada específica con parámetros específicos.
Una clase base abstracta es un contrato de implementación . Las clases base abstractas son heredadas por una clase para proporcionar la funcionalidad que requiere la clase base, pero que el implementador debe proporcionar.
En este caso, ambos se superponen, pero esto es simplemente una cuestión de conveniencia: la interfaz es lo que necesita implementar, y la clase abstracta está ahí para facilitar la implementación de la interfaz. No hay ningún requisito para usar esa clase base para ser capaz de ofrecer la interfaz, solo está ahí para que funcione menos. De ninguna manera se limita a extender la clase base para sus propios fines, sin preocuparse por el contrato de interfaz o al implementar una clase personalizada que implemente la misma interfaz.
La práctica dada es en realidad bastante común en el código COM / OLE de la vieja escuela y otros marcos que facilitan las comunicaciones entre procesos (IPC), donde se vuelve fundamental separar la implementación de la interfaz, que es exactamente lo que se hace aquí.
Creo que lo que sucedió es que inicialmente se creó una API de Matcher en forma de interfaz.
Luego, al implementar la interfaz de varias maneras, se descubrió una base de código común que luego se refactorizó en la clase BaseMatcher.
Así que supongo que la interfaz de Matcher se retuvo, ya que es parte de la API inicial y el método descriptivo se agregó como recordatorio.
Después de buscar en el código, encontré que la interfaz podría eliminarse fácilmente, ya que SOLO está implementado por BaseMatcher y en 2 unidades de prueba que podrían cambiarse fácilmente para usar BaseMatcher.
Entonces, para responder a su pregunta, en este caso particular, no hay ninguna ventaja de hacerlo de esta manera, además de no romper las implementaciones de otras personas de Matcher.
En cuanto a la mala práctica? En mi opinión, es claro y efectivo, así que no, no lo creo, solo un poco extraño :-)
El git log
tiene esta entrada, desde diciembre de 2006 (aproximadamente 9 meses después del git log
inicial):
Se agregó una clase abstracta BaseMatcher que todos los Matchers deberían extender. Esto permite la futura compatibilidad API [sic] a medida que evoluciona la interfaz de Matcher.
No he intentado descifrar los detalles. Pero mantener la compatibilidad y la continuidad a medida que un sistema evoluciona es un problema difícil. Significa que a veces terminas con un diseño que nunca, nunca, hubieras creado si hubieras diseñado todo desde cero.
Hamcrest proporciona emparejamiento y solo coincidencia. Es un pequeño nicho de mercado, pero parece que lo está haciendo bien. Las implementaciones de esta interfaz de Matcher están dispersas en un par de bibliotecas de pruebas unitarias, como por ejemplo ArgumentMatcher de Mockito y en una gran cantidad de pequeñas implementaciones anónimas de copiar y pegar en pruebas unitarias.
Quieren poder extender el Matcher con un nuevo método sin romper todas las clases de implementación existentes. Serían un infierno para mejorar. Imagínense de repente teniendo todas sus clases unittest mostrando enojados errores de compilación en rojo. La ira y la molestia matarían el nicho de mercado de Hamcrest de un solo golpe. Visita http://code.google.com/p/hamcrest/issues/detail?id=83 para ver una pequeña muestra de eso. Además, un cambio radical en Hamcrest dividiría todas las versiones de bibliotecas que usan Hamcrest en antes y después del cambio y las haría mutuamente excluyentes. De nuevo, un escenario infernal. Entonces, para mantener cierta libertad, necesitan que Matcher sea una clase base abstracta.
Pero también están en el negocio de burlarse, y las interfaces son mucho más fáciles de burlar que las clases base. Cuando la unidad Mockito folks prueba a Mockito, deberían poder burlarse del matcher. Entonces también necesitan que la clase base abstracta tenga una interfaz de Matcher.
Creo que han considerado seriamente las opciones y encontraron que esta es la alternativa menos mala.
Hay una discusión interesante al respecto aquí . Para citar nat_pryce:
Hola. Escribí la versión original de Hamcrest, aunque Joe Walnes agregó este extraño método a la clase base.
La razón es debido a una peculiaridad del lenguaje Java. Como comentaba más abajo, definir a Matcher como una clase base facilitaría la extensión de la biblioteca sin romper clientes. Agregar un método a una interfaz evita que se compile cualquier clase de implementación en el código del cliente, pero se pueden agregar nuevos métodos concretos a una clase base abstracta sin romper las subclases.
Sin embargo, hay funciones de Java que solo funcionan con interfaces, en particular java.lang.reflect.Proxy.
Por lo tanto, definimos la interfaz de Matcher para que las personas pudieran escribir implementaciones dinámicas de Matcher. Y proporcionamos la clase base para que las personas amplíen su propio código para que su código no se rompa a medida que añadimos más métodos a la interfaz.
Desde entonces, hemos agregado el método describeMismatch a la interfaz de Matcher y el código del cliente heredó una implementación predeterminada sin romper. También proporcionamos clases base adicionales que facilitan la implementación de describeMismatch sin duplicar la lógica.
Entonces, este es un ejemplo de por qué no se puede seguir ciegamente algunas "mejores prácticas" genéricas cuando se trata de diseño. Debe comprender las herramientas que está utilizando y realizar intercambios de ingeniería dentro de ese contexto.
EDITAR: separar la interfaz de la clase base también ayuda a lidiar con el problema de la clase base frágil:
Si agrega métodos a una interfaz implementada por una clase base abstracta, puede terminar con una lógica duplicada en la clase base o en las subclases cuando se modifiquen para implementar el nuevo método. No puede cambiar la clase base para eliminar esa lógica duplicada si al hacerlo cambia la API provista a las subclases, porque eso romperá todas las subclases, no es un gran problema si la interfaz y las implementaciones están todas en la misma base de código pero malas noticias si re un autor de la biblioteca.
Si la interfaz es independiente de la clase base abstracta, es decir, si distingue entre usuarios del tipo e implementadores del tipo, cuando agrega métodos a la interfaz puede agregar una implementación predeterminada a la clase base que no lo hará. romper las subclases existentes e introducir una nueva clase base que proporcione una mejor implementación parcial para nuevas subclases. Cuando alguien viene a cambiar las subclases existentes para implementar el método de una mejor manera, puede elegir usar la nueva clase base para reducir la lógica duplicada si tiene sentido hacerlo.
Si la interfaz y la clase base son del mismo tipo (como algunos lo han sugerido en este hilo) y quiere introducir múltiples clases base de esta manera, está atascado. No puede introducir un nuevo supertipo para actuar como interfaz, ya que eso romperá el código del cliente. No puede mover la implementación parcial por la jerarquía de tipos a una nueva clase base abstracta porque eso romperá las subclases existentes.
Esto se aplica tanto a los rasgos como las interfaces y clases de estilo Java o la herencia múltiple de C ++.
Java8 ahora permite agregar nuevos métodos a una interfaz si contienen implementaciones predeterminadas.
interface Match<T>
default void newMethod(){ impl... }
esta es una gran herramienta que nos da mucha libertad en el diseño y la evolución de la interfaz.
Sin embargo, ¿qué sucede si realmente desea agregar un método abstracto que no tiene una implementación predeterminada?
Creo que deberías seguir y agregar el método. Romperá algunos códigos existentes; y tendrán que ser arreglados. no realmente un gran acuerdo. Probablemente sea mejor que otras soluciones que conservan la compatibilidad binaria a costa de arruinar todo el diseño.