Est-ce que “Parent x = nouvel enfant ();” au lieu de “Enfant x = nouvel enfant ();” est une mauvaise pratique si nous pouvons utiliser ce dernier?


32

Par exemple, j'avais vu des codes créer un fragment comme celui-ci:

Fragment myFragment=new MyFragment();

qui déclare une variable sous la forme Fragment au lieu de MyFragment, lequel MyFragment est une classe enfant de Fragment. Je ne suis pas satisfait de cette ligne de codes car je pense que ce code devrait être:

MyFragment myFragment=new MyFragment();

qui est plus précis, est-ce vrai?

Ou en général, est-ce une mauvaise pratique d'utiliser:

Parent x=new Child();

au lieu de

Child x=new Child();

si nous pouvons changer le premier en dernier sans erreur de compilation?


6
Si nous faisons cela, il n'y a plus de raison d'abstraction / généralisation. Bien sûr, il y a des exceptions ...
Walfrat

2
Dans un projet sur lequel je travaille, mes fonctions attendent le type parent et exécutent des méthodes de type parent. Je peux accepter n'importe quel enfant, et le code derrière ces méthodes (héritées et surchargées) est différent, mais je veux exécuter ce code, quel que soit l'enfant que j'utilise.
Stephen S

4
@Walfrat En effet, l'abstraction n'a aucun intérêt si vous installez déjà le type concret, mais vous pouvez toujours renvoyer le type abstrait pour que le code client soit parfaitement ignorant.
Jacob Raihle

4
Ne pas utiliser parent / enfant. Utilisez Interface / Class (pour Java).
Thorbjørn Ravn Andersen

4
Google "programmation pour une interface" pour un tas d'explications sur pourquoi utiliser Parent x = new Child()est une bonne idée.
Kevin Workman

Réponses:


69

Cela dépend du contexte, mais je dirais que vous devriez déclarer le type le plus abstrait possible. Ainsi, votre code sera aussi général que possible et ne dépendra pas de détails non pertinents.

Un exemple serait d'avoir un LinkedListet ArrayListdont les deux descendent List. Si le code fonctionne aussi bien avec n'importe quel type de liste, il n'y a aucune raison de le limiter arbitrairement à l'une des sous-classes.


24
Exactement. Pour ajouter un exemple, réfléchissez à List list = new ArrayList()ceci: Le code utilisant la liste ne s'intéresse pas au type de liste, mais répond au contrat de l'interface Liste. À l'avenir, nous pourrions facilement passer à une autre implémentation de List et le code sous-jacent n'aurait pas besoin d'être modifié ou mis à jour.
Maybe_Factor

19
Une bonne réponse mais qui devrait développer le type le plus abstrait possible signifie que le code dans le champ d'application ne devrait pas être rediffusé à l'enfant pour qu'il effectue une opération spécifique à l'enfant.
Mike

10
@ Mike: J'espère que cela va sans dire, sinon toutes les variables, paramètres et champs seraient déclarés comme Object!
JacquesB

3
@JacquesB: comme cette question et cette réponse ont fait l'objet de tant d'attention, je pensais qu'être explicite serait utile pour des personnes de tous niveaux de compétences.
Mike

1
Cela dépend du contexte n'est pas une réponse.
Billal Begueradj

11

La réponse de JacquesB est correcte, bien qu'un peu abstraite. Permettez-moi de souligner plus clairement certains aspects:

Dans votre extrait de code, vous utilisez explicitement le newmot - clé et vous souhaitez peut-être poursuivre avec des étapes d'initialisation supplémentaires, qui sont probablement spécifiques au type concret: vous devez ensuite déclarer la variable avec ce type concret spécifique.

La situation est différente lorsque vous obtenez cet objet ailleurs, par exemple IFragment fragment = SomeFactory.GiveMeFragments(...);. Ici, l’usine devrait normalement renvoyer le type le plus abstrait possible et le code descendant ne devrait pas dépendre des détails d’implémentation.


18
"bien qu'un peu abstrait". Badumtish.
rosuav

1
cela n'aurait-il pas pu être une édition?
beppe9000

