web-dev-qa-db-fra.com

Est-ce une mauvaise pratique de programmation de passer des paramètres en tant qu'objets?

Donc, nous avons un gars qui aime écrire des méthodes qui prennent des objets comme paramètres, afin qu'ils puissent être "très flexibles". Ensuite, en interne, il effectue soit la coulée directe, la réflexion ou la surcharge de méthode pour gérer les différents types.

J'ai l'impression que c'est une mauvaise pratique, mais je ne peux pas expliquer exactement pourquoi, sauf que cela rend la lecture plus difficile. Y a-t-il d'autres raisons plus concrètes pour lesquelles ce serait une mauvaise pratique? Quels seraient-ils?


Ainsi, certaines personnes ont demandé un exemple. Il a une interface définie, quelque chose comme:

public void construct(Object input, Object output);

Et il prévoit de les utiliser en en mettant plusieurs dans une liste, donc il construit en quelque sorte des bits et les ajoute à l'objet de sortie, comme ceci:

for (ConstructingThing thing : constructingThings)
    thing.construct(input, output);
return output;

Ensuite, dans la chose qui implémente construct, il y a une chose de réflexion branlante qui trouve la bonne méthode qui correspond à l'entrée/sortie et l'appelle, en passant l'entrée/sortie.

Je n'arrête pas de lui dire que cela fonctionne, je comprends que cela fonctionne, mais il en va de même de tout mettre dans une seule classe. Nous parlons de réutilisation et de maintenabilité, et je pense qu'il se contraint et a moins de réutilisation qu'il ne le pense. La maintenabilité, bien que probablement élevée pour lui en ce moment, sera probablement très faible pour lui et pour quiconque à l'avenir.

101
Risser

D'un point de vue pratique je vois ces problèmes:

  • Une multitude d'erreurs de type d'exécution possibles - à moins de nombreuses vérifications de type dynamiques qui pourraient être évitées avec le Java inclus un vérificateur de type fort).
  • Beaucoup de lancers inutiles
  • Difficulté à comprendre ce que fait une méthode par sa signature

D'un point de vue théorique je vois ces problèmes:

  • Un manque de contrats de l'interface d'une classe. Si tous les paramètres sont de type Object, vous ne déclarez rien d'information aux classes clientes.
  • Un manque de possibilités de surcharge
  • L'inexactitude du remplacement. Vous pouvez remplacer une méthode et modifier ses types de paramètres, rompant ainsi tout ce qui est lié à l'héritage.
104
Jack

La méthode viole le principe de substitution Liskov .

Si une méthode accepte un paramètre de type Object, je dois m'attendre à ce que tout objet que je passe à la méthode fonctionne.

Il semble que seules certaines sous-classes de Object soient autorisées. La seule façon de savoir quels types sont autorisés est de connaître les détails de la méthode. En d'autres termes, la méthode est moins flexible que celle annoncée.

110
Aaron Kurtzhals

Je considérerais les implications directes suivantes:

lisibilité

Je suppose que travailler avec lui n'est pas exactement l'expérience la plus agréable au monde. Vous ne savez jamais quel sera le type à la fin ni ce qui arrivera aux paramètres. Je doute que vous appréciez perdre votre temps avec ça.

Signature du type

Dans Java tout est un objet. Que fait donc cette méthode à la fin?

Vitesse

La réflexion, les projections, les vérifications de type, etc. contribuent tous à une performance plus lente. Pourquoi? Parce qu'à son avis, cela rend les choses flexibles.

Contre la nature

Java a été fortement typé depuis sa naissance. Dites-lui de commencer à écrire JavaScript ou Python ou Ruby s'il veut un dialecte typé dynamiquement. Son expérience est probablement fortement basée sur l'un d'eux).

Flexibilité

Pour utiliser le même terme que le collègue en question, Java a déjà compris la flexibilité depuis longtemps. Il est inutile de le regarder avec les lunettes de cheval mises par un langage typé dynamiquement.

