¿Por qué se implementaron las colecciones Java con "métodos opcionales" en la interfaz?

69

Durante mi primera implementación que extendió el marco de la colección Java, me sorprendió ver que la interfaz de la colección contiene métodos declarados como opcionales. Se espera que el implementador arroje UnsupportedOperationExceptions si no es compatible. Esto me pareció inmediatamente una mala elección de diseño de API.

Después de leer gran parte del excelente libro "Eficaz Java" de Joshua Bloch, y luego de enterarse de que él podría ser responsable de estas decisiones, no parecía coincidir con los principios expuestos en el libro. Creo que declarar dos interfaces: Collection y MutableCollection, que amplía Collection con los métodos "opcionales", habría llevado a un código de cliente mucho más fácil de mantener.

Hay un excelente resumen de los problemas aquí .

¿Hubo una buena razón por la cual se eligieron métodos opcionales en lugar de la implementación de dos interfaces?

glenviewjeff
fuente
OMI, no hay una buena razón. El C ++ STL fue creado por Stepanov a principios de los años 80. Aunque su usabilidad estaba limitada por la incómoda sintaxis de la plantilla C ++, es un modelo de consistencia y usabilidad en comparación con las clases de colección de Java.
Kevin Cline
Hubo un 'portado' STL de C ++ a Java. Pero fue 5 veces más grande en el recuento de clases. Con esto en mente, no creo que el más grande sea más consistente y utilizable. docs.oracle.com/javase/6/docs/technotes/guides/collections/…
m3th0dman
@ m3th0dman Es mucho más grande en Java porque Java no tiene nada equivalente en potencia a las plantillas de C ++.
Kevin Cline
Tal vez es solo un estilo extraño que he desarrollado, pero mi código tiende a tratar todas las colecciones como "solo lectura", (más precisamente, algo que cuenta o sobre el que itera) excepto los pocos métodos que realmente crean las colecciones. Probablemente una buena práctica de todos modos (especialmente por concurrencia). Y los métodos opcionales de los que muchos se quejan nunca han sido un problema real para mí. Las pesadillas genéricas con "super" y "extendidas" tampoco han sido (mucho) un problema. ¿Me pregunto si otros usan esta práctica general?
user949300

Respuestas:

28

Las preguntas frecuentes proporcionan la respuesta. En resumen, vieron una potencial explosión combinatoria de interfaces necesarias con vista modificable, no modificable, solo eliminación, solo adición, longitud fija, inmutable (para subprocesos), y así sucesivamente para cada conjunto posible de métodos de opciones implementados.

monstruo de trinquete
fuente
66
Todo esto se habría evitado si Java tuviera una constpalabra clave como C ++.
Etienne de Martel
@ Etienne: Mejor serían metaclases como Python. Entonces podría construir programáticamente el número combinatorio de interfaces. El problema con const es que sólo le da una o dos dimensiones: vector<int>, const vector<int>, vector<const int>, const vector<const int>. Hasta aquí todo bien, pero luego intenta implementar gráficos, y desea hacer la estructura gráfica constante, pero los atributos del nodo modificable, etc.
Neil G
2
@ Etienne, ¡otra razón más para subir "Aprender Scala" en mi lista de tareas pendientes!
glenviewjeff
2
Lo que no entiendo es, ¿por qué no hicieron un canmétodo que probaría si una operación es posible? Mantendría la interfaz simple y rápida.
Mehrdad
3
@Etienne de Martel Tonterías. ¿Cómo ayudaría eso en la explosión combinatoria?
Tom Hawtin - tackline
10

Me parece que Interface Segregation Principleno estaba tan bien explorado en aquel entonces como lo está ahora; esa forma de hacer las cosas (es decir, su interfaz incluye todas las operaciones posibles y tiene métodos "degenerados" que arrojan excepciones para los que no necesita) era popular antes de que SOLID e ISP se convirtieran en el estándar de facto para el código de calidad.

