J'essayais de trouver des alternatives à l'utilisation de la variable globale dans un code hérité. Mais cette question ne concerne pas les alternatives techniques, je suis principalement préoccupé par la terminologie.
La solution évidente est de passer un paramètre dans la fonction au lieu d'utiliser un global. Dans cette base de code héritée, cela signifierait que je dois changer toutes les fonctions de la longue chaîne d'appel entre le point où la valeur sera finalement 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 il aurait pu être une valeur codée en dur à la place. Le fait est que maintenant la valeur de newParam est obtenue dans higherlevel()
et doit "voyager" jusqu'à level3()
.
Je me demandais s'il y avait n 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 refonte et de décrire cette situation à mes collègues.
Les données elles-mêmes sont appelées "tramp data" . C'est une "odeur de code", indiquant qu'un morceau de code communique avec un autre morceau de code à distance, par des intermédiaires.
La refactorisation 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 cher. Il a ses coûts.
Je ne pense pas que cela, en soi, soit un anti-modèle. Je pense que le problème est que vous envisagez les fonctions comme une chaîne alors que vous devriez vraiment les considérer comme une boîte noire indépendante ( [~ # ~] note [~ # ~] : les méthodes récursives sont une exception notable à ce conseil.)
Par exemple, disons que je dois calculer le nombre de jours entre deux dates de calendrier, donc je crée une fonction:
int daysBetween(Day a, Day b)
Pour ce faire, je crée ensuite une nouvelle fonction:
int daysSinceEpoch(Day day)
Alors ma première fonction devient simplement:
int daysBetween(Day a, Day b)
{
return daysSinceEpoch(b) - daysSinceEpoch(a);
}
Il n'y a rien d'anti-modèle à ce sujet. Les paramètres de la méthode daysBetween sont passés à une autre méthode et ne sont jamais référencés autrement 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 regardez un fatras de code sans un seul but intégré dans une méthode, vous devriez commencer par le démêler. Cela peut être fastidieux. Commencez par les choses les plus faciles à retirer et passez à une méthode distincte et répétez jusqu'à ce que vous ayez quelque chose de cohérent.
Si vous avez juste trop de paramètres, pensez à Refactorisation de méthode en objet .
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 courante de données de clochard excessives est d'avoir un tas de variables d'état liées qui devraient vraiment être 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 personnage de joueur personnalisable, avec des propriétés comme playerName
, playerEyeColor
et ainsi de suite. Bien sûr, le joueur a également une position physique sur la carte du jeu, et diverses autres propriétés comme, par exemple, le niveau de santé actuel et maximum, etc.
Dans une première itération d'un tel jeu, cela pourrait être 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. Votre état global peut donc contenir des variables comme:
playerName = "Bob"
playerEyeColor = GREEN
playerXPosition = -8
playerYPosition = 136
playerHealth = 100
playerMaxHealth = 100
Mais à un moment donné, vous constaterez peut-être que vous devez modifier cette conception, peut-être parce que vous souhaitez ajouter un mode multijoueur dans le 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 pouvez alors constater qu'une action particulière dans votre jeu peut impliquer une chaîne d'appels de fonction comme, par exemple:
mainGameLoop()
-> processInputEvent()
-> doPlayerAction()
-> movePlayer()
-> checkCollision()
-> interactWithNPC()
-> interactWithShopkeeper()
... et la fonction interactWithShopkeeper()
a l'adresse du commerçant au joueur par son nom, vous devez donc soudainement passer playerName
comme données de tramp via all ceux les fonctions. Et, bien sûr, si le commerçant pense que les joueurs aux yeux bleus sont naïfs et leur factureront des prix plus élevés, vous devrez alors passer playerEyeColor
à travers toute la chaîne de fonctions, etc.
La solution appropriée, dans ce cas, est bien sûr de définir un objet joueur qui encapsule le nom, la couleur des yeux, la position, la santé et toute autre propriété du personnage joueur. De cette façon, vous n'avez qu'à transmettre cet objet unique à toutes les fonctions qui impliquent le joueur d'une manière ou d'une autre.
De plus, plusieurs des fonctions ci-dessus pourraient être naturellement 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, c'est juste du sucre syntaxique, car appeler une méthode sur un objet transmet effectivement l'instance d'objet comme paramètre caché à la méthode, mais cela 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 le simple joueur; par exemple, vous auriez presque certainement une sorte de carte sur laquelle le jeu se déroule, et une liste de personnages non-joueurs se déplaçant sur la carte, et peut-être des éléments placés dessus, et ainsi de suite. Vous pouvez également passer tous ceux-ci comme des objets clochards, mais cela encombrerait à nouveau vos arguments de méthode.
Au lieu de cela, la solution consiste à demander aux objets de 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 tous les objets NPC aussi) devrait probablement stocker une référence à l'objet "world game", qui aurait une référence au niveau/carte actuel, de sorte qu'une méthode comme player.moveTo(x, y)
n'a pas besoin de recevoir explicitement la carte comme paramètre.
De même, si notre personnage joueur avait, disons, un chien de compagnie qui les suivait, nous regrouperions naturellement toutes les variables d'état décrivant le chien en un seul objet, et donnerions à l'objet joueur une référence au chien (afin que le joueur puisse disons, 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 faire du joueur et du chien des objets des sous-classes d'un objet "acteur" plus générique, afin que nous puissions réutiliser le même code pour, disons, déplacer les deux sur la carte.
Ps. Même si j'ai utilisé un jeu comme exemple, il existe d'autres types de programmes où de tels problèmes se posent également. D'après mon expérience, cependant, le problème sous-jacent a toujours tendance à être le même: vous avez un tas de variables distinctes (locales ou globales) qui veulent vraiment être regroupées en un ou plusieurs objets liés. Que les "données de tramp" entrant dans vos fonctions consistent en des paramètres d'option "globaux" ou des requêtes de base de données en cache ou des vecteurs d'état dans une simulation numérique, la solution est invariablement d'identifier le contexte naturel que les données appartient à, et en faire un objet (ou tout ce qui est l'équivalent le plus proche dans votre langue choisie).
Je ne connais pas de nom spécifique pour cela, mais je suppose qu'il convient 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, lorsqu'elle conduit à de nombreuses listes de paramètres répétitifs dans les chaînes d'appels
donc comme compromis, vous pouvez souvent faire d'un tel paramètre une variable membre dans une ou plusieurs classes, et c'est ce que j'appellerais juste conception de classe appropriée.
Je crois que le modèle que vous décrivez est exactement injection de dépendance . Plusieurs commentateurs ont fait valoir qu'il s'agit 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, où il prétend que c'est une bonne pratique de programmation de traiter chaque fonction comme une boîte noire qui prend toutes ses entrées comme explicites paramètres. Autrement dit, si vous écrivez une fonction qui fait un sandwich au beurre d'arachide et à la gelée, vous pouvez l'écrire comme
Sandwich make_sandwich() {
PeanutButter pb = get_peanut_butter();
Jelly j = get_jelly();
return pb + j;
}
extern PhysicalRefrigerator g_refrigerator;
PeanutButter get_peanut_butter() {
return g_refrigerator.get("peanut butter");
}
Jelly get_jelly() {
return g_refrigerator.get("jelly");
}
mais il serait meilleure pratique d'appliquer l'injection de dépendance et de l'écrire comme ceci à la place:
Sandwich make_sandwich(Refrigerator& r) {
PeanutButter pb = get_peanut_butter(r);
Jelly j = get_jelly(r);
return pb + j;
}
PeanutButter get_peanut_butter(Refrigerator& r) {
return r.get("peanut butter");
}
Jelly get_jelly(Refrigerator& r) {
return r.get("jelly");
}
Vous avez maintenant une fonction qui documente clairement toutes ses dépendances dans sa signature de fonction, ce qui est idéal pour la lisibilité. Après tout, c'est vrai que pour make_sandwich
vous devez avoir accès à un Refrigerator
; de sorte que l'ancienne signature de fonction était fondamentalement malhonnête en ne prenant pas le réfrigérateur dans le cadre de ses entrées.
En prime, si vous faites correctement votre hiérarchie de classe, évitez de découper, etc., vous pouvez même tester à l'unité le make_sandwich
fonction en passant un MockRefrigerator
! (Vous devrez peut-être effectuer un test unitaire de cette manière car votre environnement de test unitaire peut ne pas avoir accès aux PhysicalRefrigerator
s.)
Je comprends que toutes les utilisations de l'injection de dépendance ne nécessitent pas la plomberie d'un paramètre de nom similaire à plusieurs niveaux dans la pile des appels, donc je ne réponds pas exactement la question que vous avez posée ... mais si vous cherchez plus de lecture sur ce sujet, "l'injection de dépendance" est certainement un mot-clé pertinent pour vous.
C'est à peu près la définition du manuel de couplage, un module ayant une dépendance qui affecte profondément un autre, et qui crée un effet d'entraînement lorsqu'il est modifié. Les autres commentaires et réponses sont exacts qu'il s'agit d'une amélioration par rapport au global, car le couplage est désormais plus explicite et plus facile à voir pour le programmeur, au lieu d'être subversif. Cela ne signifie pas qu'il ne devrait pas être corrigé. Vous devriez être en mesure de refactoriser pour retirer ou réduire l'accouplement, même si cela a été là pendant un certain temps, cela peut être douloureux.
Bien que cette réponse ne réponde pas directement à votre question, je pense que je serais négligent de la laisser passer sans mentionner comment l'améliorer (car comme vous le dites, cela peut être un anti-modèle). J'espère que vous et d'autres lecteurs pourrez tirer parti de ce commentaire supplémentaire sur la façon d'éviter les "données de clochards" (comme Bob Dalgleish l'a si bien nommé 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 d'aider également à réduire ce passage d'arguments en profondeur sans simplement sauter à "juste passer une classe où vous aviez l'habitude de passer de nombreux arguments! "est de refactoriser de sorte que certaines étapes de votre processus se produisent au niveau supérieur au lieu du niveau inférieur. Par exemple, voici un code avant:
public void PerformReporting(StuffRepository repo, string desiredName) {
var stuffs = repo.GetStuff(DateTime.Now());
FilterAndReportStuff(stuffs, desiredName);
}
public void FilterAndReportStuff(IEnumerable<Stuff> stuffs, string desiredName) {
var filter = CreateStuffFilter(FilterTypes.Name, desiredName);
ReportStuff(stuffs.Filter(filter));
}
public void ReportStuff(IEnumerable<Stuff> stuffs) {
stuffs.Report();
}
Notez que cela devient encore pire avec plus de choses à faire dans ReportStuff
. Vous devrez peut-être transmettre l'instance du Reporter que vous souhaitez utiliser. Et toutes sortes de dépendances qui doivent être transmises, fonction à fonction imbriquée.
Ma suggestion est de tirer tout cela à un niveau supérieur, où la connaissance des étapes nécessite la vie dans une seule méthode au lieu d'être répartie sur une chaîne d'appels de méthode. Bien sûr, ce serait plus compliqué en vrai code, mais cela vous donne une idée:
public void PerformReporting(StuffRepository repo, string desiredName) {
var stuffs = repo.GetStuff(DateTime.Now());
var filter = CreateStuffFilter(FilterTypes.Name, desiredName);
var filteredStuffs = stuffs.Filter(filter)
filteredStuffs.Report();
}
Notez que la grande différence ici est que vous n'avez pas à passer les dépendances à travers une longue chaîne. Même si vous aplatissez non seulement à un niveau, mais à quelques niveaux de profondeur, si ces niveaux atteignent également un certain "aplatissement" de sorte que le processus soit considéré comme une série d'étapes à ce niveau, vous aurez fait une amélioration.
Bien que cela soit encore procédural et que rien n'ait encore été transformé en objet, c'est une bonne étape pour décider du type d'encapsulation que vous pouvez réaliser en transformant quelque chose en classe. La méthode profondément enchaînée appelle dans le scénario avant cache les détails de ce qui se passe réellement et peut rendre le code très difficile à comprendre. Bien que vous puissiez exagérer cela et finir 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 la responsabilité unique, en général, j'ai trouvé que l'aplatissement des choses aide un peu dans la clarté et en faisant des changements incrémentiels vers un meilleur code.
Notez que pendant que vous faites tout cela, vous devez considérer la testabilité. Les appels de méthode chaînés font en fait des tests unitaires plus difficile parce que vous n'avez pas un bon point d'entrée et un bon point de sortie dans l'assembly 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, ne nécessitant pas autant de simulations!
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, qui devaient toutes être moquées! Je n'ai pas encore tout compris, mais j'ai divisé la classe en trois classes, chacune traitant d'un des noms distincts qui l'intéressait, et j'ai réduit la liste de dépendances à 12 pour le pire et environ 8 pour le meilleur.
La testabilité vous obligera à écrire un meilleur code. Vous devriez écrire des tests unitaires car vous constaterez que cela vous fait penser différemment à votre code et vous écrirez un meilleur code dès le départ, quel que soit le nombre de bogues que vous pourriez avoir avant d'écrire les tests unitaires.
Vous ne violez pas littéralement la loi de Déméter, mais votre problème est similaire à cela à certains égards. Étant donné que le but de votre question est de trouver des ressources, je vous suggère de lire sur Law of Demeter et de voir dans quelle mesure ces conseils s'appliquent à votre situation.
Il y a des cas où il est préférable (en termes d'efficacité, de maintenabilité et de facilité de mise en œuvre) d'avoir certaines variables comme globales plutôt que la surcharge de toujours tout faire circuler (disons que vous avez une quinzaine de variables qui doivent persister). Il est donc logique de trouver un langage de programmation qui prend mieux en charge la portée (en tant que variables statiques privées de C++) pour atténuer le gâchis potentiel (de l'espace de noms et de la falsification des choses). Bien sûr, ce n'est qu'une connaissance commune.
Mais, l'approche indiquée par l'OP est très utile si l'on fait de la programmation fonctionnelle.
Il n'y a aucun anti-modèle ici, car l'appelant ne connaît pas tous ces niveaux ci-dessous et s'en fiche.
Quelqu'un appelle HigherLevel (params) et s'attend à ce que HigherLevel fasse son travail. Ce que HigherLevel fait avec les paramètres n'est pas l'affaire des appelants. HigherLevel gère le problème de la meilleure façon possible, dans ce cas en passant des paramètres au niveau 1 (paramètres). C'est absolument correct.
Vous voyez une chaîne d'appel - mais il n'y a pas de chaîne d'appel. 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.