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?
la source
ISomeClass myClass = new SomeClass();
? Si vous voulez vraiment dire cela, c'est la récursivité dans le constructeur, probablement pas ce que vous voulez. ?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.Réponses:
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 etISomeClass
c'est un mauvais exemple, car ce serait comme avoir uneOracleObjectSerializer
classe et uneIOracleObjectSerializer
interface.Un exemple plus précis serait quelque chose comme
OracleObjectSerializer
et aIObjectSerializer
. 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
IObjectSerializer
pas vous soucier de son fonctionnement. Supposons une seconde maintenant que vous disposez également d'uneSQLServerObjectSerializer
implémentationOracleObjectSerializer
. 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
IObjectSerializer
et de la convertirOracleObjectSerializer
, puis d'appeler la méthodesetProperty
disponible uniquement surOracleObjectSerializer
. C'est mauvais car même si vous savez qu'une instance est unOracleObjectSerializer
, 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 uneIObjectSerializer
instance en unOracleObjectSerializer
et 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
setProperty
dans la classe d'implémentation comme dans le cas de monOracleObjectSerializer
si elles sont faites correctement. Si vous enlevez une classeOracleObjectSerializer
à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 uneDog
classe comme uneIPerson
implémentation par exemple).L'approche correcte serait de fournir une
setProperty
méthode pourIObjectSerializer
. Des méthodes similairesSQLServerObjectSerializer
fonctionneraient idéalement grâce à cettesetProperty
méthode. Mieux encore, vous standardisez les noms de propriété via unEnum
où 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
ISomeClass
n'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.la source
IObjectSerializer
àOracleObjectSerializer
parce 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 utiliserOracleObjectSerializer
tout 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.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:
D'une manière générale, ce n'est pas terrible. La chose à éviter autant que possible serait de faire ceci:
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
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
myClass
a le typeISomeClass
, 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.la source
Je pense qu'il est plus facile de montrer votre code avec un mauvais exemple:
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.
la source
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 .
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.
la source
Animal
/Giraffe
ci-dessus, si vous deviez faireAnimal 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").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
:Supposons maintenant qu'il existe de nombreuses implémentations:
On peut penser à de nombreuses implémentations différentes.
Supposons maintenant que nous ayons le code suivant:
Cela montre clairement notre intention que nous voulons utiliser
iList
. Bien sûr, nous ne pouvons plus effectuerDynamicArrayList
d'opérations spécifiques, mais nous avons besoin d'uniList
.Considérez le code suivant:
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).
la source
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.
la source