Wayne Molina
fuente
2
¿Por qué un voto negativo? ¿A alguien no le gusta el ISP?
Wayne Molina
También vale la pena señalar que en los marcos que admiten la varianza, existe una GRAN ventaja para segregar aquellos aspectos de una interfaz que pueden ser covariantes o contravariantes de aquellos que son fundamentalmente invariantes. Incluso en ausencia de dicho soporte, en los marcos que no usan borrado de tipo, sería valioso segregar los aspectos de una interfaz que son independientes del tipo (por ejemplo, permitir obtener Countuna colección sin tener que preocuparse por los tipos de elementos que contiene), pero en marcos basados ​​en borrado de tipos como Java, ese no es un problema.
supercat
4

Si bien algunas personas pueden detestar los "métodos opcionales", en muchos casos pueden ofrecer una semántica mejor que las interfaces altamente segregadas. Entre otras cosas, permiten las posibilidades de que un objeto gane habilidades o características durante su vida útil, o que un objeto (especialmente un objeto envolvente) no sepa cuándo se construye qué habilidades exactas debería reportar.

Si bien difícilmente llamaré a las clases de colecciones de Java modelos de buen diseño, sugeriría que un buen marco de colecciones debería incluir en su base una gran cantidad de métodos opcionales junto con formas de preguntarle a una colección sobre sus características y habilidades . Tal diseño permitirá que se use una sola clase de envoltura con una gran variedad de colecciones sin oscurecer accidentalmente las habilidades que la colección subyacente podría poseer. Si los métodos no fueran opcionales, sería necesario tener una clase de contenedor diferente para cada combinación de características que las colecciones podrían admitir, o de lo contrario, algunos contenedores no podrán utilizarse en algunas situaciones.

Por ejemplo, si una colección admite escribir un elemento por índice o agregar elementos al final, pero no admite la inserción de elementos en el medio, entonces el código que desea encapsularlo en un contenedor que registraría todas las acciones realizadas en él necesitaría una versión del contenedor de registro que proporcionó la combinación exacta de habilidades compatibles, o si no hubiera ninguno disponible, tendría que usar un contenedor que admitiera agregar o escribir por índice, pero no ambos. Sin embargo, si una interfaz de colección unificada proporciona los tres métodos como "opcionales", pero luego incluye métodos para indicar cuál de los métodos opcionales sería utilizable, entonces una sola clase de contenedor podría manejar colecciones que implementan cualquier combinación de características. Cuando se le preguntó qué características admite, un contenedor podría simplemente informar lo que sea compatible con la colección encapsulada.

Tenga en cuenta que la existencia de "habilidades opcionales" puede permitir en algunos casos que las colecciones agregadas implementen ciertas funciones de manera mucho más eficiente de lo que sería posible si las habilidades se definieran por la existencia de implementaciones. Por ejemplo, suponga concatenateque se usó un método para formar una colección compuesta de otras dos, la primera de las cuales resultó ser una ArrayList con 1,000,000 de elementos y la última de las cuales fue una colección de veinte elementos que solo se pudo repetir desde el principio. Si a la colección compuesta se le pidiera el elemento 1,000,013 (índice 1,000,012), podría preguntarle a la ArrayList cuántos elementos contenía (es decir, 1,000,000), restar eso del índice solicitado (arrojando 12), leer y omitir doce elementos del segundo colección, y luego devuelve el siguiente elemento.

En tal situación, a pesar de que la colección compuesta no tendría una forma instantánea de devolver un artículo por índice, pedirle a la colección compuesta el 1,000,00013 ítem aún sería mucho más rápido que leer 1,000,013 artículos de forma individual e ignorar todos menos el último uno.

