Que veut dire l'auteur en transposant la référence d'interface à n'importe quelle implémentation?

17

Je suis actuellement en train d'essayer de maîtriser le C #, donc je lis Adaptive Code via C # par Gary McLean Hall .

Il écrit sur les modèles et les anti-modèles. Dans la partie implémentations versus interfaces, il écrit ce qui suit:

Les développeurs qui sont nouveaux dans le concept de programmation vers les interfaces ont souvent du mal à abandonner ce qui se cache derrière l'interface.

Au moment de la compilation, tout client d'une interface ne devrait avoir aucune idée de l'implémentation de l'interface qu'il utilise. De telles connaissances peuvent conduire à des hypothèses incorrectes qui couplent le client à une implémentation spécifique de l'interface.

Imaginez l'exemple courant dans lequel une classe doit enregistrer un enregistrement dans un stockage persistant. Pour ce faire, il délègue à juste titre à une interface, qui cache les détails du mécanisme de stockage persistant utilisé. Cependant, il ne serait pas juste de faire des hypothèses sur l'implémentation de l'interface utilisée au moment de l'exécution. Par exemple, la conversion de la référence d'interface dans n'importe quelle implémentation est toujours une mauvaise idée.

C'est peut-être la barrière de la langue ou mon manque d'expérience, mais je ne comprends pas très bien ce que cela signifie. Voici ce que je comprends:

J'ai un projet amusant de temps libre pour pratiquer le C #. Là j'ai une classe:

public class SomeClass...

Cette classe est utilisée dans de nombreux endroits. En apprenant le C #, j'ai lu qu'il valait mieux résumer avec une interface, donc j'ai fait ce qui suit

public interface ISomeClass <- Here I made a "contract" of all the public methods and properties SomeClass needs to have.

public class SomeClass : ISomeClass <- Same as before. All implementation here.

Je suis donc allé dans toutes les références de classe et les ai remplacées par ISomeClass.

Sauf dans la construction, où j'ai écrit:

ISomeClass myClass = new SomeClass();

Est-ce que je comprends bien que c'est faux? Si oui, pourquoi et que dois-je faire à la place?

Marshall
la source
25
Nulle part dans votre exemple vous ne transformez un objet de type interface en type d'implémentation. Vous affectez quelque chose de type d'implémentation à une variable d'interface, ce qui est parfaitement fin et correct.
Caleth
1
Que voulez-vous dire "dans le constructeur où j'ai écrit ISomeClass myClass = new SomeClass();? Si vous voulez vraiment dire cela, c'est la récursivité dans le constructeur, probablement pas ce que vous voulez. ?
Erik Eidt
@Erik: Oui. En construction. Vous avez raison. Corrigera la question. Merci
Marshall
Fait amusant: F # a une meilleure histoire que C # à cet égard - il supprime les implémentations d'interface implicites, donc chaque fois que vous voulez appeler une méthode d'interface, vous devez passer au type d'interface. Cela indique très clairement quand et comment vous utilisez les interfaces dans votre code, et rend la programmation des interfaces beaucoup plus ancrée dans le langage.
scrwtp
3
C'est un peu hors sujet, mais je pense que l'auteur diagnostique mal le problème que les nouveaux utilisateurs ont. À mon avis, le problème est que les personnes nouvelles au concept ne savent pas comment faire de bonnes interfaces. Il est très facile de créer des interfaces trop spécifiques qui ne fournissent en fait aucune généralité (ce qui pourrait bien se produire avec ISomeClass), mais il est également facile de créer des interfaces trop générales pour lesquelles il est impossible d'écrire du code utile contre lequel les seules options sont de repenser l'interface et de réécrire le code ou de downcast.
Derek Elkins a quitté le SE

Réponses:

37

Résumé de votre classe dans une interface est quelque chose que vous devriez considérer si et seulement si vous avez l'intention d'écrire d'autres implémentations de ladite interface ou s'il existe une forte possibilité de le faire à l'avenir.

Alors peut SomeClass- être et ISomeClassc'est un mauvais exemple, car ce serait comme avoir une OracleObjectSerializerclasse et une IOracleObjectSerializerinterface.

Un exemple plus précis serait quelque chose comme OracleObjectSerializeret a IObjectSerializer. Le seul endroit dans votre programme où vous vous souciez de l'implémentation à utiliser est lorsque l'instance est créée. Parfois, cela est encore découplé en utilisant un modèle d'usine.