Les interfaces, le polymorphisme, la covariance et la contre-variance, les génériques, l'héritage, les classes abstraites et surcharge de méthode ne sont que quelques-unes des choses qui contribuent à la soi-disant "flexibilité". La flexibilité est donnée par le fait qu'il est trivial de spécialiser pratiquement n'importe quoi d'un type donné à un autre (le cas habituel signifiant la spécialisation d'un type supérieur direct ou d'un type inférieur).

Documentation

L'un des principaux objectifs de la programmation est d'écrire du code réutilisable et stable. À quoi ressemble la documentation de votre exemple? Quel est le temps d'apprentissage pour un autre développeur avant de pouvoir utiliser ce genre de choses? Je suggérerais à votre gars d'écrire du code assembleur auto-modifiant et de rester responsable de cela.

53
flavian

Il y a déjà beaucoup de bonnes réponses ici. Je veux ajouter une pensée secondaire:

Ce modèle est très probablement un "odeur de code."

En d'autres termes, si vous devez passer un objet à votre méthode pour une flexibilité "ultime", vous avez très probablement une méthode très mal définie . Le fait d'avoir des paramètres fortement saisis indique un "contrat" ​​pour l'appelant (j'ai besoin de choses qui ressemblent à "ceci"). L'impossibilité de spécifier un tel contrat signifie très probablement que votre méthode en fait trop ou a une portée mal définie.

41
jtahlborn

Il supprime la vérification de type effectuée par le compilateur et peut entraîner davantage d'exceptions d'exécution lorsque l'objet est converti en type concret. Ainsi, l'utilisateur final verra l'exception non gérée, à moins que les transtypages non valides soient tous interceptés et traités correctement.

22

Je suis d'accord avec les autres réponses que c'est généralement une mauvaise pratique.

Il y a un cas spécifique où je trouve que le passage d'un objet peut être supérieur: lorsque vous avez finalement affaire à des chaînes, par exemple si vous créez du XML ou du JSON. Dans ce cas, au lieu de quelque chose comme:

public void addAttribute(String s) { 
  //... add s to the XML
}

Je préfère:

public void addAttribute(Object o) {
   String s = String.valueOf(o);  // perhaps check for null explicitly
   // now add s to the XML
}

Cela enregistre une infinité de String.valueOf Dans le code d'appel. En outre, cela fonctionne bien avec les vararg. Et, avec l'autoboxing, vous pouvez passer ints, floats, etc ... Puisque tous les objets ont une toString(), cela, sans doute, ne viole pas Liskov etc. .

16
user949300

oui. Java est censé être strictement typecasted. Mais avoir Object comme paramètre va virtuellement enfreindre cette règle. De plus, cela rendra votre programme plus sujet aux bogues.

Et c'est l'une des principales raisons pour lesquelles generics a été introduit.

5
Ankit

Être flexible et capable de gérer plusieurs types de données est exactement ce pour quoi la surcharge de méthode et les interfaces ont été créées. Java possède déjà les fonctionnalités qui simplifient la gestion de ces situations tout en garantissant la sécurité du temps de compilation via la vérification de type.

De même, s'il est impossible de définir de manière précise sur quelles données la méthode opère, alors cette méthode peut faire trop et doit être décomposée en méthodes plus petites.

J'aime une règle empirique que j'ai lue (dans Code Complete, je pense). La règle stipule que vous devriez être capable de dire ce qu'une fonction ou une méthode fait simplement en lisant son nom. S'il est vraiment difficile de nommer clairement une fonction\méthode en utilisant cette idée (ou de devoir utiliser Et dans le nom), alors la fonction/méthode en fait vraiment trop.

5
Peter Smith

En fait, j'ai dû travailler sur un package comme ça une fois, c'était un package GUI - le concepteur a dit qu'il venait d'un arrière-plan Pascal et pensait que ne faire circuler rien d'autre qu'une classe de base partout était une idée géniale.

En 20 ans, travailler avec cette boîte à outils a été l'une des expériences les plus ennuyeuses de ma carrière.

