Bonne ou mauvaise pratique pour masquer les collections Java avec des noms de classe significatifs?

46

Dernièrement, j'ai l'habitude de "masquer" les collections Java avec des noms de classes conviviaux. Quelques exemples simples:

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

Ou:

// 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 collègue m'a fait remarquer que c'était une mauvaise pratique et introduisait un décalage / latence, tout en étant un anti-pattern OO. Je peux comprendre que cela introduise un très faible degré de performance, mais je ne peux pas imaginer que cela soit significatif. Je demande donc: est-ce bon ou mauvais de le faire et pourquoi?

herpylderp
la source
11
C'est beaucoup plus simple que ça. C'est une mauvaise pratique car j'imagine que vous étendez les implémentations de ces Java Basic JDK Collection. En Java, vous ne pouvez étendre qu'une classe. Vous devez donc penser et concevoir davantage lorsque vous avez une extension. En Java, utilisez extend avec parcimonie.
InformedA
ChangeListla compilation se cassera à extends, car List est une interface, nécessite implements. @randomA ce que vous imaginez manque le but à cause de cette erreur
gnat
@gnat Il ne manque pas le point, je suppose qu'il étendait un implementationie HashMapou ou TreeMapet qu'il y avait une faute de frappe.
InformedA
4
C'est une mauvaise pratique. MAUVAIS MAUVAIS MAUVAIS. Ne fais pas ça. Tout le monde sait ce qu'est une carte <String, Widget>. Mais un WidgetCache? Maintenant, je dois ouvrir WidgetCache.java, je dois me rappeler que WidgetCache est juste une carte. Je dois vérifier à chaque nouvelle version que vous n'avez pas ajouté quelque chose de nouveau à WidgetCache. Dieu non, ne fais jamais ça.
Miles Rout
"Si vous voyiez une ArrayList <ArrayList <?> Être transmise dans le code, feriez-vous fuir en hurlant ...?" Non, je suis assez à l'aise avec les collections génériques imbriquées. Et vous devriez l'être aussi.
Kevin Krumwiede

Réponses:

75

Lag / latence? J'appelle BS à ce sujet. Il devrait y avoir exactement zéro frais généraux de cette pratique. ( Edit: il a été souligné dans les commentaires que cela peut en fait empêcher les optimisations effectuées par la machine virtuelle HotSpot. Je ne connais pas suffisamment l'implémentation de la machine virtuelle pour confirmer ou infirmer cela. Je me suis basé mon commentaire sur C ++. implémentation de fonctions virtuelles.)

Il y a une surcharge de code. Vous devez créer tous les constructeurs de la classe de base souhaitée, en transmettant leurs paramètres.

Je ne vois pas non plus cela comme un anti-modèle, en soi. Cependant, je le vois comme une opportunité manquée. Au lieu de créer une classe qui dérive la classe de base uniquement pour renommer, pourquoi ne pas créer une classe contenant la collection et offrant une interface améliorée spécifique à la casse? Votre cache de widgets doit-il vraiment offrir l'interface complète d'une carte? Ou devrait-il plutôt offrir une interface spécialisée?

De plus, dans le cas de collections, le modèle ne fonctionne tout simplement pas avec la règle générale d’utilisation d’interfaces, et non d’implémentations - c’est-à-dire que dans le code de collection simple, vous créez un HashMap<String, Widget>, puis vous l’assignez à une variable de type Map<String, Widget>. Votre WidgetCachene peut pas s'étendre Map<String, Widget>, car c'est une interface. Il ne peut s'agir d'une interface qui étend l'interface de base, car elle HashMap<String, Widget>n'implémente pas cette interface, pas plus que toute autre collection standard. Et bien que vous puissiez en faire une classe qui s'étend HashMap<String, Widget>, vous devez ensuite déclarer les variables comme WidgetCacheou Map<String, Widget>, et le premier vous perd la flexibilité nécessaire pour substituer une collection différente (peut-être la collection de chargement paresseux de certains ORM), tandis que le second défait le point. d'avoir la classe.

Certains de ces contrepoints s'appliquent également à la classe spécialisée proposée.

Ce sont tous des points à considérer. Ce peut être ou ne pas être le bon choix. Dans les deux cas, les arguments proposés par votre collègue ne sont pas valides. S'il pense que c'est un anti-modèle, il devrait le nommer.

