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

43

Últimamente he tenido la costumbre de "enmascarar" las colecciones de Java con nombres de clase amigables con 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 antipatrón OO. Puedo entenderlo introduciendo un muy pequeño grado de sobrecarga de rendimiento, pero no puedo imaginar que sea significativo. Entonces pregunto: ¿es esto bueno o malo, y por qué?

    
pregunta herpylderp 27.06.2014 - 20:48

6 respuestas

73

Lag / Latencia? Llamo a BS sobre 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 el HotSpot VM. No sé lo suficiente sobre la implementación de VM para confirmar o negar esto. Estaba basando mi comentario en la implementación de C ++ de funciones virtuales.)

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

Tampoco lo veo como un anti-patró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 contiene la colección y ofrece una interfaz mejorada, específica para cada caso? ¿Debería tu 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 uso de interfaces, no implementaciones, es decir, en el 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> . Su WidgetCache no puede extender Map<String, Widget> , porque eso es una interfaz. No puede ser una interfaz que amplíe 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 convertirla en una clase que extiende HashMap<String, Widget> , entonces tiene que declarar las variables como WidgetCache o Map<String, Widget> , y la primera le pierde la flexibilidad para sustituir una colección diferente (tal vez alguna colección de carga lenta 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 él piensa que es un anti-patrón, debería nombrarlo.

    
respondido por el Sebastian Redl 27.06.2014 - 20:58
23

Según IBM , en realidad esto es un antipatrón. Estas clases '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 espere un WidgetCache no puede manejar un Map<String, Widget>
  • Estos pseudotipos son 'virales' cuando se usan varios paquetes, lo que lleva a incompatibilidades, mientras que el tipo base (solo un mapa tonto < ... >) funcionó en todos los casos en todos los paquetes.
  • Los pseudo tipos a menudo son 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 utilizar pseudo tipos:

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

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

Lo que funciona debido a la inferencia de tipos automática.

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

    
respondido por el Roy T. 28.06.2014 - 10:37
13

El impacto en el rendimiento se limitaría, como máximo, a una búsqueda vtable, en la que es muy probable que ya estés 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 tipificados estáticamente tengan una sintaxis especial para los tipos de alias, generalmente llamados typedef . Probablemente Java no las copió porque originalmente no tenía tipos parametrizados. Extender una clase no es lo 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.

Las definiciones de tipo tienen varias ventajas. Expresan la intención del programador más claramente, con un nombre mejor, a un nivel más apropiado de abstracción. Son más fáciles de buscar para fines de depuración o refactorización. Considere encontrar en todas partes que se use WidgetCache en lugar de encontrar esos usos específicos de Map . Son más fáciles de cambiar, por ejemplo, si luego encuentra que necesita LinkedHashMap en su lugar, o incluso su propio contenedor personalizado.

    
respondido por el Karl Bielefeldt 27.06.2014 - 21:36
11

Le sugiero, como otros ya han mencionado, 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 su clase realmente necesitan saber que WidgetCache es un mapa? ¿Y poder hacer con eso lo que quieran? ¿O solo necesitan saber que es un caché para widgets?

Ejemplo de clase de mi código base, con solución a un problema similar que usted 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 */
    }
}

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

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

    
respondido por el user11153 28.06.2014 - 17:30
6

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

  1. Oculta los 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 la interfaz completa del padre, pero en muchos casos, mucho de eso puede estar mejor oculto, por lo que querría hacer lo que sugiere Sebastian Redl y favorezca la composición sobre la herencia y agregue una instancia del padre como miembro privado de su clase personalizada. Cualquiera de los métodos de interfaz que tengan sentido para su abstracción se pueden delegar fácilmente (en su caso) a la colección interna.

En cuanto al impacto en el rendimiento, siempre es una buena idea optimizar 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.

    
respondido por el Mike Partridge 27.06.2014 - 21:37
4

+1 a las otras respuestas aquí. También agregaré que la comunidad de Diseño Dirigido por Dominio (DDD, por sus siglas en inglés) lo considera una buena práctica. Abogan por que su dominio y las interacciones con él deban tener un significado de dominio semántico en oposición a la estructura de datos subyacente. Un Map<String, Widget> podría ser un caché, pero también podría ser otra cosa, lo que has hecho correctamente en Mi opinión no tan humilde (IMNSHO) es modelar lo que representa la colección, en este caso un caché.

Agregaré una edición importante porque la envoltura de la clase de dominio en torno a la estructura de datos subyacente probablemente también tenga 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 es Java teníamos tipos de valor, los obtendremos en Java 10 - ¡promesa!)

Será interesante ver qué impacto tendrán las secuencias de Java 8 en todo esto, puedo imaginar que quizás algunas interfaces públicas preferirán tratar con una Corriente de (insertar primitiva de Java común o Cadena) en lugar de una Java objeto.

    
respondido por el Martijn Verburg 27.06.2014 - 21:59

Lea otras preguntas en las etiquetas