Quand utiliser des génériques dans la conception d'interfaces

11

J'ai certaines interfaces que j'ai l'intention d'implémenter à l'avenir par des tiers et je fournis moi-même une implémentation de base. Je vais seulement utiliser un couple pour montrer l'exemple.

Actuellement, ils sont définis comme

Article:

public interface Item {

    String getId();

    String getName();
}

ItemStack:

public interface ItemStackFactory {

    ItemStack createItemStack(Item item, int quantity);
}

ItemStackContainer:

public interface ItemStackContainer {

    default void add(ItemStack stack) {
        add(stack, 1);
    }

    void add(ItemStack stack, int quantity);
}

Maintenant, Itemet ItemStackFactoryje peux absolument prévoir qu'un tiers devra l'étendre à l'avenir. ItemStackContainerpourrait également être étendu à l'avenir, mais pas de manière que je puisse prévoir, en dehors de mon implémentation par défaut fournie.

Maintenant, j'essaie de rendre cette bibliothèque aussi robuste que possible; ceci est encore aux premiers stades (pré-pré-alpha), donc cela peut être un acte de suringénierie (YAGNI). Est-ce un endroit approprié pour utiliser des génériques?

public interface ItemStack<T extends Item> {

    T getItem();

    int getQuantity();
}

Et

public interface ItemStackFactory<T extends ItemStack<I extends Item>> {

    T createItemStack(I item, int quantity);
}

Je crains que cela ne finisse par rendre les implémentations et l'utilisation plus difficiles à lire et à comprendre; Je pense que c'est la recommandation d'éviter autant que possible l'imbrication de génériques.

Zymus
la source
1
On a l'impression que vous exposez les détails de l'implémentation de toute façon. Pourquoi l'appelant doit-il travailler avec des piles plutôt que de simples quantités?
cHao
C'est quelque chose auquel je n'avais pas pensé. Cela donne un bon aperçu de ce problème spécifique. Je m'assurerai d'apporter les modifications à mon code.
Zymus

Réponses:

9

Vous utilisez des génériques dans votre interface lorsque votre implémentation est également susceptible d'être générique.

Par exemple, toute structure de données qui peut accepter des objets arbitraires est un bon candidat pour une interface générique. Exemples: List<T>et Dictionary<K,V>.

Toute situation dans laquelle vous souhaitez améliorer la sécurité des types de manière généralisée est un bon candidat pour les génériques. A List<String>est une liste qui ne fonctionne que sur des chaînes.

Toute situation dans laquelle vous souhaitez appliquer SRP de manière généralisée et éviter les méthodes spécifiques au type est un bon candidat pour les génériques. Par exemple, un PersonDocument, PlaceDocument et ThingDocument peuvent être remplacés par un Document<T>.

Le modèle Abstract Factory est un bon cas d'utilisation pour un générique, tandis qu'une méthode Factory ordinaire créerait simplement des objets héritant d'une interface concrète commune.

Robert Harvey
la source
Je ne suis pas vraiment d'accord avec l'idée que les interfaces génériques devraient être associées à des implémentations génériques. Déclarer un paramètre de type générique dans une interface, puis l'affiner ou l'instancier dans diverses implémentations est une technique puissante. C'est particulièrement courant à Scala. Interfaces les choses abstraites; les classes les spécialisent.
Benjamin Hodgson
@BenjaminHodgson: cela signifie simplement que vous produisez des implémentations sur une interface générique pour des types spécifiques. L'approche globale est toujours générique, quoique un peu moins.
Robert Harvey
Je suis d'accord. J'ai peut-être mal compris votre réponse - vous sembliez déconseiller ce genre de conception.
Benjamin Hodgson
@BenjaminHodgson: Une méthode d'usine ne serait-elle pas écrite contre une interface concrète , plutôt que générique? (même si une usine abstraite serait générique)
Robert Harvey
Je pense que nous sommes d'accord :) J'écrirais probablement l'exemple du PO comme quelque chose comme class StackFactory { Stack<T> Create<T>(T item, int count); }. Je peux écrire un exemple de ce que je voulais dire par "affiner un paramètre de type dans une sous-classe" dans une réponse si vous voulez plus de précisions, bien que la réponse ne soit que tangentiellement liée à la question d'origine.
Benjamin Hodgson
8

Votre plan sur la façon d'introduire la généralité dans votre interface me semble correct. Cependant, votre question de savoir si ce serait une bonne idée nécessite une réponse un peu plus compliquée.

Au fil des ans, j'ai interviewé un certain nombre de candidats à des postes de génie logiciel pour le compte d'entreprises pour lesquelles j'ai travaillé, et je me suis rendu compte que la majorité des demandeurs d'emploi sont à l'aise avec l' utilisation de classes génériques (par exemple, les classes de collection standard), mais pas avec l' écriture de classes génériques. La plupart n'ont jamais écrit une seule classe générique au cours de leur carrière et semblent comme s'ils seraient complètement perdus si on leur en demandait une. Donc, si vous forcez l'utilisation de génériques à quiconque souhaite implémenter votre interface ou étendre vos implémentations de base, vous risquez de rebuter les gens.

