¿Buena o mala práctica para enmascarar colecciones de Java con nombres de clase significativos?

46

Últimamente he tenido la costumbre de "enmascarar" colecciones de Java con nombres de clase amigables para los humanos. Algunos ejemplos simples:

// Facade class that makes code more readable and understandable.
public class WidgetCache extends Map<String, Widget> {
}

O:

// If you saw a ArrayList<ArrayList<?>> being passed around in the code, would you
// run away screaming, or would you actually understand what it is and what
// it represents?
public class Changelist extends ArrayList<ArrayList<SomePOJO>> {
}

Un colega me señaló que esta es una mala práctica e introduce retraso / latencia, además de ser un anti-patrón OO. Puedo entender que introduce un nivel muy pequeño de sobrecarga de rendimiento, pero no puedo imaginar que sea significativo. Entonces pregunto: ¿es bueno o malo hacerlo y por qué?

herpylderp
fuente
11
Es mucho más simple que esto. Es una mala práctica porque imagino que está extendiendo las implementaciones de esos JDK Collection básicos de Java. En Java, solo puede extender una clase, por lo que debe pensar y diseñar más cuando tiene una extensión. En Java, use extender con moderación.
InformadoA
ChangeListla compilación se romperá extends, porque List es una interfaz, requiere implements. @randomA lo que estás imaginando pierde el punto debido a este error
mosquito
@gnat No se pierde el punto, supongo que estaba extendiendo un implementationie HashMapo TreeMapy lo que tenía allí era un error tipográfico.
InformadoA
44
Esta es una práctica MALA. MALO MALO MALO. No hagas esto. Todo el mundo sabe qué es un Mapa <String, Widget>. ¿Pero un WidgetCache? Ahora necesito abrir WidgetCache.java, debo recordar que WidgetCache es solo un mapa. Tengo que comprobar cada vez que aparece una nueva versión que no ha agregado algo nuevo a WidgetCache. Dios no, nunca hagas esto.
Miles Rout
"Si vieras una ArrayList <ArrayList <? >> que se pasa en el código, ¿huirías gritando ...?" No, me siento bastante cómodo con las colecciones genéricas anidadas. Y tú también deberías estarlo.
Kevin Krumwiede

Respuestas:

75

Retraso / Latencia? Llamo a BS por eso. Debe haber exactamente cero sobrecarga de esta práctica. ( Editar: se ha señalado en los comentarios que esto puede, de hecho, inhibir las optimizaciones realizadas por la VM de HotSpot. No sé lo suficiente sobre la implementación de VM para confirmar o negar esto. Estaba basando mi comentario en C ++ implementación de funciones virtuales).

Hay algo de código sobrecarga. Debe crear todos los constructores de la clase base que desee, reenviando sus parámetros.

Tampoco lo veo como un antipatrón, per se. Sin embargo, lo veo como una oportunidad perdida. En lugar de crear una clase que derive la clase base solo por cambiar el nombre, ¿qué tal si crea una clase que contenga la colección y ofrezca una interfaz mejorada específica para cada caso? ¿Debe su caché de widgets realmente ofrecer la interfaz completa de un mapa? ¿O debería ofrecer una interfaz especializada?

Además, en el caso de las colecciones, el patrón simplemente no funciona junto con la regla general de usar interfaces, no implementaciones, es decir, en un código de colección simple, crearía un HashMap<String, Widget>y luego lo asignaría a una variable de tipo Map<String, Widget>. No se WidgetCachepuede extender Map<String, Widget>, porque esa es una interfaz. No puede ser una interfaz que extienda la interfaz base, porque HashMap<String, Widget>no implementa esa interfaz, y tampoco lo hace ninguna otra colección estándar. Y si bien puede convertirlo en una clase que se extienda HashMap<String, Widget>, debe declarar las variables como WidgetCacheo Map<String, Widget>, y el primero le pierde la flexibilidad para sustituir una colección diferente (tal vez una colección de carga diferida de ORM), mientras que el segundo tipo de derrota el punto de tener la clase.

Algunos de estos contrapuntos también se aplican a mi clase especializada propuesta.

Estos son todos los puntos a considerar. Puede o no ser la elección correcta. En cualquier caso, los argumentos ofrecidos por su colega no son válidos. Si cree que es un antipatrón, debería nombrarlo.