Sebastian Redl
la source
1
Notez que s'il est tout à fait possible d'avoir un impact sur les performances, cela ne va pas être aussi horrible (il manque quelques optimisations en ligne et obtient un appel supplémentaire dans le pire des cas - pas génial dans votre boucle la plus intérieure, mais sinon probablement très bien).
Voo le
5
Cette très bonne réponse indique à tout le monde "ne jugez pas la décision en fonction des coûts liés aux performances" - et la seule discussion ci-dessous est "comment HotSpot gère-t-il cela? Existe-t-il (dans 99,9% des cas) un impact non pertinent sur les performances" - les gars, avez-vous même pris la peine de lire plus que la première phrase?
Doc Brown
16
+1 pour "Au lieu de créer une classe qui dérive la classe de base juste pour renommer, pourquoi ne pas créer une classe contenant la collection et offrant une interface améliorée spécifique à la casse?"
user11153
6
Exemple pratique illustrant le point essentiel de cette réponse: La documentation de public class Properties extends Hashtable<Object,Object>in java.utilindique "Comme Properties hérite de Hashtable, les méthodes put et putAll peuvent être appliquées à un objet Properties. Leur utilisation est fortement déconseillée, car elles permettent à l'appelant d'insérer des entrées dont les clés ou les valeurs ne sont pas des chaînes. ". La composition aurait été beaucoup plus propre.
Patricia Shanahan
3
@DocBrown Non, cette réponse affirme qu'il n'y a aucun impact sur les performances. Si vous faites des déclarations fortes, vous ferez mieux de les soutenir, sinon les gens vous appelleront probablement à ce sujet. Le but des commentaires (sur les réponses) est de souligner des faits ou des hypothèses inexacts ou d'ajouter des notes utiles, mais certainement pas de féliciter les gens, c'est à cela que sert le système de vote.
Voo le
25

Selon IBM, il s’agit en réalité d’un anti-modèle. Ces classes comme 'typedef' sont appelées types psuedo.

L'article l'explique beaucoup mieux que moi, mais je vais essayer de le résumer au cas où le lien tomberait:

  • Tout code qui attend un WidgetCachene peut pas gérer unMap<String, Widget>
  • Ces pseudotypes sont "viraux" lorsqu'ils utilisent plusieurs packages, ils entraînent des incompatibilités, alors que le type de base (juste une carte idiote <...>) aurait fonctionné dans tous les cas, dans tous les packages.
  • Les pseudo-types sont souvent trop concrets, ils n'implémentent pas d'interfaces spécifiques car leurs classes de base implémentent uniquement la version générique.

Dans l'article, ils proposent l'astuce suivante pour rendre la vie plus facile sans utiliser de pseudo-types:

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

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

Ce qui fonctionne grâce à l'inférence de type automatique.

(Je suis venu à cette réponse via cette question connexe de débordement de pile)

Roy T.
la source
13
L’ newHashMapopérateur de diamants n’a-t-il pas résolu le problème maintenant?
svick
Absolument vrai, j'ai oublié ça. Je ne travaille pas normalement en Java.
Roy T.
Je souhaite que les langues permettent un univers de types de variables contenant des références à des instances d'autres types, mais pouvant néanmoins supporter la vérification au moment de la compilation, de telle sorte qu'une valeur de type WidgetCachepuisse être affectée à une variable de type WidgetCacheou Map<String,Widget>sans transt était un moyen par lequel une méthode statique in WidgetCachepouvait faire une référence à Map<String,Widget>et la renvoyer en tant que type WidgetCacheaprès avoir effectué la validation souhaitée. Une telle fonctionnalité pourrait être particulièrement utile avec les génériques, qui n'auraient pas à être effacés (...
supercat
... les types n'existeraient de toute façon que dans l'esprit du compilateur). Pour de nombreux types mutables, il existe deux types de champs de référence courants: un qui contient la seule référence à une instance pouvant être mutée ou un qui contient une référence partageable à une instance à laquelle personne ne peut muter. Il serait utile de pouvoir attribuer des noms différents à ces deux types de champs, car ils nécessitent des modèles d'utilisation différents, mais ils doivent tous deux contenir des références aux mêmes types d'instances d'objet.
Supercat
5
C'est un excellent argument pour expliquer pourquoi Java a besoin d'alias de types.
GlenPeterson
13

La performance atteinte serait tout au plus limitée à une recherche vtable, que vous êtes probablement déjà en train de subir. Ce n'est pas une raison valable pour s'y opposer.

