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?
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 arrivez à décider quelle couche dans l'architecture globale du programme doit diviser l'objet en primitives, donc , vous devriez penser à la pile d'appels entière , pas seulement à cette méthode dans laquelle vous vous trouvez actuellement. Il est probable que la rupture doit être effectuée quelque part, et cela n'aurait aucun 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 devrait ou ne devrait pas être modifié si l'objet est modifié . Développons légèrement votre exemple:
function addWidgetButtonClicked(clickEvent) {
// get form data
// get user's account
insertIntoDatabase(account, data);
}
function insertIntoDatabase(Account account, Otherthing data) {
// open database connection
// check data doesn't already exist
database.insertMethod(account.getId(), data.getId(), data.getSomeValue());
}
contre
function addWidgetButtonClicked(clickEvent) {
// get form data
// get user's account
insertIntoDatabase(account.getId(), data.getId(), data.getSomeValue());
}
function insertIntoDatabase(long accountId, long dataId, double someValue) {
// open database connection
// check data doesn't already exist
database.insertMethod(accountId, dataId, someValue);
}
Dans la première version, le code de l'interface utilisateur passe aveuglément l'objet data
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'objet data
dans 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'objet data
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 des deux est correct dépend en grande partie du type de données que contient l'objet data
, mais il est généralement très évident. Par exemple, si data
est une chaîne fournie par l'utilisateur comme "20/05/1999", le code d'interface utilisateur devrait être converti en un type Date
approprié avant de le transmettre .
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:
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 forts 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.
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 pouvez envisager de créer une classe dont le seul but est d'agir comme un objet 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 immuabilité des objets.
Pensez à 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 distribué 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" que vous pouvez mapper à la place.
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.
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é.
Parfois, l'argument le plus fort pour les arguments "Plain old data" est simplement que la classe réceptrice 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)
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.
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 devait logiquement être avec les objets (quelle que soit leur implémentation), ou avec les champs qui se produisent pour 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, alors 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.
Ç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 un EmailInviter
et ces trois implémentations possibles d'une méthode invite
:
void invite(String emailAddressString) {
invite(EmailAddress.parse(emailAddressString));
}
void invite(EmailAddress emailAddress) {
...
}
void invite(User user) {
invite(user.getEmailAddress());
}
Passer un String
où vous devez passer un EmailAddress
est défectueux car toutes les chaînes ne sont pas des adresses e-mail. La classe EmailAddress
correspond mieux sémantiquement au comportement de la méthode. Cependant le passage d'un User
est également imparfait car pourquoi diable un EmailInviter
devrait-il 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
ou int
mais que toutes les chaînes ou entiers ne sont pas valides ou qu'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.
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.
// this has exactly one way to call it
insertIntoDatabase(Account ..., Otherthing ...)
// the parameter order can be confused in practice
insertIntoDatabase(long ..., long ...)
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.
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 la "base de données" Word, 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 vers la mise en place de cette fonction dans cet objet, puis il devient ce travail d'objets pour pouvoir gérer un compte et un autre chose, transformer, puis persister 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 changé 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.
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 multiples multiples, référence d'objet Con:
Référence d'objet Pro:
Donc, ce qui doit être utilisé et quand cela dépend beaucoup des cas d'utilisation
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. parce que
insertIntoDatabase(new Account(id) , new Otherthing(id, "Value"));
est un appel plus lisible que
insertIntoDatabase(myAccount.getId(), myOtherthing.getId(), myOtherthing.getValue() );
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 OOP.
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:
public User getUser(String primaryKey) {
return ...;
}
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.
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.