Une interface est-elle considérée comme «vide» si elle hérite d'autres interfaces?

12

Les interfaces vides sont généralement considérées comme une mauvaise pratique, pour autant que je sache - en particulier lorsque des éléments tels que les attributs sont pris en charge par le langage.

Cependant, une interface est-elle considérée comme «vide» si elle hérite d'autres interfaces?

interface I1 { ... }
interface I2 { ... } //unrelated to I1

interface I3
    : I1, I2
{
    // empty body
}

Tout ce qui implémente I3devra implémenter I1and I2, et les objets de différentes classes qui héritent I3peuvent alors être utilisés de manière interchangeable (voir ci-dessous), est-il donc juste d'appeler I3 empty ? Si tel est le cas, quelle serait la meilleure façon d’architecturer cela?

// with I3 interface
class A : I3 { ... }
class B : I3 { ... }

class Test {
    void foo() {
        I3 something = new A();
        something = new B();
        something.SomeI1Function();
        something.SomeI2Function();
    }
}

// without I3 interface
class A : I1, I2 { ... }
class B : I1, I2 { ... }

class Test {
    void foo() {
        I1 something = new A();
        something = new B();
        something.SomeI1Function();
        something.SomeI2Function(); // we can't do this without casting first
    }
}
Kai
la source
1
L'impossibilité de spécifier plusieurs interfaces comme type de paramètre / champ, etc. est un défaut de conception dans C # / .NET.
CodesInChaos
Je pense que la meilleure façon d'architecture cette partie devrait aller dans une question distincte. À l'heure actuelle, la réponse acceptée n'a rien à voir avec le titre de la question.
Kapol
@CodesInChaos Non, ce n'est pas le cas car il existe déjà deux façons de le faire. L'une est dans cette question, l'autre manière serait de spécifier un paramètre de type générique pour l'argument de la méthode et d'utiliser la restriction where pour spécifier les deux interfaces.
Andy
Est-il logique qu'une classe qui implémente I1 et I2, mais pas I3, ne puisse pas être utilisée par foo? (Si la réponse est "oui", alors allez-y!)
user253751
@Andy Bien que je ne sois pas entièrement d'accord avec l'affirmation de CodesInChaos selon laquelle c'est une faille en tant que telle, je ne pense pas que les paramètres de type génériques sur la méthode soient une solution - cela pousse la responsabilité de connaître un type utilisé dans l'implémentation de la méthode sur quoi que ce soit appelle la méthode, qui se sent mal. Peut-être qu'une syntaxe comme var : I1, I2 something = new A();pour les variables locales serait bien (si nous ignorons les bons points soulevés dans la réponse de Konrad)
Kai

Réponses:

27

Tout ce qui implémente I3 devra implémenter I1 et I2, et les objets de différentes classes qui héritent d'I3 peuvent alors être utilisés de manière interchangeable (voir ci-dessous), est-il donc correct d'appeler I3 vide? Si tel est le cas, quelle serait la meilleure façon d’archiver cela?

"Bundling" I1et I2into I3offre un bon raccourci (?), Ou une sorte de sucre de syntaxe pour l'endroit où vous souhaitez que les deux soient implémentés.

Le problème avec cette approche est que vous ne pouvez pas empêcher d'autres développeurs d'implémenter explicitement I1 et I2 côte à côte, mais cela ne fonctionne pas dans les deux sens - alors qu'il : I3est équivalent à : I1, I2, : I1, I2n'est pas un équivalent de : I3. Même si elles sont fonctionnellement identiques, une classe qui prend en charge les deux I1et I2ne sera pas détectée comme prenant en charge I3.

Cela signifie que vous offrez deux façons d'accomplir la même chose - "manuelle" et "douce" (avec votre sucre de syntaxe). Et cela peut avoir un mauvais impact sur la cohérence du code. Offrir 2 façons différentes d'accomplir la même chose ici, là et ailleurs entraîne déjà 8 combinaisons possibles :)

void foo() {
    I1 something = new A();
    something = new B();
    something.SomeI1Function();
    something.SomeI2Function(); // we can't do this without casting first
}

OK, tu ne peux pas. Mais c'est peut-être en fait un bon signe?

Si I1et I2impliquent des responsabilités différentes (sinon il ne serait pas nécessaire de les séparer en premier lieu), alors peut foo()- être qu'il est faux d'essayer de faire somethingexécuter deux responsabilités différentes en une seule fois?

En d'autres termes, il se pourrait que foo()lui - même viole le principe de la responsabilité unique, et le fait qu'il nécessite des vérifications de type de coulée et d'exécution pour le retirer est un drapeau rouge nous alarmant à ce sujet.

ÉDITER:

Si vous insistez pour vous assurer que fooprend quelque chose qui implémente I1et I2, vous pouvez les passer en tant que paramètres distincts:

void foo(I1 i1, I2 i2) 

Et ils pourraient être le même objet:

foo(something, something);

Qu'importe foosi c'est la même instance ou non?