La situation est suffisamment commune pour que la plupart des langages de programmation à typage statique aient une syntaxe spéciale pour les types de crénelage, généralement appelée un typedef. Java n'a probablement pas copié ceux-ci car à l'origine, il n'avait pas de types paramétrés. L'extension d'une classe n'est pas idéale, pour les raisons que Sebastian a si bien abordée dans sa réponse, mais cela peut constituer une solution de contournement raisonnable pour la syntaxe limitée de Java.

Les typedefs présentent de nombreux avantages. Ils expriment plus clairement l'intention du programmeur, avec un meilleur nom, à un niveau d'abstraction plus approprié. Ils sont plus faciles à rechercher à des fins de débogage ou de refactoring. Envisagez de trouver partout où a WidgetCacheest utilisé par opposition à trouver ces utilisations spécifiques de a Map. Ils sont plus faciles à modifier, par exemple si vous vous rendez compte par la suite que vous avez besoin d’un LinkedHashMapou de votre propre conteneur personnalisé.

Karl Bielefeldt
la source
Il y a aussi une minuscule surcharge dans les constructeurs.
Stephen C
11

Comme d’autres ont déjà mentionné, je vous suggère d’utiliser la composition plutôt que l’héritage, de sorte que vous ne puissiez exposer que les méthodes réellement nécessaires, avec des noms correspondant au cas d’utilisation souhaité. Les utilisateurs de votre classe ont-ils vraiment besoin de savoir qu’il WidgetCaches’agit d’une carte? Et être capable de faire avec tout ce qu'ils veulent? Ou ont-ils simplement besoin de savoir qu'il s'agit d'un cache pour les widgets?

Exemple de classe de ma base de code, avec la solution au problème similaire que vous avez décrit:

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 */
    }
}

Vous pouvez voir qu’en interne, c’est «juste une carte», mais l’interface publique ne l’affiche pas. Et il a des méthodes "conviviales pour les programmeurs" comme appendMessage(Locale locale, String code, String message)pour un moyen plus facile et plus significatif d'insérer de nouvelles entrées. Et les utilisateurs de classe ne peuvent pas faire, par exemple translations.clear(), parce que Translationsne s'étend pas Map.

Facultativement, vous pouvez toujours déléguer certaines des méthodes nécessaires à la carte utilisée en interne.

utilisateur11153
la source
6

Je vois cela comme un exemple d'abstraction significative. Une bonne abstraction a quelques traits:

  1. Il cache des détails d'implémentation qui ne sont pas pertinents pour le code qui le consomme.

  2. C'est aussi complexe que cela doit être.

En étendant, vous exposez toute l'interface du parent, mais dans de nombreux cas, une grande partie de cette information est peut-être mieux masquée. Vous voudrez donc faire ce que Sebastian Redl suggère, préférer la composition à l'héritage et ajouter une instance du parent comme un membre privé de votre classe personnalisée. Toutes les méthodes d'interface utiles pour votre abstraction peuvent facilement être déléguées (dans votre cas) à la collection interne.

En ce qui concerne l’impact sur les performances, il est toujours judicieux d’optimiser d’abord le code pour en améliorer la lisibilité. Si un impact sur les performances est suspecté, profilez le code afin de comparer les deux implémentations.

Mike Partridge
la source
4

+1 aux autres réponses ici. J'ajouterai également que c'est en fait une très bonne pratique de la part de la communauté DDD (Domain Driven Design). Ils préconisent que votre domaine et les interactions avec ce dernier aient une signification de domaine sémantique, par opposition à la structure de données sous-jacente. A Map<String, Widget>pourrait être un cache, mais cela pourrait aussi être autre chose. Ce que vous avez correctement fait dans Mon opinion pas si humble (IMNSHO) est de modéliser ce que la collection représente, dans ce cas un cache.

J'ajouterai une modification importante dans le fait que le wrapper de classe de domaine autour de la structure de données sous-jacente devrait probablement également contenir d'autres variables ou fonctions membres qui en font réellement une classe de domaine avec des interactions, par opposition à une structure de données uniquement (si seulement Java avait des types de valeur , nous les aurons en Java 10 - promis!)

Il sera intéressant de voir quel impact les flux de Java 8 auront sur tout cela. J'imagine que certaines interfaces publiques préféreront peut-être traiter un Stream de (insérer une primitive Java commune ou String) par opposition à un objet Java.

Martijn Verburg
la source