Super gato
fuente
@kevincline: ¿No está de acuerdo con la frase citada? Consideraría que el diseño de los medios por los cuales las implementaciones de interfaz pueden describir sus características y habilidades esenciales es uno de los aspectos más importantes del diseño de la interfaz, a pesar de que a menudo no recibe mucha atención. Si las diferentes implementaciones de una interfaz tendrán diferentes formas óptimas de lograr ciertas tareas comunes, debería haber un medio para que los clientes que se preocupan por el rendimiento seleccionen el mejor enfoque en escenarios donde sería importante.
supercat
1
lo siento, comentario incompleto Estaba a punto de decir que "'formas de preguntar sobre una colección' ..." mueve lo que debería ser una verificación en tiempo de compilación a tiempo de ejecución.
Kevin Cline
@kevincline: en los casos en que un cliente necesita tener cierta capacidad y no puede vivir sin ella, las comprobaciones en tiempo de compilación pueden ser útiles. Por otro lado, hay muchas situaciones en las que las colecciones podrían tener habilidades más allá de las que se puede garantizar que tengan en tiempo de compilación. En algunos casos, puede tener sentido tener una interfaz derivada cuyo contrato especifique que todas las implementaciones legítimas de la interfaz derivada deben admitir métodos particulares que son opcionales en la interfaz base, pero en los casos en que el código podrá manejar algo independientemente de si tiene alguna característica ...
supercat
... pero se beneficiará de la función que existe, es mejor que el código pregunte al objeto si es compatible con la función que al sistema de tipos si el tipo de objeto promete admitir la función. Si se va a utilizar ampliamente una interfaz "específica" particular, se podría incluir un AsXXXmétodo en la interfaz base que devolverá el objeto sobre el que se invoca si implementa esa interfaz, devolverá un objeto contenedor que admita esa interfaz si es posible, o arroje una excepción si no. Por ejemplo, una ImmutableCollectioninterfaz puede requerir por contrato ...
supercat
2
Hay un problema con su propuesta: un patrón de "preguntar y hacer" tiene un problema de concurrencia si las capacidades de un objeto pueden cambiar durante su vida útil. (Si va a tener que arriesgar una excepción de todos modos, es mejor que no se moleste en preguntar ...)
jhominal
-1

Lo atribuiría a los desarrolladores originales simplemente porque no sabían mejor en ese entonces. Hemos recorrido un largo camino en el diseño OO desde 1998 más o menos cuando se lanzaron Java 2 y Colecciones por primera vez. Lo que parece un mal diseño obvio ahora no era tan obvio en los primeros días de OOP.

Pero puede haberse hecho para evitar el lanzamiento adicional. Si se tratara de una segunda interfaz, tendría que emitir sus instancias de colecciones para llamar a esos métodos opcionales, que también es algo feo. Tal como está ahora, detectará una UnsupportedOperationException de inmediato y corregirá su código. Pero si hubiera dos interfaces, tendría que usar instanceof y emitir por todas partes. Quizás lo consideraron una compensación válida. También en los primeros días de Java 2, instancia de era muy mal visto debido a su lento rendimiento, podrían haber estado tratando de evitar el uso excesivo de la misma.

Por supuesto, todo esto es una especulación salvaje, dudo que podamos responder esto con seguridad a menos que uno de los arquitectos de las colecciones originales intervenga.

Jberg
fuente
77
¿Qué casting? Si un método le devuelve un Collectiony no un MutableCollection, es una clara señal de que no debe modificarse. No sé por qué alguien necesitaría lanzarlos. Tener interfaces separadas significa que obtendrá ese tipo de errores en tiempo de compilación en lugar de obtener una excepción en tiempo de ejecución. Cuanto antes obtenga el error, mejor.
Etienne de Martel
1
Debido a que es mucho menos flexible, uno de los mayores beneficios para la biblioteca de recopilación es que puede devolver las interfaces de alto nivel a todas partes y no preocuparse por la implementación real. Si está utilizando dos interfaces, ahora está más estrechamente acoplado. En la mayoría de los casos, simplemente querría devolver List, y no ImmutableList porque generalmente desea dejarlo a la clase que llama para que lo determine.
Jberg
66
Si le doy una colección de solo lectura, es porque no quiero que se modifique. Lanzar una excepción y confiar en la documentación se parece mucho a un parche. En C ++, simplemente devolvería un constobjeto e instantáneamente le diría al usuario que el objeto no puede modificarse.
Etienne de Martel
77
1998 no fue "los primeros días del diseño OO".
quant_dev