2
votes

Meilleures pratiques de type de retour covariant

Je me souviens vaguement avoir appris à l'université que le type de retour d'une méthode doit toujours être aussi restreint que possible, mais ma recherche de références en ligne est restée vide et SonarQube appelle cela une odeur de code. Par exemple. dans l'exemple suivant (notez que TreeSet et Set ne sont que des exemples)

Set<Number> allNumbers = integerGetter.getAll();

SonarQube me dit

Les déclarations doivent utiliser des interfaces de collecte Java telles que "List" plutôt que des classes d'implémentation spécifiques telles que "LinkedList". Le but de l'API Java Collections est de fournir une hiérarchie d'interfaces bien définie afin de masquer les détails d'implémentation. (calmar: S1319)

Je vois l'intérêt de cacher les détails de l'implémentation, car je ne peux pas facilement changer le type de retour de IntegerGetter :: getAll () sans rompre la rétrocompatibilité. Mais ce faisant, je fournis également aux consommateurs des informations potentiellement précieuses, c'est-à-dire qu'ils pourraient changer leur algorithme pour être plus approprié pour l'utilisation d'un TreeSet . Et s'ils ne se soucient pas de cette propriété, ils peuvent toujours simplement utiliser IntegerGetter (quelle que soit la manière dont ils l'ont obtenu), comme ceci:

public interface NumberGetter {
    Number get();

    Set<Number> getAll();
}

public class IntegerGetter implements NumberGetter {

    @Override
    public Integer get() {
        return 1;
    }

    @Override
    public TreeSet<Number> getAll() {
        return new TreeSet<>(Collections.singleton(1));
    }
}

Donc J'ai les questions suivantes:

  • Est-il approprié pour IntegerGetter :: get () de renvoyer un type plus étroit?
  • Est-il approprié pour IntegerGetter :: getAll () de renvoyer un type plus étroit?
  • Existe-t-il des bonnes pratiques concernant ce sujet ou la réponse est-elle simplement "ça dépend"?