Tant que vous ne devez pas travailler avec du code comme celui-ci, vous ne remarquez pas à quel point vous comptez sur les types de données dans vos paramètres pour vous guider. Si j'ai dit que j'avais une méthode "connect (Object dest)", qu'est-ce que cela signifie? Même si vous voyez "connect (site Web Object)", cela ne nous aide pas beaucoup, mais "connect (site Web String)" ou "connect (site Web URL)", vous n'avez même pas besoin d'y penser.

Vous pourriez penser que "se connecter (site Web Object)" est une excellente idée car il peut prendre une chaîne ou une URL, mais cela signifie simplement que je dois deviner quels cas le programmeur a décidé de prendre en charge et lesquels il n'a pas pris. Au mieux, il déplace la plupart des essais et erreurs du développement du temps de codage au temps de compilation, au pire, il rend l'ensemble du processus de développement boueux et ennuyeux.

I (Et la plupart des programmeurs Java, je crois)) trouvent constamment des chaînes d'appels API en examinant un groupe de paramètres aux constructeurs pour voir quel constructeur utiliser, puis comment construire les objets dont vous avez besoin pour passer jusqu'à ce que vous arriviez à des objets que vous avez ou que vous pouvez fabriquer - c'est comme assembler un puzzle au lieu de chercher dans la documentation pourrie des exemples incomplets (mon expérience pré-Java avec C).

Bien que le code de construction comme un puzzle semble ad-hock, il fonctionne incroyablement bien - tout le temps.

Je sais que cela a été répondu, mais c'était une expérience suffisamment ennuyeuse pour que je me sente vraiment motivé à faire en sorte que le moins de personnes possible aient besoin de recommencer.

2
Bill K

Tout d'abord, si vous passez les paramètres en tant qu'objet concret, toute erreur d'utilisation du mauvais objet (instance d'une classe non attendue) sera détectée au moment de la compilation et sera donc plus rapide à corriger. Dans l'autre cas, l'erreur sera affichée lors de l'exécution et sera plus difficile à détecter et à corriger.

Deuxièmement, le code de vérification de la classe réelle de l'instance, de la réflexion, etc., sera plus lent qu'un appel direct

2
Evans

La règle que je suis est ... les définitions de paramètres doivent être aussi spécifiques que nécessaire par la méthode, ni plus ni moins.

Si tout ce dont j'ai besoin est d'itérer un énumérable, le paramètre est défini comme un énumérable, pas un tableau ou une liste. Cela permet d'utiliser n'importe quel type d'énumérable, en évitant de convertir en un type spécifique.

Je ne le définirai pas non plus comme un objet car j'ai besoin d'un énumérable. Tout objet qui n'est pas un énumérable ne fonctionnera pas dans cette méthode. Il est difficile de lire une signature de méthode et de ne pas savoir ce que cela signifie, et seuls les tests d'exécution peuvent trouver l'erreur.

2
drewburlingame

Ce qu'il fait, c'est jeter tous les avantages d'avoir un système de type statique.

Simplement: Il est beaucoup moins clair de savoir ce que font ces méthodes, à la fois pour un compilateur et pour les personnes qui lisent le code.

L'une des principales raisons pour lesquelles un système de type statique est d'avoir des garanties à la compilation sur ce que fait un programme . Le compilateur peut vérifier qu'un morceau de code est utilisé correctement et sinon, émettre une erreur au moment du compilateur. Les méthodes comme celles que votre collègue écrit sont bien plus sujettes aux erreurs d'exécution, car les vérifications au moment du compilateur sont manquantes. Soyez prêt à exécuter les erreurs d'exécution et à écrire beaucoup plus de tests.

Un autre gros problème est qu'il n'est pas du tout clair de ce que fait une méthode. Lire un tel code, le maintenir ou l'utiliser est une grande douleur.

Java a ajouté des génériques pour donner des garanties de compilation plus solides sur le code, Scala va encore plus loin avec son approche fonctionnelle, et un système de type sophistiqué avec des types covariants et contre-variantes. Votre collègue revient en arrière contre cette ligne de développement, jetant tous ses avantages.

