Je suis actuellement en train d'essayer de maîtriser C #, donc je lis Code adaptatif 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, convertir la référence d'interface à n'importe quelle implémentation est toujours une mauvaise idée.
Cela pourrait ê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();
Suis-je en train de comprendre que c'est faux? Si oui, pourquoi et que dois-je faire à la place?
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.
Donc peut-être que SomeClass
et ISomeClass
est un mauvais exemple, car ce serait comme avoir une classe OracleObjectSerializer
et une interface IOracleObjectSerializer
.
Un exemple plus précis serait quelque chose comme OracleObjectSerializer
et un 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 devez utiliser IObjectSerializer
sans vous soucier de son fonctionnement. Supposons une seconde maintenant que vous avez également une implémentation SQLServerObjectSerializer
en plus de 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 principe de substitution de Liskov .
La manière incorrecte, et l'instance même mentionnée dans votre livre, serait de prendre une instance de IObjectSerializer
et de la convertir en OracleObjectSerializer
puis d'appeler la méthode setProperty
disponible uniquement sur OracleObjectSerializer
. C'est mauvais car même si vous savez peut-être qu'une instance est un OracleObjectSerializer
, vous introduisez encore un autre point dans votre programme où vous vous souciez de savoir de quoi 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 instance IObjectSerializer
en OracleObjectSerializer
et vous recevez un échec d'exécution en production.
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 mon OracleObjectSerializer
si elles sont faites correctement. Si vous extrayez 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 un Dog
classe fonctionne comme une implémentation IPerson
par exemple).
L'approche correcte serait de fournir une méthode setProperty
à IObjectSerializer
. Des méthodes similaires dans SQLServerObjectSerializer
fonctionneraient idéalement grâce à cette méthode setProperty
. Mieux encore, vous standardisez les noms de propriété via un Enum
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'en représente que la moitié. 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 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, il transforme en une implémentation spécifique, "Parce que je sais que ce sera seulement 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 transtyper d'une interface vers 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 myClass
a le type ISomeClass
, mais il n'a aucune raison car il est toujours affecté à 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.
Pour plus de clarté, définissons le casting.
La diffusion 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 caster à partir de cette page Microsoft docs .
// 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 pourriez 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 devrait 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.
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.
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 (mélange entre Java et C++). J'espère que c'est OK.
Supposons que vous ayez l'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:
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 d'utiliser iList
. Bien sûr, nous ne pouvons plus effectuer d'opérations spécifiques à DynamicArrayList
, 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).
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.