Java Generics: cómo lograr un equilibrio entre expresividad y simplicidad

8

Estoy desarrollando un código que utiliza genéricos, y uno de mis principios rectores fue hacerlo utilizable para escenarios futuros, y no solo para los de hoy. Sin embargo, varios compañeros de trabajo han expresado que podría haber cambiado la legibilidad en aras de la extensibilidad. Quería reunir algunos comentarios sobre posibles formas de resolver esto.

Para ser específicos, aquí hay una interfaz que define una transformación: comienza con una colección de elementos de origen y aplica la transformación a cada elemento, almacenando los resultados en una colección de destino. Además, quiero poder devolver la colección de destino a la persona que llama, y ​​en lugar de obligarlos a usar una referencia de Colección, quiero que puedan usar el tipo de colección que realmente proporcionaron para la colección de destino.

Finalmente, hago posible que el tipo de elementos en la colección de destino sea diferente del tipo de elementos en la colección de origen, porque tal vez eso es lo que hace la transformación. En mi código, por ejemplo, varios elementos de origen forman un elemento de destino después de la transformación.

Esto produce la siguiente interfaz:

interface Transform<Src, Dst> {
    <DstColl extends Collection<? super Dst>> DstColl transform(
                Collection<? extends Src> sourceCollection,
                DstColl                   destinationCollection);
}

Traté de ser amable y aplicar el principio PECS de Josh Bloch (productor se extiende, consumidor súper) para asegurarme de que la interfaz sea utilizable con súper y subtipos cuando sea apropiado. El resultado final es algo monstruoso.

Ahora, hubiera sido bueno si pudiera extender esta interfaz y especializarla de alguna manera. Por ejemplo, si realmente no me importa jugar bien con los subtipos de los elementos de origen y los supertipos de los elementos de destino, podría tener:

interface SimpleTransform<Src, Dst> {
    <DstColl extends Collection<Dst>> DstColl transform(
                  Collection<Src> sourceCollection,
                  DstColl         destinationCollection);
}

Pero no hay forma de hacerlo en Java. Quiero hacer que las implementaciones de esta interfaz sean algo que otros realmente considerarían hacer, en lugar de correr con miedo. He considerado varias opciones:

  • No devuelva la colección de destino. Parece extraño dado que haces una transformación pero no recuperas nada.
  • Tenga una clase abstracta que implemente esta interfaz, pero luego traduzca los parámetros a algo más fácil de usar y llame a otro método "translateImpl ()" que tenga la firma más simple y, por lo tanto, presente una carga cognitiva menor para los implementadores. Pero entonces es extraño tener que escribir una clase abstracta solo para hacer que una interfaz sea fácil de usar.
  • Olvídese de la extensibilidad y solo tenga la interfaz más simple. Posiblemente junte eso con no devolver la colección de destino. Pero eso limita mis opciones en el futuro.

¿Qué piensas? ¿Me estoy perdiendo un enfoque que podría usar?

RuslanD
fuente
2
Intentar predecir un escenario futuro en lugar de conocer el caso de uso no es una buena idea. Aquí es donde está en juego el principio KISS .
lorus
1
Además del principio KISS, también existe el principio YAGNI que sugiere que no debes hacer eso.
Frank
@lorus: mi idea era tener una interfaz de "transformación" que pudiera usar para una variedad de transformaciones, en lugar de un caso específico que se adapta a una combinación específica de tipos de elementos de origen y destino. Una forma de resumir mi publicación es "¿cómo puedo mantenerlo simple de una manera que no requiera una revisión significativa en el futuro". Estamos en un entorno corporativo típico donde los plazos son los reyes y no puedo simplemente refactorizar las cosas a menos que haya una razón comercial sólida. La misma preocupación se aplica a YAGNI en el comentario de Frank.
RuslanD
En lugar de Collection<? extends Src> srcCollection, deberías usarIterable<? extends Src>
kevin cline

Respuestas:

3

Guava Collections ya ofrece esta funcionalidad y si puede esperar un poco más, Java 8 también lo hará :-). FWIW Creo que tu uso de ? extends Tes el idioma correcto, no hay mucho que puedas hacer dado que Java usa Type Erasure