Sebastian Redl
fuente
1
Sin embargo, tenga en cuenta que si bien es bastante posible tener algún impacto en el rendimiento, no va a ser tan horrible (perder algunas optimizaciones de línea y obtener una vcall adicional en el peor de los casos, no es excelente en su bucle más íntimo, pero por lo demás probablemente esté bien).
Voo
55
Esta muy buena respuesta les dice a todos "no juzguen la decisión por los gastos generales de rendimiento", y la única discusión a continuación es "¿cómo maneja HotSpot esto? ¿Hay algún impacto irrelevante en el rendimiento (en el 99.9% de todos los casos)? ¿incluso te molestaste en leer más que la primera oración?
Doc Brown
16
+1 para "En lugar de crear una clase que derive la clase base solo por cambiar el nombre, ¿qué tal si creas una clase que contenga la colección y ofrezca una interfaz mejorada específica para cada caso?"
user11153
66
Ejemplo práctico que ilustra el punto principal de esta respuesta: la documentación de public class Properties extends Hashtable<Object,Object>in java.utildice "Debido a que las propiedades heredan de Hashtable, los métodos put y putAll se pueden aplicar a un objeto Properties. Su uso se desaconseja en gran medida ya que permiten al llamante insertar entradas cuyas claves o los valores no son cadenas ". La composición habría sido mucho más limpia.
Patricia Shanahan
3
@DocBrown No, esta respuesta afirma que no hay ningún impacto en el rendimiento. Si haces declaraciones fuertes, será mejor que puedas apoyarlas; de lo contrario, las personas probablemente te denunciarán. El punto principal de los comentarios (sobre las respuestas) es señalar hechos o suposiciones inexactos o agregar notas útiles, pero ciertamente no felicitar a las personas, para eso es el sistema de votación.
Voo
25

Según IBM, esto en realidad es un antipatrón. Estas clases tipo 'typedef' se llaman tipos psuedo.

El artículo lo explica mucho mejor que yo, pero intentaré resumirlo en caso de que el enlace se caiga:

  • Cualquier código que espera un WidgetCacheno puede manejar unMap<String, Widget>
  • Estos pseudotipos son 'virales' cuando se usan múltiples paquetes que conducen a incompatibilidades, mientras que el tipo base (solo un tonto Mapa <...>) hubiera funcionado en todos los casos en todos los paquetes.
  • Los pseudo tipos suelen ser concretos, no implementan interfaces específicas porque sus clases base solo implementan la versión genérica.

En el artículo proponen el siguiente truco para hacer la vida más fácil sin usar pseudo tipos:

public static <K,V> Map<K,V> newHashMap() {
    return new HashMap<K,V>(); 
}

Map<Socket, Future<String>> socketOwner = Util.newHashMap();

Que funciona debido a la inferencia automática de tipos.

(Llegué a esta respuesta a través de esta pregunta de desbordamiento de pila relacionada)

Roy T.
fuente
13
¿No es la necesidad de ser newHashMapresuelta ahora por el operador de diamantes?
svick
Absolutamente cierto, se olvidó de eso. Normalmente no trabajo en Java.
Roy T.
Desearía que los idiomas permitieran un universo de tipos de variables que contuvieran referencias a instancias de otros tipos, pero que aún pudieran admitir la verificación en tiempo de compilación, de modo que se WidgetCachepudiera asignar un valor de tipo a una variable de tipo WidgetCacheo Map<String,Widget>sin una conversión, pero hay era un medio por el cual un método estático WidgetCachepodría hacer tomar una referencia Map<String,Widget>y devolverlo como tipo WidgetCachedespués de realizar la validación deseada. Tal característica podría ser especialmente útil con los genéricos, que no tendrían que ser borrados (desde ...
supercat
... los tipos existirían solo en la mente del compilador de todos modos). Para muchos tipos mutables, hay dos tipos comunes de campos de referencia: uno que contiene la única referencia a una instancia que puede estar mutada, o uno que contiene una referencia que se puede compartir a una instancia que nadie puede mutar. Sería útil poder dar diferentes nombres a esos dos tipos de campos, ya que requieren diferentes patrones de uso, pero ambos deben contener referencias a los mismos tipos de instancias de objetos.
supercat
55
Este es un gran argumento de por qué Java necesita alias de tipo.
GlenPeterson
13

El éxito en el rendimiento se limitaría a lo sumo a una búsqueda de vtable, en la que probablemente ya esté incurriendo. Esa no es una razón válida para oponerse.

