J'essayais de trouver des alternatives à l'utilisation de variable globale dans certains codes hérités. Mais cette question ne concerne pas les alternatives techniques, je suis principalement préoccupé par la terminologie .
La solution évidente consiste à transmettre un paramètre à la fonction au lieu d'utiliser un paramètre global. Dans cette ancienne base de code, cela signifierait que je dois changer toutes les fonctions de la longue chaîne d'appels entre le point où la valeur sera éventuellement utilisée et la fonction qui reçoit le paramètre en premier.
higherlevel(newParam)->level1(newParam)->level2(newParam)->level3(newParam)
où newParam
était auparavant une variable globale dans mon exemple, mais cela aurait pu être une valeur précédemment codée en dur. Le fait est que maintenant la valeur de newParam est obtenue à higherlevel()
et doit "voyager" jusqu’à level3()
.
Je me demandais s'il y avait un nom pour ce type de situation / modèle où vous devez ajouter un paramètre à de nombreuses fonctions qui "transmettent" simplement la valeur non modifiée.
J'espère que l'utilisation de la terminologie appropriée me permettra de trouver plus de ressources sur les solutions de restructuration et de décrire cette situation à mes collègues.
Réponses:
Les données elles-mêmes sont appelées "données de tramp" . C'est une "odeur de code", indiquant qu'un morceau de code communique avec un autre morceau de code à distance, par le biais d'intermédiaires.
Le refactoring pour supprimer les variables globales est difficile, et les données de tramp sont une méthode pour le faire, et souvent le moyen le moins coûteux. Cela a des coûts.
la source
Je ne pense pas que cela, en soi, est un anti-modèle. Je pense que le problème est que vous envisagez les fonctions comme une chaîne alors qu'en réalité, vous devriez considérer chacune d'elles comme une boîte noire indépendante ( REMARQUE : les méthodes récursives sont une exception notable à ce conseil.)
Par exemple, supposons que je dois calculer le nombre de jours entre deux dates de calendrier pour créer une fonction:
Pour ce faire, je crée ensuite une nouvelle fonction:
Alors ma première fonction devient simplement:
Il n'y a rien d'anti-pattern à ce sujet. Les paramètres de la méthode daysBetween sont transmis à une autre méthode et ne sont jamais autrement référencés dans la méthode, mais ils sont toujours nécessaires pour que cette méthode fasse ce qu'elle doit faire.
Ce que je recommanderais, c'est d'examiner chaque fonction et de commencer par quelques questions:
Si vous examinez un fouillis de code sans un seul objectif intégré dans une méthode, commencez par le démêler. Cela peut être fastidieux. Commencez par les éléments les plus faciles à extraire et passez à une méthode distincte, puis répétez l'opération jusqu'à obtenir un résultat cohérent.
Si vous avez trop de paramètres, envisagez le refactoring Méthode en objet .
la source
BobDalgleish a déjà noté que ce modèle (anti) est appelé " données de tramp ".
D'après mon expérience, la cause la plus fréquente de données de tramp excessives est la présence d'un ensemble de variables d'état liées qui doivent être réellement encapsulées dans un objet ou une structure de données. Parfois, il peut même être nécessaire d'imbriquer un tas d'objets pour organiser correctement les données.
Pour un exemple simple, considérons un jeu qui a un caractère de joueur personnalisable, avec des propriétés telles que
playerName
,playerEyeColor
etc. Bien sûr, le joueur a également une position physique sur la carte du jeu, ainsi que diverses autres propriétés telles que, par exemple, le niveau de santé actuel et maximal, etc.Lors d'une première itération d'un tel jeu, il peut s'avérer un choix parfaitement raisonnable de transformer toutes ces propriétés en variables globales - après tout, il n'y a qu'un seul joueur et presque tout dans le jeu implique le joueur d'une manière ou d'une autre. Donc, votre état global peut contenir des variables telles que:
Mais à un moment donné, vous constaterez peut-être que vous devez modifier cette conception, peut-être parce que vous souhaitez ajouter un mode multijoueur au jeu. Dans un premier temps, vous pouvez essayer de rendre toutes ces variables locales et de les transmettre aux fonctions qui en ont besoin. Cependant, vous constaterez peut-être qu'une action particulière de votre jeu peut impliquer une chaîne d'appels de fonctions, par exemple:
... et la
interactWithShopkeeper()
fonction demande au commerçant d’adresser son nom au joueur, de sorte que vous devez maintenant soudainement transmettre desplayerName
données clandestines à travers toutes ces fonctions. Et, bien sûr, si le commerçant pense que les joueurs aux yeux bleus sont naïfs et qu'ils leur demanderont des prix plus élevés, vous devrez passerplayerEyeColor
par toute la chaîne de fonctions, etc.La solution appropriée , dans ce cas, consiste bien entendu à définir un objet de joueur qui encapsule le nom, la couleur des yeux, la position, la santé et toutes autres propriétés du personnage de joueur. De cette façon, il vous suffit de transmettre cet objet unique à toutes les fonctions qui impliquent le joueur.
En outre, plusieurs des fonctions ci-dessus pourraient naturellement être transformées en méthodes de cet objet joueur, ce qui leur donnerait automatiquement accès aux propriétés du joueur. D'une certaine manière, il ne s'agit que d'un simple sucre syntaxique, car l'appel d'une méthode sur un objet transmet de toute façon l'instance d'objet en tant que paramètre masqué à la méthode, mais rend le code plus clair et plus naturel s'il est utilisé correctement.
Bien sûr, un jeu typique aurait un état beaucoup plus "global" que celui du joueur; Par exemple, vous avez presque certainement une sorte de carte sur laquelle se déroule le jeu, ainsi qu'une liste des personnages non-joueurs se déplaçant sur la carte, et peut-être des objets placés dessus, etc. Vous pouvez également passer tous ceux qui se trouvent autour de vous comme des objets tramp, mais cela encombrerait encore les arguments de votre méthode.
Au lieu de cela, la solution consiste à laisser les objets stocker des références à tout autre objet avec lequel ils ont des relations permanentes ou temporaires. Ainsi, par exemple, l’objet joueur (et probablement aussi tout objet NPC) devrait probablement stocker une référence à l’objet "world game", qui aurait une référence au niveau / map en cours, de sorte qu’une méthode telle
player.moveTo(x, y)
que être explicitement donné la carte en tant que paramètre.De même, si notre personnage joueur avait, par exemple, un chien qui les suivait, nous regrouperions naturellement toutes les variables d'état décrivant le chien dans un seul objet et donnerions à l'objet joueur une référence au chien (afin que le joueur puisse , appelez le chien par son nom) et vice-versa (pour que le chien sache où se trouve le joueur). Et, bien sûr, nous voudrions probablement que le joueur et les objets chien soient les deux sous-classes d’un objet "acteur" plus générique, afin de pouvoir réutiliser le même code pour déplacer les deux sur la carte, par exemple.
Ps. Même si j'ai utilisé un jeu comme exemple, il existe d'autres types de programmes dans lesquels de tels problèmes se posent également. D'après mon expérience, toutefois, le problème sous-jacent a tendance à être toujours le même: vous avez un ensemble de variables distinctes (qu'elles soient locales ou globales) qui souhaitent réellement être regroupées dans un ou plusieurs objets liés. Que la "donnée tampon" introduite dans vos fonctions se compose de paramètres d'option "globaux" ou de requêtes de base de données en cache ou de vecteurs d'état dans une simulation numérique, la solution consiste invariablement à identifier le contexte naturel auquel les données appartiennent et à en faire un objet. (ou quel que soit l'équivalent le plus proche dans la langue de votre choix).
la source
foo.method(bar, baz)
etmethod(foo, bar, baz)
, il existe d’autres raisons (notamment le polymorphisme, l’encapsulation, la localisation, etc.) pour préférer le premier.Je ne connais pas de nom spécifique pour cela, mais je suppose qu'il est utile de mentionner que le problème que vous décrivez est simplement le problème de trouver le meilleur compromis pour la portée d'un tel paramètre:
en tant que variable globale, la portée est trop grande lorsque le programme atteint une certaine taille
en tant que paramètre purement local, la portée peut être trop petite, car elle entraîne de nombreuses listes de paramètres répétitives dans les chaînes d'appels
en tant que compromis, vous pouvez souvent faire de ce paramètre une variable membre dans une ou plusieurs classes, et c'est ce que j'appellerais simplement la conception de classe appropriée .
la source
Je crois que le modèle que vous décrivez est exactement l' injection de dépendance . Plusieurs intervenants ont fait valoir qu'il s'agissait d'un modèle , et non d'un anti-modèle , et j'aurais tendance à être d'accord.
Je suis également d'accord avec la réponse de @ JimmyJames, dans laquelle il affirme qu'il est judicieux de traiter chaque fonction comme une boîte noire prenant toutes ses entrées en tant que paramètres explicites. Autrement dit, si vous écrivez une fonction qui fait un sandwich au beurre d’arachide et à la gelée, vous pouvez l’écrire ainsi:
mais il serait préférable d'appliquer l'injection de dépendance et de l'écrire comme suit:
Vous avez maintenant une fonction qui documente clairement toutes ses dépendances dans sa signature, ce qui est excellent pour la lisibilité. Après tout, il est vrai que pour pouvoir
make_sandwich
accéder à unRefrigerator
; ainsi, l'ancienne signature de fonction était fondamentalement hypocrite en ne prenant pas le réfrigérateur comme partie intégrante de ses entrées.En prime, si vous respectez la hiérarchie de votre classe, évitez les découpages en tranches, etc., vous pouvez même tester la
make_sandwich
fonction par unité en transmettant unMockRefrigerator
! (Vous devrez peut-être effectuer des tests unitaires de cette manière car votre environnement de test unitaire risque de ne pas avoir accès à aucunPhysicalRefrigerator
.Je comprends que toutes les utilisations de l’injection de dépendance n’exigent pas l’ installation d’un paramètre similaire dans de nombreux niveaux de la pile d’appel. Je ne réponds donc pas exactement à la question que vous avez posée ... mais si vous souhaitez en savoir plus à ce sujet, "injection de dépendance" est certainement un mot clé pour vous.
la source
Refrigerator
dans unIngredientSource
, voire même généraliser la notion de "sandwich"template<typename... Fillings> StackedElementConstruction<Fillings...> make_sandwich(ElementSource&)
; c'est ce qu'on appelle la "programmation générique" et elle est raisonnablement puissante, mais elle est sûrement bien plus mystérieuse que le PO veut vraiment entrer pour le moment. N'hésitez pas à poser une nouvelle question sur le niveau d'abstraction approprié pour les programmes en sandwich. ;)make_sandwich()
.C'est à peu près la définition classique du couplage , un module ayant une dépendance qui en affecte un autre, et qui crée un effet d'entraînement une fois modifié. Les autres commentaires et réponses indiquent correctement qu'il s'agit d'une amélioration par rapport au global, car le couplage est désormais plus explicite et plus facile à voir par le programmeur, au lieu d'être subversif. Cela ne signifie pas que cela ne devrait pas être corrigé. Vous devriez être capable de refactoriser pour supprimer ou réduire le couplage, même si cela peut être douloureux.
la source
level3()
besoin estnewParam
, le couplage est sûr, mais différentes parties du code doivent communiquer les unes avec les autres. Je n'appellerais pas nécessairement un paramètre de fonction mauvais couplage si cette fonction utilise le paramètre. Je pense que l’aspect problématique de la chaîne est le couplage supplémentaire introduit pourlevel1()
etlevel2()
qui n’a aucune utilité,newParam
sauf pour le transmettre. Bonne réponse, +1 pour le couplage.Bien que cette réponse ne réponde pas directement à votre question, je pense que je m'en voudrais de la laisser passer sans mentionner comment l’améliorer (puisque comme vous le dites, cela peut être un anti-modèle). J'espère que vous et les autres lecteurs pourrez tirer parti de ce commentaire supplémentaire sur la manière d'éviter les "données de tramp" (comme l'a si gentiment nommé Bob Dalgleish pour nous).
Je suis d'accord avec les réponses qui suggèrent de faire quelque chose de plus OO pour éviter ce problème. Cependant, une autre façon de réduire considérablement le passage d'arguments sans passer simplement à " juste passer une classe où vous passiez beaucoup d'arguments! " Est de refactoriser afin que certaines étapes de votre processus se déroulent au niveau supérieur un. Par exemple, voici quelques exemples de code avant :
Notez que cela devient encore pire avec le plus de choses à faire
ReportStuff
. Vous devrez peut-être transmettre l’instance du rapporteur que vous souhaitez utiliser. Et toutes sortes de dépendances qui doivent être transférées fonctionnent d'une fonction imbriquée.Ma suggestion est de tirer tout cela à un niveau supérieur, où la connaissance des étapes nécessite de vivre dans une seule méthode au lieu d'être répartie sur une chaîne d'appels de méthode. Bien sûr, cela serait plus compliqué dans le code réel, mais cela vous donne une idée:
Notez que la grande différence ici est que vous n'avez pas à passer les dépendances par une longue chaîne. Même si vous nivelez à un niveau, mais à quelques niveaux, si ces niveaux obtiennent également un «nivellement» afin que le processus soit considéré comme une série d'étapes à ce niveau, vous aurez fait une amélioration.
Même si cela reste une procédure et que rien n’a encore été transformé en objet, c’est un bon pas en avant pour décider quel type d’encapsulation vous pouvez obtenir en transformant quelque chose en classe. Les appels de méthode profondément chaînés dans le scénario précédent masquent les détails de ce qui se passe réellement et peuvent rendre le code très difficile à comprendre. Bien que vous puissiez en faire trop et que vous finissiez par faire savoir au code de niveau supérieur ce qu’il ne devrait pas faire, ou à créer une méthode qui fait trop de choses, violant ainsi le principe de responsabilité unique, j’ai généralement constaté qu’aplatir un peu les choses dans la clarté et en faisant des changements progressifs vers un meilleur code.
Notez que pendant que vous faites tout cela, vous devriez envisager la testabilité. Les appels de méthode en chaîne rendent en réalité les tests unitaires plus difficiles, car vous ne disposez pas des bons point d'entrée et de sortie dans l'assemblage pour la tranche que vous souhaitez tester. Notez qu'avec cet aplatissement, puisque vos méthodes ne prennent plus autant de dépendances, elles sont plus faciles à tester et ne nécessitent pas autant de simulacres!
J'ai récemment essayé d'ajouter des tests unitaires à une classe (que je n'ai pas écrite) qui prenait quelque chose comme 17 dépendances, dont il fallait toutes se moquer! Je n'ai pas encore tout réglé, mais j'ai divisé la classe en trois classes, chacune correspondant à l'un des noms distincts qui la concernaient, et j'ai ramené la liste des dépendances à 12 pour la pire et à environ 8 pour la première. meilleur.
La testabilité vous obligera à écrire un meilleur code. Vous devriez écrire des tests unitaires, car vous constaterez que cela vous fait penser à votre code différemment et que vous écrivez un meilleur code dès le départ, quel que soit le nombre de bugs que vous auriez pu avoir avant l'écriture des tests unitaires.
la source
Vous ne violez pas littéralement la loi de Demeter, mais votre problème est similaire à celui-ci à certains égards. Puisque votre question a pour but de trouver des ressources, je vous suggère de lire à propos de Law of Demeter et de voir dans quelle mesure ces conseils s’appliquent à votre situation.
la source
Dans certains cas, il est préférable (en termes d’efficacité, de maintenabilité et de facilité d’implémentation) d’avoir certaines variables globales plutôt que de faire passer tout le temps (par exemple, il faut environ 15 variables qui doivent persister). Il est donc logique de trouver un langage de programmation qui prenne mieux en charge la portée (en tant que variables statiques privées de C ++) afin d’atténuer les dégâts potentiels (de l’espace de noms et de la falsification). Bien sûr, ceci est juste une connaissance commune.
Cependant, l'approche indiquée par le PO est très utile si l'on fait de la programmation fonctionnelle.
la source
Il n'y a aucun anti-modèle ici, parce que l'appelant ne connaît pas tous ces niveaux ci-dessous et s'en moque.
Quelqu'un appelle HigherLevel (params) et s'attend à ce que HigherLevel fasse son travail. Ce que fait plus haut niveau avec params ne fait pas l'affaire des appelants. higherLevel traite le problème de la meilleure façon possible, dans ce cas en passant les paramètres à level1 (paramètres). C'est absolument ok.
Vous voyez une chaîne d'appels - mais il n'y en a pas. Il y a une fonction au sommet qui fait son travail de la meilleure façon possible. Et il y a d'autres fonctions. Chaque fonction peut être remplacée à tout moment.
la source