(NB SonarQube ne se plaint pas si je remplace TreeSet par exemple par SortedSet . Ce code ne sent-il que le fait de ne pas utiliser une interface API Collection? s'il n'y a qu'une classe concrète pour mon problème particulier?)


2 commentaires

S'ils souhaitent utiliser un algorithme spécialisé, ils peuvent utiliser instanceof . En général, SonarQube recommande la bonne pratique correcte: utilisez toujours une interface de collecte appropriée chaque fois que possible.


C'est vraiment une question de jugement. Il y a un argument selon lequel vous devez coder sur une interface, donc retourner un Set signifie que vous pouvez apporter des modifications plus tard et que vous n'êtes pas en quelque sorte vissé. D'un autre côté, si vous utilisez le getter et que vous devez continuer à travailler avec un TreeSet, cela fait vraiment partie de votre interface. Si vous n'utilisez les méthodes Set qu'après avoir appelé le getter, vous pouvez renvoyer Set en toute sécurité. Vous pouvez toujours le rendre plus précis si vous avez une raison de le faire.


3 Réponses :


1
votes

J'essaie - en règle générale - d'utiliser sur la signature de méthode le type le plus général (la classe ou l'interface n'a pas d'importance) qui prend en charge l'API dont j'ai besoin: plus général le type , plus l'API est minimale. Donc, si j'ai besoin d'un paramètre représentant une famille d'objets du même type, je commence à utiliser Collection ; si dans le scénario spécifique l'idée de commander est importante, j'utilise List , en évitant de publier des informations sur l'implémentation spécifique de List que j'utilise en interne : mon idée est de conserver la possibilité de modifier l'implémentation (peut être pour l'optimisation des performances, pour prendre en charge une structure de données différente, etc.) sans interrompre les clients.

Comme vous l'avez dit, publier des informations telles que J'utilise un TreSet peut laisser la place à l'optimisation côté client - mais mon idée est que cela dépend : au cas par cas, vous pouvez évaluer si le scénario spécifique nécessite d'assouplir la règle générale pour exposer l'interface plus générale que vous pouvez .

Pour en venir à vos questions:

  1. oui, il convient dans l'implémentation IntegerGetter de l'interface NumberGetter de renvoyer un type plus étroit: Java vous permet de le faire et vous ne cassez pas mon plus règle générique est plus belle : l'interface NumberGetter expose l'interface plus générale en utilisant Number comme type de retour, mais dans des implémentations spécifiques, nous pouvons utiliser un type de retour plus étroit pour guider l'implémentation de la méthode: les clients se référant à l'interface la plus abstraite ne sont pas affectés par ce choix et les clients se référant à la sous-classe spécifique peuvent essayer d'utiliser l'interface plus concrète
  2. le même que le point précédent, mais je pense que c'est moins utile que dans le cas précédent pour les clients: peut-être qu'un client peut trouver utile de se référer à Integer plutôt qu'à Number (si j'utilise explicitement un NumberGetter , peut-être je pense en termes de Integer s, pas en terme de Number s), mais se référer à TreeSet plutôt qu'à Set n'est utile que si vous avez besoin de l'API exposée par la sous-classe et non par l'interface ...
  3. voir la thèse initiale

C'est une question quasi philosophique - et ma réponse l'est aussi: j'espère que cela pourra vous être utile!


0 commentaires

0
votes

Le type de retour doit trouver un équilibre entre les besoins de l'appelant et les besoins de l'implémentation: plus vous indiquez à l'appelant le type de retour, plus il est difficile de changer ce type plus tard.

Ce qui est le plus important dépendra des circonstances spécifiques. Avez-vous déjà vu ce type changer? Quelle est la valeur de la connaissance du type de l'appelant?

Dans le cas de IntegerGetter.get () , il serait très surprenant que le type de retour change un jour, alors dire à l'appelant ne fait aucun mal.

Dans le cas de IntegerGetter.getAll () , cela dépend de ce que l'appelant utilise pour la méthode:

  • S'il veut simplement itérer, un Iterable serait le bon choix.
  • Si nous avons besoin de plus de méthodes telles que la taille, Collection pourrait.
  • S'il pense que les numéros sont uniques, un Set .
  • S'il compte en plus sur les nombres triés, un SortedSet .
  • Et s'il doit être l'arbre rouge et noir du JDK afin qu'il puisse manipuler son état interne directement pour un hack laid, un TreeSet pourrait être le bon choix.


0
votes

Cela ne peut pas avoir une seule réponse, car cela dépend du cas d'utilisation.
Et par là, je veux dire que cela dépend du degré de flexibilité que vous souhaitez que votre implémentation ait, ainsi que du degré de flexibilité que vous souhaitez offrir aux consommateurs de votre API .

Je vais vous donner une réponse plus générale.

Souhaitez-vous que votre consommateur effectue uniquement une boucle? Renvoyer une Collection .
Voulez-vous que votre consommateur puisse y accéder par index? Renvoyer une List
Voulez-vous que votre consommateur sache et puisse vérifier efficacement si un élément est présent? Renvoyer un Set
Et ainsi de suite.

La même approche est valable pour les paramètres d'entrée. Quel est l'intérêt d'accepter une List , ou même des classes concrètes telles que ArrayList ou LinkedList , si vous seulement en boucle? Vous donnez simplement moins de flexibilité à votre code pour de futures modifications.

Ce que vous faites ici avec les types de retour des méthodes héritées de IntegerGetter s'appelle spécialisation de type . Ce n'est pas grave tant que vous continuez à exposer les interfaces au monde extérieur.

Ma règle de base est d'être aussi générique que possible lorsqu'il s'agit du monde extérieur, et d'être aussi spécifique que possible lors de l'implémentation des parties critiques (principales) de mon application, pour restreindre les cas d'utilisation possibles, me protéger contre les abus Je viens de coder, et à des fins de documentation.

Ce que vous ne devriez pas faire absolument, c'est utiliser la comparaison instanceof ou classe pour découvrir le type réel et emprunter différentes routes. Cela ruine une base de code.


0 commentaires