Envoyez-vous généralement des objets ou leurs variables membres dans des fonctions?

31

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?

AJJ
la source
5
Cela dépendrait entièrement de la fonction de la fonction et de sa relation (ou non) avec l'objet en question.
MetaFight
C'est le problème. Je ne peux pas dire quand j'utiliserais l'un ou l'autre. J'ai l'impression que je pourrais toujours changer le code pour s'adapter à l'une ou l'autre approche.
AJJ
1
En termes d'API (et ne pas regarder du tout les implémentations), la première est abstraite et orientée domaine (ce qui est bien), tandis que la seconde ne l'est pas (ce qui est mauvais).
Erik Eidt
1
La première approche serait davantage d'OO à 3 niveaux. Mais cela devrait être encore plus vrai en éliminant la base de données de mots de la méthode. Il doit s'agir de "Store" ou "Persist" et faire soit Account ou Thing (pas les deux). En tant que client de cette couche, vous ne devez pas connaître le support de stockage. Lors de la récupération d'un compte, vous devez cependant saisir l'identifiant ou une combinaison de valeurs de propriété (pas de valeurs de champ) pour identifier l'objet souhaité. Ou / et implémente une méthode d'énumération qui passe tous les comptes.
Martin Maat
1
Typiquement, les deux seraient faux (ou, plutôt, moins qu'optimaux). La manière dont un objet doit être sérialisé dans la base de données doit être une propriété (une fonction membre) de l'objet, car elle dépend généralement directement des variables membres de l'objet. Si vous modifiez des membres de l'objet, vous devrez également modifier la méthode de sérialisation. Cela fonctionne mieux s'il fait partie de l'objet
tofro

Réponses:

24

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:

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' dataobjet 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' dataobjet 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' dataobjet 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 que datacontient l' objet, mais c'est généralement très évident. Par exemple, sidataest une chaîne fournie par l'utilisateur comme "20/05/1999", il devrait appartenir au code UI de le convertir en un Datetype approprié avant de le transmettre.

Ixrec
la source
8

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.

Ben Cottrell
la source
5

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.

Telastyn
la source
Pourquoi ne changez-vous pas le nom de la méthode en insertAccountIntoDatabase ou allez-vous passer tout autre type?. À un certain nombre d'arguments, utiliser obj facilite la lecture du code. Dans votre cas, je pense plutôt que si le nom de la méthode efface ce que je vais insérer au lieu de la façon dont je vais le faire.
Laiv
3

Ç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 EmailInviteret ces trois implémentations possibles d'une inviteméthode:

void invite(String emailAddressString) {
  invite(EmailAddress.parse(emailAddressString));
}
void invite(EmailAddress emailAddress) {
  ...
}
void invite(User user) {
  invite(user.getEmailAddress());
}

Passer un Stringoù vous devez passer un EmailAddressest défectueux car toutes les chaînes ne sont pas des adresses e-mail. La EmailAddressclasse correspond mieux sémantiquement au comportement de la méthode. Cependant, le passage dans un Userest également imparfait car pourquoi diable devrait-on EmailInviterse 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 Stringou intmais 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.

Jack
la source
0

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

insertIntoDatabase(new Account(id) , new Otherthing(id, "Value"));

est un appel plus lisible que

insertIntoDatabase(myAccount.getId(), myOtherthing.getId(), myOtherthing.getValue() );
un gars
la source
ne peut pas être d'accord là-bas. Les deux ne sont pas synonymes. Créer 2 nouvelles instances d'objet juste pour les passer à une méthode n'est pas bon. J'utiliserais insertIntoDatabase (myAccount, myOtherthing) au lieu de l'une ou l'autre de vos options.
jwenting
0

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:

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.


la source
1
"Que se passe-t-il si vous modifiez les champs 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 ...
David Arno
@DavidArno le point de ma réponse n'est pas que rien ne se briserait, mais moins se briserait. N'oubliez pas non plus le premier paragraphe: indépendamment de ce qui se casse, l'état interne d'un objet doit être abstrait à l'aide de l'interface de l'objet. Le fait de contourner son état interne est une violation des principes de la POO.
0

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.

Joeri Sebrechts
la source
0

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.

Thomas Carlisle
la source
0

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:

  • L'appelant n'est pas lié à une classe spécifique , il peut transmettre des valeurs de différentes sources
  • L'état de l'objet est protégé contre toute modification inattendue à l'intérieur de l'exécution de la méthode.

Référence Pro Object:

  • Interfaçage clair indiquant que la méthode est liée au type de référence d'objet, ce qui rend difficile la transmission accidentelle de valeurs non liées / non valides
  • Renommer un champ / getter nécessite des changements à toutes les invocations de la méthode et pas seulement dans sa mise en œuvre
  • Si une nouvelle propriété est ajoutée et doit être transmise, aucune modification n'est requise dans la signature de la méthode
  • La méthode peut muter l'état de l'objet
  • Le fait de passer trop de variables de types primitifs similaires complique la tâche de l'appelant concernant l'ordre (problème de modèle de générateur)

Donc, ce qui doit être utilisé et quand cela dépend beaucoup des cas d'utilisation

  1. Passer des paramètres individuels: en général, si la méthode n'a rien à voir avec le type d'objet, il est préférable de passer la liste des paramètres individuels afin qu'elle soit applicable à un public plus large.
  2. Introduire un nouvel objet modèle: si la liste des paramètres augmente (plus de 3), il est préférable d'introduire un nouvel objet modèle appartenant à l'API appelée (modèle de générateur préféré)
  3. Passer la référence d'objet: si la méthode est liée aux objets du domaine, il vaut mieux, du point de vue de la maintenabilité et de la lisibilité, passer les références d'objet.
Rahul
la source
0

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.

gnasher729
la source