S'il utilisait à la place des méthodes correctement typées et surchargées, il (et vous tous) gagnerait:

  • Temps du compilateur vérifiant l'utilisation correcte de ces méthodes.
  • Indication claire des combinaisons d'arguments autorisées et non autorisées.
  • La possibilité de documenter correctement et séparément chacune de ces combinaisons.
  • Lisibilité et maintenabilité du code.
  • Cela l'aiderait à diviser ses méthodes en parties plus petites, qui sont plus faciles à déboguer, tester et maintenir. (Voir aussi Comment convaincre votre collègue développeur d'écrire des méthodes courtes? .)
2
Petr Pudlák

Pour donner quelque chose à un type, donnez:

  1. Utilise une interface pour coder avec une description sémantique.
  2. Les compilateurs ont une interface à analyser pour permettre à leurs optimiseurs d'augmenter le temps d'exécution du code ou de réduire la taille du code plus facilement.
  3. Liaison précoce (statique) qui détecte les erreurs de code au début du cycle de développement.

L'utilisation d'un seul objet ne transmet presque aucune information, supprimant ainsi toutes ces fonctionnalités qui ont été mises dans le langage pour les raisons énumérées ci-dessus.

En outre, le système de type a été conçu pour être flexible avec ses relations is-a permettant toutes ces conditions. Utilisez-le donc à votre avantage. Ne le gaspillez pas en créant du code impossible à maintenir.

Il peut y avoir une raison particulière de faire quelque chose comme ça à un moment donné, mais le faire de façon générique tout le temps est une mauvaise pratique. Si vous le faites, demandez à d'autres d'examiner le problème et voyez si cela peut être fait d'une autre manière. Dans la majorité des cas (99,999%) cela peut l'être.

Dans ce 0,001% que cela ne peut pas être, au moins vous êtes tous à bord et savez ce qui se passe (bien que vous ayez probablement juste besoin d'un regard neuf sur le problème).

1
Adrian

Oui, c'est une mauvaise pratique car cela ne garantit pas que la méthode sera en mesure de faire ce dont elle a besoin pour tout apport particulier transmis. L'objectif de créer une méthode qui est largement utilisable n'est pas un mauvais objectif, mais il y a mieux façons de l'accomplir.

Pour ce cas particulier, cela ressemble à une situation parfaite pour l'utilisation des interfaces. Une interface garantirait que les objets transmis à la méthode fournissent les informations nécessaires et permettent d'éviter le casting. Il y a des situations où cela ne fonctionnera pas bien (comme si cela devait fonctionner avec des classes scellées ou de base auxquelles vous ne pouvez pas ajouter d'implémentation d'interface), mais dans l'ensemble une méthode ne devrait prendre que des entrées valides pour elle comme paramètres.

1
AJ Henderson

Excellentes réponses en profondeur jusqu'à présent, je dois vraiment dire qu'avant même d'entrer dans la philosophie et la méthodologie de haut niveau, elles sont paresseuses, courtes et simples.

Ils ne fournissent pas de bonnes informations dans leur code pour les autres à maintenir et à comprendre, et ils vont à l'encontre de la conception du langage de programmation à portée de main. J'adore Perl, j'adore Objective-C. Je ne fais cependant pas d'efforts pour plier Java, C #, ou C++ d'ailleurs, vers mes préférences au détriment d'avoir du code maintenable, lisible et efficace.

Oui, s'ils doivent changer le fonctionnement d'une fonction ou les entrées, ils enregistrent ici quelques modifications de ligne et quelques étapes de refactoring. Cependant, le coût pour les autres d'avoir à maintenir le code est plus élevé, sans parler de la plupart des IDE modernes tels qu'Eclipse et Xcode peuvent gérer le refactoring automatiquement avec une simple commande clic droit. De plus, comme d'autres l'ont mentionné, le coût de traitement augmente avec chaque conversion, avec chaque type d'objet inconnu qui est passé, et j'imagine que les accélérations intégrées de base que l'exécution exécuterait en sachant que les classes appropriées sont également jetées par la fenêtre.

Le sucre de syntaxe est toujours meilleur que le sel pour le développeur, mais une alimentation équilibrée des deux assure la santé du projet et des autres personnes impliquées (client, application, autres développeurs, etc.)

0
FaultyJuggler