Martijn Verburg
fuente
Desafortunadamente, es poco probable que pueda convencer a mi equipo y a mi gerencia para que salten directamente de Java 6 a Java 8 :) Mientras tanto, ¿podría aclarar qué quiere decir con "Guava Collections ya ofrece esta funcionalidad"? ¿Estás hablando de transformar colecciones? Un puntero a un recurso relevante sería muy útil. ¡Gracias!
RuslanD
Estoy confundido en cuanto a por qué la eliminación es importante aquí. ¿Estás pensando en qué C # x.0 entra y sale parámetros genéricos? / El uso Collection<? super Dst>sería ideal aquí, si no también devolviera el mismo tipo (presumiblemente la misma referencia, lo cual no es realmente una gran idea (creo que Java SE 8 lo hará para algunos métodos ( into))). Es interesante notar que los gustos de Collections.fillusar comodines innecesariamente para ser más expresivos. / IIRC, Java SE 6 finalmente deja de ser compatible este mes.
Tom Hawtin - tackline
1
La guayaba es code.google.com/p/guava-libraries
MatrixFrog
2
+1 para Guava: es excelente y debería ser parte del kit de herramientas de cada desarrollador de Java (también Joda Time y Joda Money)
Gary Rowe
0

Personalmente, no veo muchos problemas con la forma en que ha definido su interfaz, sin embargo, estoy acostumbrado a jugar con los genéricos y puedo entender que sus compañeros de trabajo encontrarán que su código es un poco bocado si ese no es su caso.

Si yo fuera usted, elegiría la tercera solución que establezca: simplificar las cosas a expensas de tener que volver más adelante. Porque después de todo, tal vez no tengas que hacerlo al final; o terminaría siendo capaz de usarlo completamente en algún momento, pero no lo suficiente para compensar el esfuerzo que ha puesto en hacer que esta interfaz sea tan genérica, así como el esfuerzo que sus compañeros de trabajo pueden haber hecho para entenderlo. eso.

Otra cosa a tener en cuenta es la frecuencia con la que utiliza esta interfaz y en qué nivel de complejidad. Además, cuán útil resulta esta interfaz en sus proyectos. Si permite una alta reutilización de algunos componentes (y no solo potenciales, sino reales), es algo bueno. Si una interfaz mucho más simple funciona tan bien el 90% del tiempo, podría preguntarse si el 10% restante sería útil o no con una interfaz compleja.

No creo que sea una muy buena idea no usarlo supery extend, aunque por el momento puede prescindir de ellos, estoy seguro de que a sus colegas no les importará verlos desaparecer. Sin embargo, ¿realmente tiene tantas situaciones en las que también necesita cambiar el tipo Collection?

Algunos consejos ya dados son realmente buenos, y estoy de acuerdo con Frank en seguir el principio de YAGNI a menos que tenga muy buenas razones para no hacerlo. Todavía puede cambiar el código cuando surja la necesidad de una mayor complejidad, pero no es de mucha utilidad desarrollar cosas que no está seguro de que se usarán pronto. Además, el consejo de Martijn de usar guayaba es algo a considerar en serio. Todavía no he tenido la oportunidad de usarlo, pero he escuchado muchas cosas buenas al respecto, incluso en una presentación en la que se discutió el patrón Transformer (puede verlo en línea en InfoQ si está interesado).

Por otro lado, ¿no sería mejor en su interfaz actual tener destinationCollectionun tipo Class<DstColl>?

KevinLH
fuente
0

Me gusta envolver colecciones de Java. La encapsulación adecuada realmente puede ayudar. Este patrón necesita estar relacionado según el tipo, pero deja en gran medida cada línea de código sin saber el tipo de colección.

Por ejemplo, suponga que hay una lista de productos. La clase de producto sería inmutable y tendría algunos métodos de utilidad. La clase de lista de productos puede agregarle un producto u otra lista. Si desea ejecutar un método en toda la lista, sería en la clase de lista de productos. Hay un método que tomaría una clase de lista de filtros y daría una nueva lista de productos con los filtrados. El filtrado se eliminaría del original.

Hay una interfaz de filtro que decide si UN producto pasa el filtro. Habría una clase de lista de filtros que tiene una lista de filtros e implementa la interfaz al requerir que TODOS los filtros pasen un producto para que el producto pase.

Lo curioso es que puedo apilar y cambiarlo a una lista vinculada sin cambios notables como este. Puede hacer bucles y hacer condicionales muy notorios y simples. Por lo tanto, viste un código imperativo y agrega flexibilidad. Y está bien orgonizado.

Akash Patel
fuente
¿Te das cuenta de que puedes hacer todo eso en unas pocas líneas de código con colecciones estándar de Java, streams y lambdas? Tener una "clase de lista de productos" especial sería una pérdida de tiempo realmente horrible que no crea más que dolores de cabeza de mantenimiento.
Michael Borgwardt