Partout ailleurs dans votre programme, vous ne devez IObjectSerializerpas vous soucier de son fonctionnement. Supposons une seconde maintenant que vous disposez également d'une SQLServerObjectSerializerimplémentation OracleObjectSerializer. Supposons maintenant que vous devez définir une propriété spéciale à définir et que cette méthode est uniquement présente dans OracleObjectSerializer et non SQLServerObjectSerializer.

Il y a deux façons de procéder: la méthode incorrecte et l' approche du principe de substitution de Liskov .

La mauvaise façon

La manière incorrecte, et l'instance même mentionnée dans votre livre, serait de prendre une instance de IObjectSerializeret de la convertir OracleObjectSerializer, puis d'appeler la méthodesetProperty disponible uniquement sur OracleObjectSerializer. C'est mauvais car même si vous savez qu'une instance est un OracleObjectSerializer, vous introduisez un autre point dans votre programme où vous voulez savoir de quelle implémentation il s'agit. Lorsque cette implémentation change, et ce sera vraisemblablement tôt ou tard si vous avez plusieurs implémentations, dans le meilleur des cas, vous devrez trouver tous ces emplacements et effectuer les ajustements corrects. Dans le pire des cas, vous convertissez une IObjectSerializerinstance en un OracleObjectSerializeret vous recevez un échec d'exécution en production.

Approche du principe de substitution de Liskov

Liskov a dit que vous ne devriez jamais avoir besoin de méthodes comme setPropertydans la classe d'implémentation comme dans le cas de monOracleObjectSerializer si elles sont faites correctement. Si vous enlevez une classe OracleObjectSerializerà IObjectSerializer, vous devez englober toutes les méthodes nécessaires pour utiliser cette classe, et si vous ne le pouvez pas, alors quelque chose ne va pas avec votre abstraction (essayer de faire fonctionner une Dogclasse comme une IPersonimplémentation par exemple).

L'approche correcte serait de fournir une setPropertyméthode pourIObjectSerializer . Des méthodes similaires SQLServerObjectSerializerfonctionneraient idéalement grâce à cette setPropertyméthode. Mieux encore, vous standardisez les noms de propriété via un Enumoù chaque implémentation traduit cette énumération en équivalent pour sa propre terminologie de base de données.

En termes simples, l'utilisation d'un ISomeClassn'est que la moitié de celui-ci. Vous ne devriez jamais avoir besoin de le convertir en dehors de la méthode responsable de sa création. Cela est presque certainement une grave erreur de conception.

