Vaut-il mieux renvoyer une ImmutableMap ou une Map?

90

Disons que j'écris une méthode qui devrait renvoyer une Map . Par exemple:

public Map<String, Integer> foo() {
  return new HashMap<String, Integer>();
}

Après y avoir réfléchi pendant un moment, j'ai décidé qu'il n'y avait aucune raison de modifier cette carte une fois qu'elle est créée. Ainsi, je voudrais retourner un ImmutableMap .

public Map<String, Integer> foo() {
  return ImmutableMap.of();
}

Dois-je laisser le type de retour comme une carte générique, ou dois-je spécifier que je retourne une ImmutableMap?

D'un côté, c'est exactement pourquoi les interfaces ont été créées pour; pour masquer les détails d'implémentation.
D'un autre côté, si je laisse ça comme ça, d'autres développeurs pourraient manquer le fait que cet objet est immuable. Ainsi, je n'atteindrai pas un objectif majeur des objets immuables; pour rendre le code plus clair en minimisant le nombre d'objets qui peuvent changer. Pire encore, après un certain temps, quelqu'un pourrait essayer de changer cet objet, ce qui entraînera une erreur d'exécution (le compilateur ne l'avertira pas).

AMT
la source
2
Je soupçonne que quelqu'un finira par marquer cette question comme étant trop ouverte pour les arguments. Pouvez-vous poser des questions plus spécifiques au lieu de «WDYT»? et "est-ce que ça va ...?" Par exemple, "y a-t-il des problèmes ou des considérations liés au renvoi d'une carte normale?" et "quelles alternatives existent?"
ash
Et si plus tard vous décidiez qu'il devrait réellement renvoyer une carte mutable?
user253751
3
@immibis lol, ou une liste d'ailleurs?
djechlin le
3
Voulez-vous vraiment intégrer une dépendance Guava dans votre API? Vous ne pourrez pas changer cela sans rompre la compatibilité descendante.
user2357112 prend en charge Monica le
1
Je pense que la question est trop sommaire et que de nombreux détails importants manquent. Par exemple, si vous avez déjà Guava en tant que dépendance (visible) de toute façon. Même si vous l'avez déjà, les extraits de code sont pesudocode et ne communiquent pas ce que vous voulez réellement réaliser. Vous auriez certainement pas écrire return ImmutableMap.of(somethingElse). Au lieu de cela, vous stockerez le ImmutableMap.of(somethingElse)sous forme de champ et ne renverriez que ce champ. Tout cela influence le design ici.
Marco13

Réponses:

70
  • Si vous écrivez une API publique et que l'immuabilité est un aspect important de votre conception, je le rendrais certainement explicite soit en faisant en sorte que le nom de la méthode indique clairement que la carte retournée sera immuable ou en renvoyant le type concret de la carte. Le mentionner dans le javadoc ne suffit pas à mon avis.

    Puisque vous utilisez apparemment l'implémentation de Guava, j'ai regardé le document et c'est une classe abstraite donc cela vous donne un peu de flexibilité sur le type concret réel.

  • Si vous écrivez un outil / une bibliothèque interne, il devient beaucoup plus acceptable de simplement renvoyer un simple Map. Les gens connaîtront les composants internes du code qu'ils appellent ou du moins y auront facilement accès.

Ma conclusion serait qu'explicite est une bonne chose, ne laissez pas les choses au hasard.

Dici
la source
1
Une interface supplémentaire, une classe abstraite supplémentaire et une classe qui étend cette classe abstraite. C'est trop compliqué pour une chose aussi simple que l'OP demande, à savoir si la méthode doit renvoyer le Maptype d'interface ou le ImmutableMaptype d'implémentation.
fps
20
Si vous faites référence à Guava ImmutableMap, le conseil de l'équipe Guava est d'envisager ImmutableMapune interface avec des garanties sémantiques, et que vous devriez retourner ce type directement.
Louis Wasserman
1
@LouisWasserman mm, je n'ai pas vu que la question donnait un lien vers l'implémentation de Guava. Je supprimerai l'extrait de code alors, merci
Dici
3
Je suis d'accord avec Louis, si vous avez l'intention de retourner une carte qui est un objet immuable, assurez-vous que l'objet de retour est de préférence de ce type. Si, pour une raison quelconque, vous souhaitez masquer le fait qu'il s'agit d'un type immuable, définissez votre propre interface. Le laisser comme Map est trompeur.
WSimpson
1
@WSimpson Je crois que c'est ce que dit ma réponse. Louis parlait d'un extrait que j'avais ajouté, mais je l'ai supprimé.
Dici
38

Vous devriez avoir ImmutableMapcomme type de retour. Mapcontient des méthodes qui ne sont pas prises en charge par l'implémentation de ImmutableMap(par exemple put) et qui sont marquées @deprecateddans ImmutableMap.

L'utilisation de méthodes obsolètes entraînera un avertissement du compilateur et la plupart des IDE avertiront lorsque des personnes tenteront d'utiliser les méthodes obsolètes.

Cet avertissement avancé est préférable aux exceptions d'exécution comme premier indice que quelque chose ne va pas.

Synesso
la source
1
C'est à mon avis la réponse la plus sensée. L'interface Map est un contrat et ImmutableMap en enfreint clairement une partie importante. Utiliser Map comme type de retour dans ce cas n'a pas de sens.
TonioElGringo
@TonioElGringo et Collections.immutableMapalors?
OrangeDog le
@TonioElGringo veuillez noter que les méthodes que l'ImmutableMap n'implémente pas sont explicitement marquées comme facultatives dans la documentation de l'interface docs.oracle.com/javase/7/docs/api/java/util/… . Ainsi, je pense qu'il peut toujours être judicieux de renvoyer une carte (avec les javadocs appropriés). Même si, je choisis de renvoyer l'ImmutableMap explicitement, pour les raisons évoquées ci-dessus.
AMT
3
@TonioElGringo il ne viole rien, ces méthodes sont facultatives. Comme dans List, il n'y a aucune garantie List::addvalable. Essayez Arrays.asList("a", "b").add("c");. Il n'y a aucune indication qu'il échouera à l'exécution, mais c'est le cas. Au moins, Guava vous avertit.
njzk2
14

D'un autre côté, si je laisse ça comme ça, d'autres développeurs pourraient manquer le fait que cet objet est immuable.

Vous devriez le mentionner dans les javadocs. Les développeurs les lisent, vous savez.

Ainsi, je n'atteindrai pas un objectif majeur des objets immuables; pour rendre le code plus clair en minimisant le nombre d'objets qui peuvent changer. Pire encore, après un certain temps, quelqu'un pourrait essayer de changer cet objet, ce qui entraînera une erreur d'exécution (le compilateur ne l'avertira pas).

Aucun développeur ne publie son code non testé. Et quand il le teste, il obtient une exception lancée où il voit non seulement la raison, mais aussi le fichier et la ligne où il a essayé d'écrire sur une carte immuable.

Notez cependant que seul le Maplui - même sera immuable, pas les objets qu'il contient.

tkausl
la source
10
"Aucun développeur ne publie son code sans avoir été testé." Voulez-vous parier là-dessus? Il y aurait beaucoup moins (ma présomption) de bogues dans les logiciels publics, si cette phrase était vraie. Je ne serais même pas sûr que "la plupart des développeurs ..." serait vrai. Et oui, c'est décevant.
Tom
1
D'accord, Tom a raison, je ne suis pas sûr de ce que vous essayez de dire, mais ce que vous avez réellement écrit est clairement faux. (Aussi: coup de
pouce
@Tom Je suppose que vous avez manqué le sarcasme dans cette réponse
Captain Fogetti
10

si je laisse ça comme ça, d'autres développeurs pourraient manquer le fait que cet objet est immuable

C'est vrai, mais les autres développeurs devraient tester leur code et s'assurer qu'il est couvert.

Néanmoins, vous avez 2 autres options pour résoudre ce problème:

  • Utiliser Javadoc

    @return a immutable map
  • Choisissez un nom de méthode descriptif

    public Map<String, Integer> getImmutableMap()
    public Map<String, Integer> getUnmodifiableEntries()

    Pour un cas d'utilisation concret, vous pouvez même mieux nommer les méthodes. Par exemple

    public Map<String, Integer> getUnmodifiableCountByWords()

Que pouvez vous faire d'autre?!

Vous pouvez retourner un

  • copie

    private Map<String, Integer> myMap;
    
    public Map<String, Integer> foo() {
      return new HashMap<String, Integer>(myMap);
    }

    Cette approche doit être utilisée si vous prévoyez que de nombreux clients modifieront la carte et tant que la carte ne contient que quelques entrées.

  • CopyOnWriteMap

    Les collections copy on write sont généralement utilisées lorsque vous devez gérer la
    concurrence. Mais le concept vous aidera également dans votre situation, car un CopyOnWriteMap crée une copie de la structure de données interne sur une opération mutative (par exemple, ajouter, supprimer).

    Dans ce cas, vous avez besoin d'un wrapper fin autour de votre carte qui délègue tous les appels de méthode à la carte sous-jacente, à l'exception des opérations mutatives. Si une opération mutative est appelée, elle crée une copie de la carte sous-jacente et toutes les autres invocations seront déléguées à cette copie.

    Cette approche doit être utilisée si vous prévoyez que certains clients modifieront la carte.

    Malheureusement, java n'a pas un tel fichier CopyOnWriteMap. Mais vous pouvez trouver un tiers ou le mettre en œuvre vous-même.

Enfin, vous devez garder à l'esprit que les éléments de votre carte peuvent encore être modifiables.

René Link
la source
8

Renvoyez définitivement un ImmutableMap, la justification étant:

  • La signature de la méthode (y compris le type de retour) doit être auto-documentée. Les commentaires sont comme le service client: si vos clients ont besoin de compter sur eux, alors votre produit principal est défectueux.
  • Que quelque chose soit une interface ou une classe n'est pertinent que lors de son extension ou de son implémentation. Étant donné une instance (objet), 99% du temps, le code client ne saura pas ou ne se souciera pas si quelque chose est une interface ou une classe. J'ai d'abord supposé qu'ImmutableMap était une interface. Ce n'est qu'après avoir cliqué sur le lien que j'ai réalisé que c'était une classe.
Benito Ciaro
la source
5

Cela dépend de la classe elle-même. Guava ImmutableMapn'est pas destiné à être une vue immuable dans une classe mutable. Si votre classe est immuable et a une structure qui est essentiellement an ImmutableMap, alors créez le type de retour ImmutableMap. Cependant, si votre classe est modifiable, ne le faites pas. Si vous avez ceci:

public ImmutableMap<String, Integer> foo() {
    return ImmutableMap.copyOf(internalMap);
}

Guava copiera la carte à chaque fois. C'est lent. Mais si internalMapc'était déjà un ImmutableMap, alors tout va bien.

Si vous ne limitez pas votre classe au retour ImmutableMap, vous pouvez à la place revenir Collections.unmodifiableMapcomme suit:

public Map<String, Integer> foo() {
    return Collections.unmodifiableMap(internalMap);
}

Notez qu'il s'agit d'une vue immuable de la carte. En cas de internalMapchangement, une copie en cache de Collections.unmodifiableMap(internalMap). Cependant, je le préfère toujours pour les getters.

Justin
la source
1
Ce sont de bons points, mais par souci d'exhaustivité, j'aime cette réponse qui explique que le unmodifiableMapn'est modifiable que par le détenteur de la référence - c'est une vue et le détenteur de la backing map peut modifier la backing map puis la vue change. C'est donc une considération importante.
davidbak le
@Comme l'a commenté davidback, soyez très prudent lorsque vous utilisez Collections.unmodifiableMap. Cette méthode renvoie une vue sur la carte d'origine plutôt que de créer un nouvel objet de carte distinct et distinct. Ainsi, tout autre code référençant la carte originale peut la modifier, et alors le détenteur de la carte supposée non modifiable apprend à la dure que la carte peut en effet être modifiée à partir d'eux. J'ai moi-même été mordu par ce fait. J'ai appris à renvoyer une copie de la carte ou à utiliser Guava ImmutableMap.
Basil Bourque
5

Cela ne répond pas à la question exacte, mais cela vaut toujours la peine de se demander si une carte doit être retournée. Si la carte est immuable, la méthode principale à fournir est basée sur get (key):

public Integer fooOf(String key) {
    return map.get(key);
}

Cela rend l'API beaucoup plus stricte. Si une carte est réellement requise, cela pourrait être laissé au client de l'API en fournissant un flux d'entrées:

public Stream<Map.Entry<String, Integer>> foos() {
    map.entrySet().stream()
}

Ensuite, le client peut créer sa propre carte immuable ou mutable selon ses besoins, ou ajouter les entrées à sa propre carte. Si le client a besoin de savoir si la valeur existe, facultatif peut être renvoyé à la place:

public Optional<Integer> fooOf(String key) {
    return Optional.ofNullable(map.get(key));
}
Solubris
la source
-1

La carte immuable est un type de carte. Donc, laisser le type de retour de Map est correct.

Pour s'assurer que les utilisateurs ne modifient pas l'objet retourné, la documentation de la méthode peut décrire les caractéristiques de l'objet retourné.

toro
la source
-1

C'est sans doute une question d'opinion, mais la meilleure idée ici est d'utiliser une interface pour la classe map. Cette interface n'a pas besoin de dire explicitement qu'elle est immuable, mais le message sera le même si vous n'exposez aucune des méthodes setter de la classe parent dans l'interface.

Jetez un œil à l'article suivant:

Andy Gibson

WSimpson
la source
1
Les emballages inutiles sont considérés comme nocifs.
djechlin le
Vous avez raison, je n'utiliserais pas de wrapper ici non plus, car l'interface est suffisante.
WSimpson
J'ai le sentiment que si c'était la bonne chose à faire, ImmutableMap implémenterait déjà une ImmutableMapInterface, mais je ne comprends pas cela techniquement.
djechlin le
Le point n'est pas ce que les développeurs de Guava voulaient, c'est le fait que ce développeur aimerait encapsuler l'architecture et ne pas exposer l'utilisation d'ImmutableMap. Une façon de faire serait d'utiliser une interface. L'utilisation d'un wrapper comme vous l'avez souligné n'est pas nécessaire et donc déconseillée. Le retour de la carte n'est pas souhaitable car il est trompeur. Il semble donc que si l'objectif est l'encapsulation, alors une interface est la meilleure option.
WSimpson
L'inconvénient de renvoyer quelque chose qui n'étend pas (ou n'implémente) pas l' Mapinterface est que vous ne pouvez pas le transmettre à une fonction API qui attend un Mapsans le lancer. Cela inclut toutes sortes de constructeurs de copie d'autres types de cartes. En plus de cela, si la mutabilité est uniquement «masquée» par l'interface, vos utilisateurs peuvent toujours contourner ce problème en castant et en cassant votre code de cette façon.
Hulk