La situación es lo suficientemente común como para que la mayoría de los lenguajes de programación de tipo estático tengan una sintaxis especial para los tipos de alias, generalmente llamados a typedef. Java probablemente no los copió porque originalmente no tenía tipos parametrizados. Ampliar una clase no es ideal, debido a las razones que Sebastian cubrió tan bien en su respuesta, pero puede ser una solución razonable para la sintaxis limitada de Java.

Typedefs tiene una serie de ventajas. Expresan la intención del programador más claramente, con un mejor nombre, a un nivel de abstracción más apropiado. Son más fáciles de buscar para propósitos de depuración o refactorización. Considere buscar en todas partes donde WidgetCachese usa a versus encontrar esos usos específicos de a Map. Son más fáciles de cambiar, por ejemplo, si luego encuentra que necesita un LinkedHashMaplugar, o incluso su propio contenedor personalizado.

Karl Bielefeldt
fuente
También hay una minúscula sobrecarga en los constructores.
Stephen C
11

Le sugiero, como ya lo mencionaron otros, que use la composición sobre la herencia, para que pueda exponer solo los métodos que realmente se necesitan, con nombres que coincidan con el caso de uso previsto. ¿Los usuarios de tu clase realmente necesitan saber que WidgetCachees un mapa? ¿Y poder hacer con él lo que quieran? ¿O simplemente necesitan saber que es un caché para widgets?

Clase de ejemplo de mi base de código, con solución a un problema similar que describió:

public class Translations {

    private Map<Locale, Properties> translations = new HashMap<>();

    public void appendMessage(Locale locale, String code, String message) {
        /* code */
    }

    public void addMessages(Locale locale, Properties messages) {
        /* code */
    }

    public String getMessage(Locale locale, String code) {
        /* code */
    }

    public boolean localeExists(Locale locale) {
        /* code */
    }
}

Puede ver que internamente es "solo un mapa", pero la interfaz pública no muestra esto. Y tiene métodos "amigables para el programador", como appendMessage(Locale locale, String code, String message)una forma más fácil y significativa de insertar nuevas entradas. Y los usuarios de clase no pueden hacerlo, por ejemplo translations.clear(), porque Translationsno se está extendiendo Map.

Opcionalmente, siempre puede delegar algunos de los métodos necesarios en el mapa utilizado internamente.

usuario11153
fuente
6

Veo esto como un ejemplo de una abstracción significativa. Una buena abstracción tiene un par de rasgos:

  1. Oculta detalles de implementación que son irrelevantes para el código que lo consume.

  2. Es tan complejo como debe ser.

Al extender, está exponiendo toda la interfaz del padre, pero en muchos casos es posible que gran parte de eso esté mejor oculto, por lo que querrá hacer lo que Sebastian Redl sugiere y favorecer la composición sobre la herencia y agregar una instancia del padre como un miembro privado de tu clase personalizada. Cualquiera de los métodos de interfaz que tengan sentido para su abstracción puede delegarse fácilmente (en su caso) en la colección interna.

En cuanto a un impacto en el rendimiento, siempre es una buena idea optimizar primero el código para facilitar la lectura, y si se sospecha un impacto en el rendimiento, perfile el código para comparar las dos implementaciones.

Mike Partridge
fuente
4

+1 a las otras respuestas aquí. También agregaré que en realidad la comunidad DDD (Domain Driven Design) considera que es una práctica muy buena. Defienden que su dominio y las interacciones con él deben tener un significado de dominio semántico en lugar de la estructura de datos subyacente. A Map<String, Widget>podría ser un caché, pero también podría ser otra cosa, lo que has hecho correctamente en My Not So Humble Opinion (IMNSHO) es modelar lo que representa la colección, en este caso un caché.

Agregaré una edición importante en la que el contenedor de clase de dominio alrededor de la estructura de datos subyacente probablemente también debería tener otras variables o funciones miembro que realmente la conviertan en una clase de dominio con interacciones en lugar de solo una estructura de datos (si solo Java tuviera tipos de valor , los conseguiremos en Java 10 - ¡lo prometo!)

Será interesante ver qué impacto tendrán las secuencias de Java 8 en todo esto, me imagino que quizás algunas interfaces públicas prefieran tratar con una secuencia de (insertar primitiva o cadena común de Java) en lugar de un objeto de Java.

Martijn Verburg
fuente