Ce que vous pouvez essayer, cependant, est de fournir à la fois des versions génériques et non génériques de vos interfaces et de vos implémentations de base, afin que les personnes qui ne font pas de génériques puissent vivre heureux dans leur monde étroit et lourd de type, tandis que les personnes qui Les génériques peuvent-ils profiter de leur compréhension plus large de la langue.

Mike Nakis
la source
3

Maintenant, Itemet ItemStackFactoryje peux absolument prévoir qu'un tiers devra l'étendre à l'avenir.

Votre décision est prise pour vous. Si vous ne fournissez pas de génériques, ils devront actualiser leur implémentation à Itemchaque utilisation:

class MyItem implements Item {
}
MyItem item = new MyItem();
ItemStack is = createItemStack(item, 10);
// They would have to cast here.
MyItem theItem = (MyItem)is.getItem();

Vous devriez au moins faire des ItemStackgénériques comme vous le suggérez. L'avantage le plus profond des génériques est que vous n'avez presque jamais besoin de lancer quoi que ce soit, et l'offre de code qui oblige les utilisateurs à lancer est à mon humble avis une infraction pénale.

OldCurmudgeon
la source
2

Wow ... J'ai une vision complètement différente de cela.

Je peux absolument prévoir un tiers ayant besoin

C'est une erreur. Vous ne devriez pas écrire de code "pour l'avenir".

De plus, les interfaces sont écrites de haut en bas. Vous ne regardez pas une classe et vous dites "Hé, cette classe pourrait totalement implémenter une interface et je pourrai ensuite utiliser cette interface ailleurs". C'est une mauvaise approche, car seule la classe qui utilise une interface sait vraiment ce que devrait être l'interface. Au lieu de cela, vous devriez penser "À cet endroit, ma classe a besoin d'une dépendance. Permettez-moi de définir une interface pour cette dépendance. Lorsque j'aurai fini d'écrire cette classe, j'écrirai une implémentation de cette dépendance." Que quelqu'un puisse réutiliser votre interface et remplacer votre implémentation par défaut par la sienne est complètement hors de propos. Ils ne le feront peut-être jamais. Cela ne signifie pas que l'interface était inutile. Cela fait que votre code suit le principe d'inversion de contrôle, ce qui rend votre code très extensible à de nombreux endroits "

anton1980
la source
0

Je pense que l'utilisation de génériques dans votre situation est trop complexe.

Si vous choisissez la version générique des interfaces, la conception indique que

  1. ItemStackContainer <A> contient un seul type de pile, A. (c'est-à-dire que ItemStackContainer ne peut pas contenir plusieurs types de pile.)
  2. ItemStack est dédié à un type d'élément spécifique. Il n'y a donc pas de type d'abstraction de toutes sortes de piles.
  3. Vous devez définir un ItemStackFactory spécifique pour chaque type d'éléments. Il est considéré comme trop fin comme Factory Pattern.
  4. Écrivez du code plus difficile tel que ItemStack <? étend Item>, diminuant la maintenabilité.

Ces hypothèses et restrictions implicites sont des choses très importantes à décider soigneusement. Ainsi, en particulier au stade précoce, ces conceptions prématurées ne doivent pas être introduites. Une conception simple, facile à comprendre et commune est souhaitée.

Akio Takahashi
la source
0

Je dirais que cela dépend principalement de cette question: si quelqu'un a besoin d'étendre les fonctionnalités de Itemet ItemStackFactory,

  • vont-ils simplement écraser les méthodes et les instances de leurs sous-classes seront-elles utilisées comme les classes de base, se comporteront-elles différemment?
  • ou vont-ils ajouter des membres et utiliser les sous-classes différemment?

Dans le premier cas, il n'y a pas besoin de génériques, dans le second, probablement.

Michael Borgwardt
la source
0

Peut-être que vous pouvez avoir une hiérarchie d'interface, où Foo est une interface, Bar est une autre. Chaque fois qu'un type doit être à la fois, c'est un FooAndBar.

Pour éviter une surcharge, ne faites le type FooAndBar que lorsque cela est nécessaire, et conservez une liste d'interfaces qui ne prolongeront jamais rien.

C'est beaucoup plus confortable pour la plupart des programmeurs (la plupart aiment leur vérification de type ou aiment ne jamais traiter de typage statique d'aucune sorte). Très peu de gens ont écrit leur propre cours de génériques.

Également comme remarque: les interfaces concernent les actions possibles qui peuvent être exécutées. Ils devraient en fait être des verbes et non des noms.

Akash Patel
la source
Cela pourrait être une alternative à l'utilisation de génériques, mais la question initiale demandait quand utiliser des génériques par rapport à ce que vous avez suggéré. Pouvez-vous améliorer votre réponse pour suggérer que les génériques ne sont jamais nécessaires, mais utiliser plusieurs interfaces est la réponse à tous?
Jay Elston