Mais si cela se soucie (par exemple parce qu'ils ne sont pas apatrides), ou si vous pensez simplement que cela semble moche, on pourrait utiliser des génériques à la place:

void Foo<T>(T something) where T: I1, I2

Désormais, les contraintes génériques prennent soin de l'effet recherché sans polluer le monde extérieur avec quoi que ce soit. Il s'agit d'un langage C # idiomatique, basé sur des vérifications à la compilation, et il semble globalement plus correct.

Je vois I3 : I2, I1un peu comme un effort pour émuler des types d'union dans un langage qui ne le prend pas en charge (C # ou Java non, alors que les types d'union existent dans les langages fonctionnels).

Essayer d'imiter une construction qui n'est pas vraiment prise en charge par le langage est souvent maladroit. De même, en C #, vous pouvez utiliser des méthodes d'extension et des interfaces si vous souhaitez émuler des mixins . Possible? Oui. Bonne idée? Je ne suis pas sûr.

Konrad Morawski
la source
4

S'il était particulièrement important qu'une classe implémente les deux interfaces, je ne vois rien de mal à créer une troisième interface qui hérite des deux. Cependant, je ferais attention de ne pas tomber dans le piège de la suringénierie. Une classe peut hériter de l'un ou de l'autre, ou des deux. À moins que mon programme n'ait eu beaucoup de classes dans ces trois cas, c'est un peu trop pour créer une interface pour les deux, et certainement pas si ces deux interfaces n'avaient aucune relation l'une avec l'autre (pas d'interfaces IComparableAndSerializable s'il vous plaît).

Je vous rappelle que vous pouvez également créer une classe abstraite qui hérite des deux interfaces, et vous pouvez utiliser cette classe abstraite à la place de I3. Je pense qu'il serait faux de dire que vous ne devriez pas le faire, mais vous devriez au moins avoir une bonne raison.

Neil
la source
Hein? Vous pouvez certainement implémenter deux interfaces dans une seule classe. Vous n'héritez pas des interfaces, vous les implémentez.
Andy
@Andy Où ai-je dit qu'une seule classe ne peut pas implémenter deux interfaces? Pour ce qui concerne l'utilisation du mot "hériter" à la place de "mettre en œuvre", la sémantique. En C ++, hériter fonctionnerait parfaitement bien car la chose la plus proche des interfaces sont les classes abstraites.
Neil
L'héritage et l'implémentation d'interface ne sont pas les mêmes concepts. Les classes héritant de plusieurs sous-classes peuvent entraîner des problèmes étranges, ce qui n'est pas le cas avec la mise en œuvre de plusieurs interfaces. Et C ++ sans interfaces ne signifie pas que la différence disparaît, cela signifie que C ++ est limité dans ce qu'il peut faire. L'héritage est une relation «est-un», tandis que les interfaces spécifient «peut-faire».
Andy
@Andy Problèmes étranges causés par des collisions entre les méthodes implémentées dans lesdites classes, oui. Cependant, ce n'est plus une interface ni dans son nom ni dans la pratique. Les classes abstraites qui déclarent mais n'implémentent pas de méthodes sont dans tous les sens du terme sauf nom, une interface. La terminologie change entre les langues, mais cela ne signifie pas qu'une référence et un pointeur ne sont pas le même concept. C'est un point pédant, mais si vous insistez là-dessus, alors nous devrons accepter d'être en désaccord.
Neil
"La terminologie change entre les langues" Non, ce n'est pas le cas. La terminologie OO est cohérente dans toutes les langues; Je n'ai jamais entendu une classe abstraite signifiant autre chose dans chaque langage OO que j'ai utilisé. Oui, vous pouvez avoir la sémantique d'une interface en C ++, mais le fait est que rien n'empêche quelqu'un d'aller ajouter un comportement à une classe abstraite dans ce langage et de casser cette sémantique.
Andy
2

Les interfaces vides sont généralement considérées comme une mauvaise pratique

Je ne suis pas d'accord. Lisez à propos des interfaces de marqueur .

Alors qu'une interface typique spécifie une fonctionnalité (sous forme de déclarations de méthode) qu'une classe d'implémentation doit prendre en charge, une interface marqueur n'a pas besoin de le faire. La simple présence d'une telle interface indique un comportement spécifique de la part de la classe d'implémentation.

radarbob
la source
J'imagine que le PO en est conscient, mais ils sont en fait un peu controversés. Alors que certains auteurs les recommandent (par exemple, Josh Bloch dans "Effective Java"), certains affirment qu'ils sont un contre-modèle et un héritage de l'époque où Java n'avait pas encore d'annotations. (Les annotations en Java sont à peu près l'équivalent des attributs en .NET). Mon impression est qu'ils sont beaucoup plus populaires dans le monde Java que .NET.
Konrad Morawski
1
Je les utilise de temps en temps, mais cela semble un peu hackish - les inconvénients, au-dessus de ma tête, sont que vous ne pouvez pas vous empêcher du fait qu'ils sont hérités, donc une fois que vous marquez une classe avec une, tous ses enfants obtiennent marqué aussi bien si vous le voulez ou non. Et ils semblent encourager les mauvaises pratiques d'utilisation instanceofau lieu du polymorphisme
Konrad Morawski