6

Il y a potentiellement des différences de performances.

Si la classe dérivée est scellée, le compilateur peut s'inscrire et optimiser ou éliminer ce qui serait autrement des appels virtuels. Il peut donc y avoir une différence de performance significative.

Exemple: ToStringest un appel virtuel, mais Stringest scellé. Sur String, ToStringest un non-op, donc si vous déclarez que objectc'est un appel virtuel, si vous déclarez un Stringcompilateur connaît la classe dérivée n'est substituée la méthode parce que la classe est fermée, de sorte que est un non-op. Des considérations similaires sont applicables à ArrayListvs LinkedList.

Par conséquent, si vous connaissez le type concret de l'objet (et qu'il n'y a aucune raison d'encapsulation pour le masquer), vous devez le déclarer comme type. Comme vous venez de créer l'objet, vous connaissez le type concret.


4
Dans la plupart des cas, les différences de performances sont minimes. Et dans la plupart des cas (j'ai entendu dire que cela représentait environ 90% du temps), nous devrions oublier ces petites différences. Ce n'est que lorsque nous savons (de préférence via la mesure) que nous sommes considérés comme une section de code critique en termes de performances que nous devons nous intéresser à de telles optimisations.
Jules

Et le compilateur sait que "new Child ()" renvoie un enfant, même assigné à un parent, et tant qu'il le sait, il peut utiliser cette connaissance et appeler le code Child ().
gnasher729

+1 Également volé votre exemple dans ma réponse. = P
Nat

1
Notez qu'il existe des optimisations de performances qui rendent tout le problème discutable. HotSpot, par exemple, remarquera qu'un paramètre transmis à une fonction appartient toujours à une classe spécifique et optimisera en conséquence. Si cette hypothèse s'avère être fausse, la méthode est désoptimisée. Mais cela dépend fortement de l'environnement du compilateur / runtime. La dernière fois que j'ai vérifié le CLR, je ne pouvais même pas faire l'optimisation un peu triviale de remarquer que p Parent p = new Child()est toujours un enfant.
Voo

4

La principale différence est le niveau d'accès requis. Et chaque bonne réponse à propos de l'abstraction implique des animaux, c'est ce que l'on me dit.

Supposons que vous ayez quelques animaux - Cat Birdet Dog. Or, ces animaux ont quelques comportements communs - move(), eat()et speak(). Ils mangent tous différemment et parlent différemment, mais si j'ai besoin que mon animal mange ou parle, je ne me soucie pas vraiment de la façon dont il le fait.

Mais parfois je le fais. Je n'ai jamais été un chat ou un oiseau de garde, mais j'ai un chien de garde. Alors, quand quelqu'un s'introduit chez moi, je ne peux pas me contenter de parler pour effrayer l'intrus. J'ai vraiment besoin de mon chien pour aboyer - ceci est différent de son discours, qui est juste une trame.

Donc, dans le code qui nécessite un intrus, je dois vraiment faire Dog fido = getDog(); fido.barkMenancingly();

Mais la plupart du temps, je peux volontiers que des animaux fassent des tours où ils muent, miaulent ou tweetent pour des friandises. Animal pet = getAnimal(); pet.speak();

Donc, pour être plus concret, ArrayList a des méthodes qui Listne le font pas. Notamment, trimToSize(). Maintenant, dans 99% des cas, on s'en fiche. Le Listn'est presque jamais assez grand pour que nous nous en soucions. Mais il y a des moments où c'est. Donc, occasionnellement, nous devons spécifiquement demander à un ArrayListexécutable trimToSize()et rendre son tableau de sauvegarde plus petit.

Notez que cela ne doit pas nécessairement être fait à la construction - cela peut être fait via une méthode de retour ou même un casting si nous en sommes absolument sûrs.


Je ne comprends pas pourquoi cela pourrait avoir un vote négatif. Cela semble canonique, personnellement. Sauf pour l'idée d'un chien de garde programmable ...
corsiKa

Pourquoi devais-je lire des pages pour obtenir la meilleure réponse? Le classement est nul.
Basilevs

Merci pour ces mots gentils. Les systèmes d’évaluation ont des avantages et des inconvénients, et l’un de ceux-ci est que les réponses ultérieures peuvent parfois être laissées à l’abandon. Ça arrive!
CorsiKa

2

De manière générale, vous devriez utiliser

var x = new Child(); // C#

ou

auto x = new Child(); // C++

pour les variables locales, sauf s'il existe une bonne raison pour que la variable ait un type différent de celui avec lequel elle est initialisée.

(Ici, j'ignore le fait que le code C ++ devrait très probablement utiliser un pointeur intelligent. Il existe des cas précis et sans plusieurs lignes que je ne peux pas réellement connaître.)

L’idée générale est ici avec les langages qui prennent en charge la détection automatique des types de variables, son utilisation facilite la lecture du code et fait le bon choix (c’est-à-dire que le système de types du compilateur peut être optimisé autant que possible et changer l’initialisation pour qu’elle soit nouvelle. Le type fonctionne également si le plus abstrait est utilisé).


1
En C ++, les variables sont généralement créées sans le nouveau mot clé: stackoverflow.com/questions/6500313/…
Marian Spanik

1
La question ne porte pas sur C # / C ++, mais sur un problème général orienté objet dans un langage comportant au moins une forme de typage statique (c’est-à-dire qu’il serait insensé de demander à quelque chose comme Ruby). En outre, même dans ces deux langues, je ne vois pas vraiment pourquoi nous devrions le faire comme vous le dites.
AnoE

1

Bien parce qu'il est facile à comprendre et à modifier ce code:

var x = new Child(); 
x.DoSomething();

Bien car il communique l'intention:

Parent x = new Child(); 
x.DoSomething(); 

Ok, parce que c'est commun et facile à comprendre:

Child x = new Child(); 
x.DoSomething(); 

La seule option qui soit vraiment mauvaise est d'utiliser Parentsi seulement Childa une méthode DoSomething(). C'est mauvais parce que cela communique mal l'intention:

Parent x = new Child(); 
(x as Child).DoSomething(); // DON'T DO THIS! IF YOU WANT x AS CHILD, STORE x AS CHILD

Voyons maintenant un peu plus le cas de la question:

Parent x = new Child(); 
x.DoSomething(); 

Formulons cela différemment, en faisant de la seconde moitié un appel de fonction:

WhichType x = new Child(); 
FunctionCall(x);

void FunctionCall(WhichType x)
    x.DoSomething(); 

Cela peut être réduit à:

FunctionCall(new Child());

void FunctionCall(WhichType x)
    x.DoSomething();

Ici, je suppose qu'il est largement accepté que ce WhichTypesoit le type le plus élémentaire / abstrait permettant à la fonction de fonctionner (sauf en cas de problèmes de performances). Dans ce cas, le type approprié serait soit Parent, soit quelque chose en Parentdécoule.

Cette ligne de raisonnement explique pourquoi utiliser le type Parentest un bon choix, mais cela ne va pas dans le temps, les autres choix sont mauvais (ils ne le sont pas).


1

tl; dr - L' utilisation deChildoverParentest préférable dans le champ d'application local. Cela facilite non seulement la lisibilité, mais il est également nécessaire de s'assurer que la résolution de la méthode surchargée fonctionne correctement et contribue à une compilation efficace.


Au niveau local,

Parent obj = new Child();  // Works
Child  obj = new Child();  // Better
var    obj = new Child();  // Best

Conceptuellement, il s’agit de conserver le plus d’informations possibles. Si nous rétrogradons Parent, nous supprimons essentiellement des informations de type qui auraient pu être utiles.

Conserver les informations de type complètes présente quatre avantages principaux:

  1. Fournit plus d'informations au compilateur.
  2. Fournit plus d'informations au lecteur.
  3. Code plus propre et plus normalisé.
  4. Rend la logique du programme plus modifiable.

Avantage 1: Plus d'informations au compilateur

Le type apparent est utilisé dans la résolution de méthode surchargée et dans l'optimisation.

Exemple: résolution de méthode surchargée

main()
{
    Parent parent = new Child();
    foo(parent);

    Child  child  = new Child();
    foo(child);
}
foo(Parent arg) { /* ... */ }  // More general
foo(Child  arg) { /* ... */ }  // Case-specific optimizations

Dans l'exemple ci-dessus, les deux foo()appels fonctionnent , mais dans un cas, nous obtenons la meilleure résolution de méthode surchargée.

Exemple: optimisation du compilateur

main()
{
    Parent parent = new Child();
    var x = parent.Foo();

    Child  child  = new Child();
    var y = child .Foo();
}
class Parent
{
    virtual         int Foo() { return 1; }
}
class Child : Parent
{
    sealed override int Foo() { return 2; }
}

Dans l'exemple ci-dessus, les deux .Foo()appels appellent finalement la même overrideméthode qui renvoie 2. Dans le premier cas, il existe une recherche de méthode virtuelle pour trouver la méthode correcte; cette recherche de méthode virtuelle n'est pas nécessaire dans le deuxième cas puisque celle-ci est sealed.

Nous remercions @Ben qui a fourni un exemple similaire dans sa réponse .

Avantage 2: Plus d'informations au lecteur

Connaître le type exact, c.-à-d. Child, Fournit plus d'informations à quiconque lit le code, ce qui permet de voir plus facilement ce que fait le programme.

Bien sûr, peut - être qu'il n'a pas d' importance au code réel puisque les deux parent.Foo();et du child.Foo();sens, mais pour quelqu'un de voir le code pour la première fois, plus d' informations est tout simplement utile.

De plus, en fonction de votre environnement de développement, l'EDI peut être en mesure de fournir plus d'info-bulles et de métadonnées utiles Childque sur Parent.

Avantage 3: code plus propre et plus normalisé

La plupart des exemples de code C # que j'ai vus récemment utilisent var, qui est essentiellement un raccourci pour Child.

Parent obj = new Child();  // Sub-optimal
Child  obj = new Child();  // Optimal, but anti-pattern syntax
var    obj = new Child();  // Optimal, clean, patterned syntax "everyone" uses now

Voir une vardéclaration de non- déclaration ne fait que regarder; s'il y a une raison situationnelle pour cela, génial, mais sinon, cela semble anti-modèle.

// Clean:
var foo1 = new Person();
var foo2 = new Job();
var foo3 = new Residence();

// Staggered:
Person foo1 = new Person();
Job foo2 = new Job();
Residence foo3 = new Residence();   

Avantage 4: logique de programme plus modifiable pour le prototypage

Les trois premiers avantages étaient les plus importants. celui-ci est beaucoup plus situationnel.

Néanmoins, pour les personnes qui utilisent du code comme d’autres utilisent Excel, nous modifions constamment notre code. Peut-être n’avons-nous pas besoin d’appeler une méthode unique Childdans cette version du code, mais nous pourrions réutiliser ou reformater le code ultérieurement.

L'avantage d'un système de type fort est qu'il nous fournit certaines méta-données sur la logique de notre programme, rendant ainsi plus évidentes les possibilités. Ceci est incroyablement utile dans le prototypage, il est donc préférable de le conserver lorsque cela est possible.

Sommaire

L'utilisation de la Parentrésolution des méthodes surchargée, empêche certaines optimisations du compilateur, supprime les informations du lecteur et rend le code plus laid.

L'utilisation varest vraiment la voie à suivre. C'est rapide, propre, modelé, et aide le compilateur et l'IDE à faire leur travail correctement.


Important : Cette réponse concerne environParentvs.Childdans la portée locale d'une méthode. La question deParentvsChildest très différente pour les types de retour, les arguments et les champs de classe.


2
J'ajouterais que cela Parent list = new Child()n'a pas beaucoup de sens. Le code qui crée directement l'instance concrète d'un objet (par opposition à l'indirection via des fabriques, des méthodes d'usine, etc.) contient des informations parfaites sur le type en cours de création, il peut être nécessaire d'interagir avec l'interface concrète pour configurer le nouvel objet, etc. La portée locale n’est pas celle où vous obtenez de la flexibilité. La flexibilité est obtenue lorsque vous exposez cet objet nouvellement créé à un client de votre classe à l'aide d'une interface plus abstraite (les usines et les méthodes d'usine en sont un exemple parfait)
crizzis

"tl; dr Utiliser enfant sur parent est préférable dans la portée locale. Cela ne facilite pas seulement la lisibilité," - pas nécessairement ... si vous n'utilisez pas les détails de l'enfant, cela ajoutera simplement plus de détails que nécessaire. "mais il est également nécessaire de veiller à ce que la résolution de méthode surchargée fonctionne correctement et contribue à permettre une compilation efficace." - Cela dépend beaucoup du langage de programmation. Il y en a beaucoup qui n'ont aucun problème qui en découle.
AnoE

1
Avez - vous une source Parent p = new Child(); p.doSomething();et Child c = new Child; c.doSomething();avoir un comportement différent? Peut-être que c'est une bizarrerie dans certaines langues, mais c'est supposé être que l'objet contrôle le comportement, pas la référence. C'est la beauté de l'héritage! Tant que vous pouvez faire confiance aux implémentations enfants pour remplir le contrat de l'implémentation parent, vous pouvez continuer.
CorsiKa

@corsiKa Ouais, c'est possible de plusieurs façons. Deux exemples rapides sont ceux où les signatures de méthode sont écrasées, par exemple avec le newmot - clé en C # , et lorsqu'il y a plusieurs définitions, par exemple avec l' héritage multiple en C ++ .
Nat

@corsiKa Juste un troisième exemple rapide (parce que je pense que c'est un contraste net entre C ++ et C #), C # a un problème comme celui de C ++, où vous pouvez également obtenir ce comportement à l' aide de méthodes à interface explicite . C’est drôle parce que des langages comme C # utilisent des interfaces au lieu de plusieurs héritages pour éviter ces problèmes, mais en adoptant des interfaces, ils ont toujours une partie du problème - mais, ce n’est pas aussi grave, car, dans ce scénario, il ne s'applique que à quand Parentest un interface.
Nat

0

Étant donné que le parentType foo = new childType1();code sera limité à l’utilisation de méthodes de type parent sur foo, il pourra toutefois stocker dans fooune référence tout objet dont le type dérive de parent--not seulement des instances de childType1. Si la nouvelle childType1instance est la seule chose pour laquelle fooune référence sera jamais conservée, il peut être préférable de déclarer foole type comme childType1. D'un autre côté, if foopourrait avoir besoin de contenir des références à d'autres types, le déclarer comme parentTyperendant cela possible.


0

Je suggère d'utiliser

Child x = new Child();

au lieu de

Parent x = new Child();

Ce dernier perd des informations alors que l'ancien ne le fait pas.

Avec le premier, vous pouvez faire tout ce que vous pouvez faire avec le dernier, mais pas l'inverse.


Pour élaborer davantage le pointeur, vous devez disposer de plusieurs niveaux d’héritage.

Base-> DerivedL1->DerivedL2

Et vous voulez initialiser le résultat de new DerivedL2()à une variable. L'utilisation d'un type de base vous laisse la possibilité d'utiliser Baseou DerivedL1.

Vous pouvez utiliser

Base x = new DerivedL2();

ou

DerivedL1 x = new DerivedL2();

Je ne vois aucun moyen logique de préférer l'un à l'autre.

Si tu utilises

DerivedL2 x = new DerivedL2();

il n'y a rien à discuter.


3
Pouvoir faire moins si on n'a pas besoin de faire plus est une bonne chose, non?
Peter

1
@ Peter, la capacité de faire moins n'est pas limitée. C'est toujours possible. Si la capacité de faire plus est limitée, cela pourrait être un point sensible.
R Sahu

Mais perdre des informations est parfois une bonne chose. En ce qui concerne votre hiérarchie, la plupart des gens voteraient probablement pour Base(en supposant que cela fonctionne). Vous préférez le plus spécifique, AFAIK la majorité préfère le moins spécifique. Je dirais que pour les variables locales, cela n'a guère d'importance.
Maaartinus

@ Maaartinus, je suis surpris que la majorité des utilisateurs préfèrent perdre des informations. Je ne vois pas les avantages de perdre des informations aussi clairement que les autres.
R Sahu

Lorsque vous déclarez, Base x = new DerivedL2();vous êtes le plus contraint, ce qui vous permet d’échanger un (grand) enfant avec un minimum d’effort. De plus, vous voyez immédiatement que c'est possible. +++ Sommes-nous d'accord pour dire que void f(Base)c'est mieux que void f(DerivedL2)?
Maaartinus

0

Je suggérerais toujours de retourner ou de stocker les variables comme type le plus spécifique, mais d'accepter les paramètres comme types les plus larges.

Par exemple

<K, V> LinkedHashMap<K, V> keyValuePairsToMap(List<K> keys, List<V> values) {
   //...
}

Les paramètres sont List<T>un type très général. ArrayList<T>, LinkedList<T>et d’autres pourraient tous être acceptés par cette méthode.

Fait important, le type de retour est LinkedHashMap<K, V>, pas Map<K, V>. Si quelqu'un veut assigner le résultat de cette méthode à a Map<K, V>, il peut le faire. Il indique clairement que ce résultat est plus qu'une carte, mais une carte avec un ordre défini.

Supposons que cette méthode retourne Map<K, V>. Si l'appelant le souhaitait LinkedHashMap<K, V>, il devrait effectuer une vérification de type, une conversion et une gestion des erreurs, ce qui est vraiment fastidieux.


La même logique qui vous oblige à accepter un type large en tant que paramètre doit également s'appliquer au type de retour. Si le but de la fonction est de mapper les clés sur des valeurs, il devrait renvoyer un Mapnon LinkedHashMap, vous voudrez seulement renvoyer un LinkedHashMappour une fonction similaire keyValuePairsToFifoMapou quelque chose comme ça. Plus large est préférable jusqu'à ce que vous ayez une raison précise de ne pas le faire.
CorsiKa

@corsiKa Hmm Je suis d'accord pour dire que c'est un meilleur nom, mais ce n'est qu'un exemple. Je ne suis pas d'accord pour dire que (en général) élargir votre type de retour est préférable. Votre suppression artificielle d'informations de type, sans aucune justification. Si vous envisagez que votre utilisateur doive raisonnablement redéfinir le type large que vous lui avez fourni, vous ne leur rendrez pas service.
Alexander - Réintégrer Monica

"sans aucune justification" - mais il y a une justification! Si je peux me permettre de renvoyer un type plus large, cela facilitera la refactorisation plus tard. Si quelqu'un A BESOIN du type spécifique en raison d'un comportement sur celui-ci, je suis tout à fait pour renvoyer le type approprié. Mais je ne veux pas être enfermé dans un type spécifique si ce n'est pas mon cas. Je voudrais être libre de modifier les détails de ma méthode sans nuire à ceux qui la consomment, et si je le change, disons LinkedHashMapà TreeMapquelle qu'en soit la raison, maintenant j'ai beaucoup de choses à changer.
CorsiKa

En fait, je suis d’accord avec ça jusqu’à la dernière phrase. Vous avez changé de LinkedHashMapen TreeMapn'est pas un changement trivial, tel que passer de ArrayListà LinkedList. C'est un changement d'importance sémantique. Dans ce cas, vous voulez que le compilateur renvoie une erreur, forçant les utilisateurs de la valeur de retour à remarquer le changement et à prendre les mesures appropriées.
Alexander - Réintégrer Monica le

Mais cela peut être un changement trivial si tout ce qui vous préoccupe est le comportement de la carte (qui, si vous le pouvez, vous devriez le faire.) Si le contrat est "vous obtiendrez une carte de valeur clé" et que l’ordre n’a pas la plupart des cartes), peu importe qu’il s’agisse d’un arbre ou d’un hachage. Par exemple, Thread#getAllStackTraces()- renvoie un Map<Thread,StackTraceElement[]>message plutôt qu'un message HashMapspécifique afin qu'ils puissent le modifier à Mapleur guise.
CorsiKa
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.