web-dev-qa-db-fra.com

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

Dernièrement, j'ai pris l'habitude de "masquer" Java collections avec des noms de classe 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'est une mauvaise pratique, et introduit un retard/latence, ainsi qu'un anti-pattern OO. Je peux le comprendre en introduisant un très petit degré de performance supplémentaire, mais je ne peux pas imaginer que ce soit significatif. Alors je demande: est-ce bon ou mauvais à faire, et pourquoi?

46
herpylderp

Décalage/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, inhiber les optimisations effectuées par la machine virtuelle HotSpot. Je n'en sais pas assez sur VM implémentation pour confirmer ou infirmer cela. Je basais mon commentaire sur l'implémentation C++ des fonctions virtuelles.)

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

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

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

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

Ce sont tous des points à considérer. Ce peut être ou non 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.

75
Sebastian Redl

Selon IBM c'est en fait un anti-pattern. Ces classes de type "typedef" sont appelées types de pseudo.

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 WidgetCache ne peut pas gérer un Map<String, Widget>
  • Ces pseudotypes sont "viraux" lorsqu'ils utilisent plusieurs packages, ils conduisent à 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 n'implémentent que la version générique.

Dans l'article, ils proposent l'astuce suivante pour vous faciliter la vie 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 en raison de l'inférence de type automatique.

(Je suis venu à cette réponse via this question liée au débordement de pile)

25
Roy T.

La perte de performances serait limitée au maximum à une recherche vtable, que vous subissez probablement déjà. Ce n'est pas une raison valable pour s'y opposer.

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

Les typedefs présentent un certain nombre d'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 refactorisation. Pensez à trouver partout où un WidgetCache est utilisé par rapport à la recherche de ces utilisations spécifiques d'un Map. Ils sont plus faciles à modifier, par exemple si vous trouvez plus tard que vous avez besoin d'un LinkedHashMap à la place, ou même de votre propre conteneur personnalisé.

13
Karl Bielefeldt

Je vous suggère, comme d'autres l'ont déjà mentionné, d'utiliser la composition plutôt que l'héritage, afin de ne pouvoir exposer que les méthodes qui sont vraiment nécessaires, avec des noms qui correspondent au cas d'utilisation prévu. Les utilisateurs de votre classe ont-ils vraiment besoin de savoir que WidgetCache est une carte? Et pouvoir en faire ce qu'ils veulent? Ou ils ont juste besoin de savoir qu'il s'agit d'un cache pour les widgets?

Exemple de classe de ma base de code, avec solution à un 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 le montre pas. Et il a des méthodes "programmables" comme appendMessage(Locale locale, String code, String message) pour un moyen plus simple 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 Translations n'étend pas Map.

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

11
user11153

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

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

  2. Elle n'est aussi complexe que nécessaire.

En étendant, vous exposez l'intégralité de l'interface du parent, mais dans de nombreux cas, une grande partie de cela peut être mieux cachée, donc vous voudriez faire ce que Sebastian Redl suggère et privilégier la composition à l'héritage et ajoutez une instance du parent en tant que membre privé de votre classe personnalisée. Toutes les méthodes d'interface qui ont un sens pour votre abstraction peuvent être facilement déléguées à (dans votre cas) la collection interne.

En ce qui concerne un impact sur les performances, c'est toujours une bonne idée d'optimiser d'abord le code pour la lisibilité, et si un impact sur les performances est suspecté, profilez le code pour comparer les deux implémentations.

6
Mike Partridge

+1 aux autres réponses ici. J'ajouterai également qu'il est en fait considéré comme une très bonne pratique par la communauté DDD (Domain Driven Design). Ils préconisent que votre domaine et les interactions avec lui aient une signification de domaine sémantique par opposition à la structure de données sous-jacente. UNE Map<String, Widget> pourrait être un cache, mais cela pourrait aussi être autre chose, ce que vous avez correctement fait dans My Not So Humble Opinion (IMNSHO) est de modéliser ce que la collection représente, dans ce cas un cache.

J'ajouterai une modification importante en ce que le wrapper de classe de domaine autour de la structure de données sous-jacente devrait probablement également avoir d'autres variables ou fonctions membres qui en font vraiment une classe de domaine avec des interactions plutôt qu'une simple structure de données (si seulement Java avait des types de valeur, nous les obtiendrons dans Java 10 - promis!)

Il sera intéressant de voir quel impact Java 8 auront sur tout cela, je peux imaginer que certaines interfaces publiques préféreront peut-être traiter un flux de (insert common Java primitive ou String) par opposition à un objet Java.

4
Martijn Verburg