Neil
la source
1
Il me semble que, si vous allez casting IObjectSerializerà OracleObjectSerializerparce que vous « savez » que c'est ce qu'elle est, alors vous devriez être honnête avec vous - même (et plus important encore , avec d' autres qui peuvent maintenir ce code, qui peut inclure votre avenir auto), et utiliser OracleObjectSerializertout au long de l'endroit où il est créé jusqu'à l'endroit où il est utilisé. Cela rend très public et clair que vous introduisez une dépendance vis-à-vis d'une implémentation particulière - et le travail et la laideur impliqués dans cette opération deviennent, en soi, un indice fort que quelque chose ne va pas.
KRyan
(Et, si pour une raison quelconque , vous vraiment ne devez compter sur une mise en œuvre particulière, il devient beaucoup plus clair que c'est ce que vous faites et que vous le faites avec l' intention et le but. Ce « devrait » jamais arriver bien sûr, et 99% des fois, il peut sembler que cela se produit, ce n'est pas le cas et vous devriez réparer les choses, mais rien n'est jamais sûr à 100% ou comment les choses devraient être.)
KRyan
@KRyan Absolument. L'abstraction ne doit être utilisée que si vous en avez besoin. L'utilisation de l'abstraction lorsqu'elle n'est pas nécessaire ne fait que rendre le code légèrement plus difficile à comprendre.
Neil
29

La réponse acceptée est correcte et très utile, mais je voudrais aborder brièvement la ligne de code que vous avez demandée:

ISomeClass myClass = new SomeClass();

D'une manière générale, ce n'est pas terrible. La chose à éviter autant que possible serait de faire ceci:

void someMethod(ISomeClass interface){
    SomeClass cast = (SomeClass)interface;
}

Lorsque votre code est fourni une interface en externe, mais en interne le transforme en une implémentation spécifique, "Parce que je sais que ce ne sera que cette implémentation". Même si cela s'est avéré vrai, en utilisant une interface et en la convertissant en une implémentation, vous renoncez volontairement à la sécurité de type réel pour pouvoir prétendre utiliser l'abstraction. Si quelqu'un d'autre devait travailler sur le code plus tard et voir une méthode qui accepte un paramètre d'interface, alors il va supposer que toute implémentation de cette interface est une option valide à transmettre. Cela pourrait même être vous-même un peu plus bas après avoir oublié qu'une méthode spécifique repose sur les paramètres dont elle a besoin. Si vous ressentez le besoin de convertir une interface en une implémentation spécifique, alors l'interface, l'implémentation, ou le code qui les référence est mal conçu et doit changer. Par exemple, si la méthode ne fonctionne que lorsque l'argument transmis est une classe spécifique, le paramètre ne doit accepter que cette classe.

Maintenant, revenons à votre appel constructeur

ISomeClass myClass = new SomeClass();

les problèmes de casting ne s'appliquent pas vraiment. Rien de tout cela ne semble être exposé à l'extérieur, il n'y a donc pas de risque particulier associé. Essentiellement, cette ligne de code elle-même est un détail d'implémentation que les interfaces sont conçues pour abstraire au départ, donc un observateur externe le verrait fonctionner de la même manière, indépendamment de ce qu'elles font. Cependant, cela ne gagne rien non plus de l'existence d'une interface. Votre myClassa le type ISomeClass, mais il n'a aucune raison, car il est toujours attribué une implémentation spécifique,SomeClass. Il y a quelques avantages potentiels mineurs, tels que la possibilité d'échanger l'implémentation dans le code en changeant simplement l'appel du constructeur, ou en réaffectant cette variable ultérieurement à une implémentation différente, mais à moins qu'il n'y ait un autre endroit qui nécessite que la variable soit tapée dans l'interface plutôt que l'implémentation de ce modèle donne à votre code l'apparence d'interfaces utilisées uniquement par cœur, pas par compréhension réelle des avantages des interfaces.

Kamil Drakari
la source
1
Apache Math le fait avec Cartesian3D Source, ligne 286 , ce qui peut être vraiment ennuyeux.
J_F_B_M
1
Il s'agit de la réponse réellement correcte qui répond à la question d'origine.
Benjamin Gruenbaum
2

Je pense qu'il est plus facile de montrer votre code avec un mauvais exemple:

public interface ISomeClass
{
    void DoThing();
}

public class SomeClass : ISomeClass
{
    public void DoThing()
    {
       // Mine for BitCoin
    }

}

public class AnotherClass : ISomeClass
{
    public void DoThing()
    {
        // Mine for oil
    }
    public Decimal Depth;
 }

 void main()
 {
     ISomeClass task = new SomeClass();

     task.DoThing(); //  This is good

     Console.WriteLine("Depth = {0}", ((AnotherClass)task).Depth); <-- The task object will not have this field
 }

Le problème est que lorsque vous écrivez initialement le code, il n'y a probablement qu'une seule implémentation de cette interface, de sorte que le casting fonctionnerait toujours, c'est juste qu'à l'avenir, vous pourriez implémenter une autre classe, puis (comme mon exemple le montre), vous essayez d'accéder à des données qui n'existent pas dans l'objet que vous utilisez.

Neil
la source
Pourquoi bonjour, monsieur. Quelqu'un vous a déjà dit à quel point vous êtes beau?
Neil
2

Pour plus de clarté, définissons le casting.

La conversion consiste à convertir de force un élément d'un type à un autre. Un exemple courant consiste à convertir un nombre à virgule flottante en un type entier. Une conversion spécifique peut être spécifiée lors de la conversion, mais la valeur par défaut consiste simplement à réinterpréter les bits.

Voici un exemple de diffusion à partir de cette page de documentation Microsoft .

// Create a new derived type.  
Giraffe g = new Giraffe();  

// Implicit conversion to base type is safe.  
Animal a = g;  

// Explicit conversion is required to cast back  
// to derived type. Note: This will compile but will  
// throw an exception at run time if the right-side  
// object is not in fact a Giraffe.  
Giraffe g2 = (Giraffe) a;  

Vous pouvez faire la même chose et lancer quelque chose qui implémente une interface dans une implémentation particulière de cette interface, mais vous ne devriez pas car cela entraînera une erreur ou un comportement inattendu si une implémentation différente de celle que vous attendez est utilisée.

Ryan1729
la source
1
"Casting convertit quelque chose d'un type à un autre." - Non. Casting convertit explicitement quelque chose d'un type à un autre. (Plus précisément, «cast» est le nom de la syntaxe utilisée pour spécifier cette conversion.) Les conversions implicites ne sont pas des transtypages. "Une conversion spécifique peut être spécifiée lors de la conversion, mais la valeur par défaut consiste simplement à réinterpréter les bits." -- Certainement pas. Il existe de nombreuses conversions, implicites et explicites, qui impliquent des modifications importantes des modèles de bits.
hvd
@hvd J'ai maintenant apporté une correction concernant l'explication du casting. Quand j'ai dit que la valeur par défaut est simplement de réinterpréter les bits, j'essayais d'exprimer que si vous deviez créer votre propre type, alors dans les cas où une distribution est automatiquement définie, lorsque vous la convertissez en un autre type, les bits seront réinterprétés . Dans l' exemple Animal/ Giraffeci-dessus, si vous deviez faire Animal a = (Animal)g;les bits, ils seraient réinterprétés (toutes les données spécifiques à Giraffe seraient interprétées comme "ne faisant pas partie de cet objet").
Ryan1729
Malgré ce que dit hvd, les gens utilisent très souvent le terme "cast" en référence aux conversions implicites; voir par exemple https://www.google.com/search?q="implicit+cast"&tbm=bks . Techniquement, je pense qu'il est plus correct de réserver le terme "cast" pour les conversions explicites, tant que vous ne vous trompez pas lorsque d'autres l'utilisent différemment.
ruakh
0

Mes 5 cents:

Tous ces exemples sont corrects, mais ils ne sont pas des exemples du monde réel et n'ont pas montré les intentions du monde réel.

Je ne connais pas C # donc je vais donner un exemple abstrait (mix entre Java et C ++). J'espère que ça va.

Supposons que vous ayez une interface iList:

interface iList<Key,Value>{
   bool add(Key k, Value v);
   bool remove(Element e);
   Value get(Key k);
}

Supposons maintenant qu'il existe de nombreuses implémentations:

  • DynamicArrayList - utilise un tableau plat, rapide à insérer et à retirer à la fin.
  • LinkedList - utilise une double liste chaînée, rapide à insérer devant et à la fin.
  • AVLTreeList - utilise AVL Tree, rapide pour tout faire, mais utilise beaucoup de mémoire
  • SkipList - utilise SkipList, rapide pour tout faire, plus lent que AVL Tree, mais utilise moins de mémoire.
  • HashList - utilise HashTable

On peut penser à de nombreuses implémentations différentes.

Supposons maintenant que nous ayons le code suivant:

uint begin_size = 1000;
iList list = new DynamicArrayList(begin_size);

Cela montre clairement notre intention que nous voulons utiliser iList. Bien sûr, nous ne pouvons plus effectuer DynamicArrayListd'opérations spécifiques, mais nous avons besoin d'un iList.

Considérez le code suivant:

iList list = factory.getList();

Maintenant, nous ne savons même pas quelle est la mise en œuvre. Ce dernier exemple est souvent utilisé dans le traitement d'image lorsque vous chargez un fichier à partir du disque et que vous n'avez pas besoin de son type de fichier (gif, jpeg, png, bmp ...), mais tout ce que vous voulez, c'est faire une manipulation d'image (flip, échelle, enregistrer au format png à la fin).

pseudo
la source
0

Vous avez une interface ISomeClass, et un objet myObject dont vous ne savez rien de votre code sauf qu'il est déclaré implémenter ISomeClass.

Vous avez une classe SomeClass, qui, vous le savez, implémente l'interface ISomeClass. Vous le savez car il est déclaré implémenter ISomeClass, ou vous l'avez implémenté vous-même pour implémenter ISomeClass.

Quel est le problème avec la conversion de myClass en SomeClass? Deux choses ne vont pas. Premièrement, vous ne savez pas vraiment que myClass est quelque chose qui peut être converti en SomeClass (une instance de SomeClass ou une sous-classe de SomeClass), de sorte que la distribution peut mal tourner. Deux, vous ne devriez pas avoir à faire cela. Vous devez travailler avec myClass déclaré en tant que iSomeClass et utiliser les méthodes ISomeClass.

Le point où vous obtenez un objet SomeClass est lorsqu'une méthode d'interface est appelée. À un moment donné, vous appelez myClass.myMethod (), qui est déclaré dans l'interface, mais qui a une implémentation dans SomeClass, et bien sûr éventuellement dans de nombreuses autres classes implémentant ISomeClass. Si un appel aboutit dans votre code SomeClass.myMethod, alors vous savez que self est une instance de SomeClass, et à ce stade, il est tout à fait correct et en fait correct de l'utiliser comme objet SomeClass. Bien sûr, s'il s'agit en fait d'une instance de OtherClass et non de SomeClass, vous n'arriverez pas au code SomeClass.

gnasher729
la source