Quelle est la pratique généralement acceptée entre ces deux cas:
function insertIntoDatabase(Account account, Otherthing thing) {
database.insertMethod(account.getId(), thing.getId(), thing.getSomeValue());
}
ou
function insertIntoDatabase(long accountId, long thingId, double someValue) {
database.insertMethod(accountId, thingId, someValue);
}
En d'autres termes, est-il généralement préférable de faire circuler des objets entiers ou simplement les champs dont vous avez besoin?
Réponses:
Ni l'un ni l'autre n'est généralement meilleur que l'autre. C'est un jugement que vous devez faire au cas par cas.
Mais dans la pratique, lorsque vous êtes en mesure de prendre cette décision, c'est parce que vous décidez quelle couche de l'architecture globale du programme doit diviser l'objet en primitives, vous devez donc penser à l'appel entier pile , pas seulement cette méthode dans laquelle vous êtes actuellement. Vraisemblablement, la décomposition doit être effectuée quelque part, et cela n'aurait pas de sens (ou ce serait inutilement sujet aux erreurs) de le faire plus d'une fois. La question est de savoir où cet endroit devrait être.
La façon la plus simple de prendre cette décision est de réfléchir au code qui doit ou ne doit pas être modifié si l'objet est modifié . Développons légèrement votre exemple:
contre
Dans la première version, le code de l'interface utilisateur passe aveuglément l'
data
objet et c'est au code de la base de données d'en extraire les champs utiles. Dans la deuxième version, le code de l'interface utilisateur décompose l'data
objet en ses champs utiles et le code de la base de données les reçoit directement sans savoir d'où ils viennent. L'implication clé est que, si la structure de l'data
objet devait changer d'une manière ou d'une autre, la première version ne nécessiterait que le code de la base de données pour changer, tandis que la deuxième version ne nécessiterait que le code de l'interface utilisateur pour changer . Lequel de ces deux est correct dépend en grande partie du type de données quedata
contient l' objet, mais c'est généralement très évident. Par exemple, sidata
est une chaîne fournie par l'utilisateur comme "20/05/1999", il devrait appartenir au code UI de le convertir en unDate
type approprié avant de le transmettre.la source
Ce n'est pas une liste exhaustive, mais tenez compte de certains des facteurs suivants pour décider si un objet doit être passé à une méthode comme argument:
L'objet est-il immuable? La fonction est-elle «pure»?
Les effets secondaires sont une considération importante pour la maintenabilité de votre code. Lorsque vous voyez du code avec beaucoup d'objets avec état mutables circulant partout, ce code est souvent moins intuitif (de la même manière que les variables d'état globales peuvent souvent être moins intuitives), et le débogage devient souvent plus difficile et plus long - consommant.
En règle générale, assurez-vous, dans la mesure du possible, que tous les objets que vous passez à une méthode sont clairement immuables.
Évitez (encore une fois, dans la mesure du possible) toute conception selon laquelle l'état d'un argument devrait être modifié à la suite d'un appel de fonction - l'un des arguments les plus solides pour cette approche est le principe du moindre étonnement ; c'est-à-dire que quelqu'un qui lit votre code et qui voit un argument passé dans une fonction est «moins susceptible» de s'attendre à ce que son état change après le retour de la fonction.
Combien d'arguments la méthode a-t-elle déjà?
Les méthodes avec des listes d'arguments excessivement longues (même si la plupart de ces arguments ont des valeurs par défaut) commencent à ressembler à une odeur de code. Parfois, de telles fonctions sont cependant nécessaires et vous pourriez envisager de créer une classe dont le seul but est d'agir comme un objet de paramètre .
Cette approche peut impliquer une petite quantité de mappage de code passe-partout supplémentaire de votre objet `` source '' à votre objet de paramètre, mais c'est un coût assez faible en termes de performances et de complexité, cependant, et il y a un certain nombre d'avantages en termes de découplage et l'immuabilité des objets.
L'objet transmis appartient-il exclusivement à une "couche" dans votre application (par exemple, un ViewModel ou une entité ORM?)
Pensez à la séparation des préoccupations (SoC) . Parfois, vous demander si l'objet "appartient" à la même couche ou au même module dans lequel votre méthode existe (par exemple, une bibliothèque de wrappers d'API roulée à la main ou votre couche de logique métier principale, etc.) peut indiquer si cet objet doit vraiment être transmis à cette méthode.
SoC est une bonne base pour écrire du code modulaire propre et faiblement couplé. par exemple, un objet entité ORM (mappage entre votre code et votre schéma de base de données) ne devrait idéalement pas être transmis dans votre couche métier, ou pire dans votre couche présentation / interface utilisateur.
Dans le cas de la transmission de données entre des «couches», il est généralement préférable de transmettre des paramètres de données simples à une méthode plutôt que de passer un objet de la «mauvaise» couche. Bien que ce soit probablement une bonne idée d'avoir des modèles distincts qui existent dans la couche «droite» sur lesquels vous pouvez mapper à la place.
La fonction elle-même est-elle simplement trop grande et / ou complexe?
Lorsqu'une fonction a besoin de beaucoup d'éléments de données, il peut être utile de déterminer si cette fonction assume trop de responsabilités; rechercher des opportunités potentielles de refactorisation en utilisant des objets plus petits et des fonctions plus courtes et plus simples.
La fonction doit-elle être un objet de commande / requête?
Dans certains cas, la relation entre les données et la fonction peut être étroite; dans ces cas, déterminez si un objet de commande ou un objet de requête serait approprié.
L'ajout d'un paramètre d'objet à une méthode force-t-il la classe conteneur à adopter de nouvelles dépendances?
Parfois, l'argument le plus fort pour les arguments "Plain old data" est simplement que la classe de réception est déjà parfaitement autonome, et l'ajout d'un paramètre d'objet à l'une de ses méthodes polluerait la classe (ou si la classe est déjà polluée, alors elle aggraver l'entropie existante)
Avez-vous vraiment besoin de contourner un objet complet ou avez-vous seulement besoin d'une petite partie de l'interface de cet objet?
Considérez le principe de ségrégation d'interface en ce qui concerne vos fonctions - c'est-à-dire que lorsque vous passez un objet, il ne devrait dépendre que des parties de l'interface de cet argument dont il (la fonction) a réellement besoin.
la source
Ainsi, lorsque vous créez une fonction, vous déclarez implicitement un contrat avec du code qui l'appelle. "Cette fonction prend cette information et la transforme en cette autre chose (éventuellement avec des effets secondaires)".
Donc, si votre contrat doit logiquement être avec les objets (quelle que soit leur implémentation), ou avec les champs qui se trouvent justement faire partie de ces autres objets. Vous ajoutez le couplage de toute façon, mais en tant que programmeur, c'est à vous de décider où il appartient.
En général , si ce n'est pas clair, privilégiez les plus petites données nécessaires au fonctionnement de la fonction. Cela signifie souvent passer uniquement les champs, car la fonction n'a pas besoin des autres éléments trouvés dans les objets. Mais parfois, prendre tout l'objet est plus correct car il en résulte moins d'impact lorsque les choses changent inévitablement à l'avenir.
la source
Ça dépend.
Pour élaborer, les paramètres que votre méthode accepte doivent correspondre sémantiquement à ce que vous essayez de faire. Considérons une
EmailInviter
et ces trois implémentations possibles d'uneinvite
méthode:Passer un
String
où vous devez passer unEmailAddress
est défectueux car toutes les chaînes ne sont pas des adresses e-mail. LaEmailAddress
classe correspond mieux sémantiquement au comportement de la méthode. Cependant, le passage dans unUser
est également imparfait car pourquoi diable devrait-onEmailInviter
se limiter à inviter des utilisateurs? Et les entreprises? Que faire si vous lisez des adresses e-mail à partir d'un fichier ou d'une ligne de commande et qu'elles ne sont pas associées à des utilisateurs? Listes de diffusion? La liste continue.Il y a quelques signes d'avertissement que vous pouvez utiliser ici pour vous guider. Si vous utilisez un type de valeur simple comme
String
ouint
mais toutes les chaînes ou entiers ne sont pas valides ou s'il y a quelque chose de "spécial" à leur sujet, vous devriez utiliser un type plus significatif. Si vous utilisez un objet et que la seule chose que vous faites est d'appeler un getter, vous devriez plutôt passer directement l'objet dans le getter. Ces directives ne sont ni dures ni rapides, mais peu de directives le sont.la source
Clean Code recommande d'avoir le moins d'arguments possible, ce qui signifie que Object serait généralement la meilleure approche et je pense que cela a du sens. car
est un appel plus lisible que
la source
Faites circuler l'objet, pas son état constitutif. Cela prend en charge les principes orientés objet de l'encapsulation et du masquage des données. L'exposition des entrailles d'un objet dans diverses interfaces de méthode où il n'est pas nécessaire viole les principes de base de la POO.
Que se passe-t-il si vous modifiez les champs dans
Otherthing
? Vous pouvez peut-être modifier un type, ajouter un champ ou supprimer un champ. Maintenant, toutes les méthodes comme celle que vous mentionnez dans votre question doivent être mises à jour. Si vous passez autour de l'objet, il n'y a aucun changement d'interface.La seule fois où vous devez écrire une méthode acceptant des champs sur un objet est lors de l'écriture d'une méthode pour récupérer l'objet:
Au moment de faire cet appel, le code appelant n'a pas encore de référence à l'objet car le point d'appel de cette méthode est d'obtenir l'objet.
la source
Otherthing
?" (1) Ce serait une violation du principe ouvert / fermé. (2) même si vous passez l'intégralité de l'objet, le code qu'il contient puis accède aux membres de cet objet (et si ce n'est pas le cas, pourquoi passer l'objet?) Se casserait quand même ...Du point de vue de la maintenabilité, les arguments doivent être clairement distinguables les uns des autres, de préférence au niveau du compilateur.
La première conception conduit à une détection précoce des bogues. La deuxième conception peut entraîner des problèmes d'exécution subtils qui n'apparaissent pas dans les tests. Par conséquent, la première conception doit être préférée.
la source
Des deux, ma préférence est la première méthode:
function insertIntoDatabase(Account account, Otherthing thing) { database.insertMethod(account.getId(), thing.getId(), thing.getSomeValue()); }
La raison en est que les modifications apportées à l'un ou l'autre des objets sur la route, tant que les modifications préservent ces getters, de sorte que la modification est transparente à l'extérieur de l'objet, vous avez moins de code à modifier et à tester et moins de chances de perturber l'application.
C'est juste mon processus de réflexion, principalement basé sur la façon dont j'aime travailler et structurer des choses de cette nature et qui se révèlent tout à fait gérables et maintenables à long terme.
Je ne vais pas entrer dans les conventions de dénomination, mais je voudrais souligner que bien que cette méthode contienne le mot "base de données", ce mécanisme de stockage peut changer en cours de route. D'après le code affiché, rien ne lie la fonction à la plate-forme de stockage de base de données utilisée - ou même s'il s'agit d'une base de données. Nous avons juste supposé parce que c'est dans le nom. Encore une fois, en supposant que ces getters sont toujours préservés, il sera facile de changer comment / où ces objets sont stockés.
Je repenserais cependant la fonction et les deux objets parce que vous avez une fonction qui dépend de deux structures d'objets, et en particulier des getters utilisés. Il semble également que cette fonction lie ces deux objets en une seule chose cumulative qui persiste. Mon instinct me dit qu'un troisième objet pourrait avoir un sens. J'aurais besoin d'en savoir plus sur ces objets et comment ils se rapportent en réalité et à la feuille de route prévue. Mais mon instinct penche dans cette direction.
En l'état actuel du code, la question se pose "Où serait ou devrait être cette fonction?" Cela fait-il partie de Account, ou OtherThing? Où est-ce que ça va?
Je suppose qu'il existe déjà un troisième objet "base de données", et je penche pour mettre cette fonction dans cet objet, puis il devient ce travail d'objets pour pouvoir gérer un compte et un autre, transformer, puis persister également le résultat .
Si vous deviez aller jusqu'à rendre ce 3e objet conforme à un modèle de mappage relationnel objet (ORM), tant mieux. Cela rendrait très évident pour quiconque travaillant avec le code de comprendre "Ah, c'est là que Account et OtherThing sont écrasés et persistent".
Mais il pourrait également être judicieux d'introduire un quatrième objet, qui gère le travail de combinaison et de transformation d'un compte et d'un autre, mais pas la mécanique de la persistance. Je le ferais si vous prévoyez beaucoup plus d'interactions avec ou entre ces deux objets, car à ce moment-là, je voudrais que les bits de persistance soient intégrés dans un objet qui gère uniquement la persistance.
Je tirerais pour garder la conception de telle sorte que l'un des comptes, autres choses ou le troisième objet ORM puisse être modifié sans avoir à changer également les trois autres. À moins qu'il y ait une bonne raison de ne pas le faire, je voudrais que Account et OtherThing soient indépendants et n'aient pas à connaître le fonctionnement et les structures internes l'un de l'autre.
Bien sûr, si je savais tout le contexte que cela allait être, je pourrais changer complètement mes idées. Encore une fois, c'est juste ce que je pense quand je vois des choses comme ça, et comment un maigre.
la source
Les deux approches ont leurs propres avantages et inconvénients. Ce qui est mieux dans un scénario dépend beaucoup du cas d'utilisation en question.
Paramètres Pro multiples, référence d'objet Con:
Référence Pro Object:
Donc, ce qui doit être utilisé et quand cela dépend beaucoup des cas d'utilisation
la source
D'un côté, vous avez un compte et un objet Autre. De l'autre côté, vous avez la possibilité d'insérer une valeur dans une base de données, étant donné l'ID d'un compte et l'ID d'un Autre. Voilà les deux choses données.
Vous pouvez écrire une méthode prenant en compte Account et Otherthing comme arguments. Du côté professionnel, l'appelant n'a pas besoin de connaître les détails du compte et de tout. Du côté négatif, l'appelé a besoin de connaître les méthodes de compte et autre. Et aussi, il n'y a aucun moyen d'insérer autre chose dans une base de données que la valeur d'un objet Otherthing et aucun moyen d'utiliser cette méthode si vous avez l'ID d'un objet de compte, mais pas l'objet lui-même.
Ou vous pouvez écrire une méthode prenant deux identifiants et une valeur comme arguments. Du côté négatif, l'appelant a besoin de connaître les détails du compte et autre chose. Et il peut y avoir une situation où vous avez réellement besoin de plus de détails sur un compte ou autre chose que juste l'id à insérer dans la base de données, auquel cas cette solution est totalement inutile. D'un autre côté, j'espère qu'aucune connaissance du compte et de tout n'est nécessaire dans l'appelé, et il y a plus de flexibilité.
Votre jugement appelle: Faut-il plus de flexibilité? Ce n'est souvent pas une question d'un seul appel, mais serait cohérent dans tous vos logiciels: soit vous utilisez la plupart du temps des identifiants de compte, soit vous utilisez les objets. Le mélanger vous procure le pire des deux mondes.
En C ++, vous pouvez avoir une méthode prenant deux identifiants plus une valeur, et une méthode en ligne prenant Account et Otherthing, vous avez donc les deux façons avec zéro surcharge.
la source