Avant de commencer, permettez-moi de dire que je connais bien les concepts d'abstraction et d'injection de dépendance. Je n'ai pas besoin que mes yeux soient ouverts ici.
Eh bien, la plupart d’entre nous disent (trop) souvent, sans vraiment comprendre «Ne pas utiliser de variables globales» ou «Les singletons sont diaboliques parce qu’ils sont globaux». Mais quel est vraiment le problème de cet état mondial menaçant?
Supposons que j'ai besoin d'une configuration globale pour mon application, par exemple des chemins de dossiers système ou des informations d'identification de base de données à l'échelle de l'application.
Dans ce cas, je ne vois pas de bonne solution autre que de fournir ces paramètres dans une sorte d'espace global, qui sera généralement disponible pour l'ensemble de l'application.
Je sais que c'est mauvais d'en abuser , mais l'espace mondial est-il vraiment SI mal? Et si c'est le cas, quelles sont les bonnes alternatives?
la source
Réponses:
Très brièvement, cela rend l’état du programme imprévisible.
Pour élaborer, imaginez que vous avez deux objets qui utilisent tous les deux la même variable globale. En supposant que vous n'utilisez pas de source aléatoire dans l'un ou l'autre des modules, la sortie d'une méthode particulière peut être prédite (et donc testée) si l'état du système est connu avant l'exécution de la méthode.
Toutefois, si une méthode dans l'un des objets déclenche un effet secondaire modifiant la valeur de l'état global partagé, vous ne savez plus quel est l'état de départ lorsque vous exécutez une méthode dans l'autre objet. Vous ne pouvez plus prédire quelle sortie vous obtiendrez lorsque vous exécuterez la méthode et vous ne pourrez donc pas la tester.
Au niveau académique, cela ne semble pas si sérieux, mais être capable de coder des tests unitaires est une étape majeure dans le processus de preuve de sa correction (ou du moins de son adéquation à son objectif).
Dans le monde réel, cela peut avoir des conséquences très graves. Supposons que vous ayez une classe qui renseigne une structure de données globale et une classe différente qui utilise les données de cette structure de données, en modifiant son état ou en le détruisant au cours du processus. Si la classe de processeur exécute une méthode avant que la classe de populator ne soit terminée, le résultat est que la classe de processeur aura probablement des données incomplètes à traiter et que la structure de données sur laquelle la classe de populator travaillait pourrait être corrompue ou détruite. Le comportement du programme dans ces circonstances devient complètement imprévisible et conduira probablement à une perte épique.
En outre, l'état global nuit à la lisibilité de votre code. Si votre code a une dépendance externe qui n'est pas explicitement introduite dans celui-ci, le responsable de la maintenance de votre code devra le rechercher pour déterminer d'où il provient.
En ce qui concerne les alternatives existantes, il est impossible de ne pas avoir d'état global, mais dans la pratique, il est généralement possible de limiter l'état global à un seul objet qui englobe tous les autres, et qui ne doit jamais être référencé en s'appuyant sur les règles de cadrage. de la langue que vous utilisez. Si un objet particulier a besoin d'un état particulier, il doit le demander explicitement en le faisant passer comme argument à son constructeur ou par une méthode de définition. Ceci est connu sous le nom d'injection de dépendance.
Il peut sembler stupide de passer dans un état auquel vous pouvez déjà accéder en raison des règles de portée de la langue que vous utilisez, mais les avantages sont énormes. Maintenant, si quelqu'un regarde le code de manière isolée, il est clair de quel état il a besoin et d'où il vient. Il présente également d’énormes avantages en ce qui concerne la flexibilité de votre module de code et, par conséquent, les possibilités de le réutiliser dans différents contextes. Si l'état est passé et que les modifications apportées à l'état sont locales au bloc de code, vous pouvez alors transmettre l'état de votre choix (s'il s'agit du type de données correct) et laisser votre code le traiter. Le code écrit dans ce style a généralement l’apparence d’une collection de composants faiblement associés qui peuvent facilement être interchangés. Le code d'un module ne devrait pas se soucier d'où vient l'état, mais comment le traiter.
Il y a beaucoup d'autres raisons pour lesquelles le fait de passer d'un état à un autre est nettement supérieur au fait de s'appuyer sur un état global. Cette réponse n’est nullement exhaustive. Vous pourriez probablement écrire un livre entier sur la raison pour laquelle l'état global est mauvais.
la source
being able to unit test code is a major step in the process of proving its correctness (or at least fitness for purpose)
. Non ce n'est pas. "Cela fait maintenant deux décennies qu'il a été souligné que les tests de programmes peuvent démontrer de manière convaincante la présence de bogues, mais ne peuvent jamais démontrer leur absence. Après avoir cité cette remarque bien médiatisée, l'ingénieur en logiciel revient à l'ordre du jour et continue pour affiner ses stratégies de tests, tout comme l’alchimiste d’autrefois, qui a continué à affiner ses purifications chrysocosmiques. " - Djikstra, 1988. (Cela fait maintenant 4,5 décennies ...)L'état global modifiable est diabolique pour plusieurs raisons:
Alternatives à l'état global mutable:
la source
grep
de savoir quelles fonctions l’utilisent.Il suffit de passer une référence aux fonctions qui en ont besoin. Ce n'est pas si dur.
la source
Si vous dites "état", cela signifie généralement "état mutable". Et l'état mondial mutable est totalement pervers, car cela signifie que toute partie du programme peut influencer toute autre partie (en modifiant l'état global).
Imaginez déboguer un programme inconnu: vous trouvez que la fonction A se comporte d’une certaine manière pour certains paramètres d’entrée, mais parfois elle fonctionne différemment pour les mêmes paramètres. Vous constatez qu'il utilise la variable globale x .
Vous recherchez des endroits qui modifient x et constatez qu'il y a cinq endroits qui le modifient. Maintenant, bonne chance pour savoir dans quels cas la fonction A fait quoi ...
la source
Vous avez en quelque sorte répondu à votre propre question. Ils sont difficiles à gérer lorsqu'ils sont «maltraités», mais ils peuvent être utiles et [assez] prévisibles lorsqu'ils sont utilisés correctement par quelqu'un qui sait les contenir. La maintenance et les modifications apportées aux globaux sont généralement un cauchemar, aggravé par l’augmentation de la taille de l’application.
Les programmeurs expérimentés qui peuvent faire la différence entre les options globales étant la seule option et les solutions faciles, peuvent avoir un minimum de problèmes pour les utiliser. Mais les problèmes sans fin qui peuvent survenir avec leur utilisation nécessitent de ne pas les utiliser.
edit: Pour clarifier ce que je veux dire, les globals sont imprévisibles par nature. Comme pour tout ce qui est imprévisible, vous pouvez prendre des mesures pour limiter l'imprévisibilité, mais il y a toujours des limites à ce qui peut être fait. Ajoutez à cela les soucis des nouveaux développeurs rejoignant le projet devant traiter des variables relativement inconnues, les recommandations contre l'utilisation de globals doivent être compréhensibles.
la source
Il y a beaucoup de problèmes avec Singletons - voici les deux plus gros problèmes dans mon esprit.
Cela rend les tests unitaires problématiques. L'état global peut être contaminé d'un test à l'autre
Il applique la règle stricte "Un-et-un-un" qui, même si elle ne peut pas changer, le fait soudainement. Toute une série de codes d'utilitaires qui utilisaient l'objet accessible de manière globale doivent ensuite être modifiés.
Cela dit, la plupart des systèmes ont besoin de Big Global Objects. Il s’agit d’éléments volumineux et coûteux (par exemple, gestionnaires de connexion de base de données) ou contenant des informations d’état omniprésentes (par exemple, des informations de verrouillage).
L'alternative à un Singleton est de créer ces grands objets globaux au démarrage et de les transmettre en tant que paramètres à toutes les classes ou méthodes ayant besoin d'accéder à cet objet.
Le problème ici est que vous vous retrouvez avec un gros jeu de "passe-le-colis". Vous avez un graphique des composants et de leurs dépendances, et certaines classes créent d'autres classes, et chacune doit contenir un ensemble de composants de dépendance simplement parce que leurs composants générés (ou les composants des composants générés) en ont besoin.
Vous rencontrez de nouveaux problèmes de maintenance. Un exemple: Soudainement, votre composant "WidgetFactory" situé au cœur du graphique nécessite un objet de minuterie que vous souhaitez simuler. Toutefois, "WidgetFactory" est créé par "WidgetBuilder", qui fait partie de "WidgetCreationManager". Trois classes doivent être informées de cet objet de minuterie, même si une seule l'utilise réellement. Vous vous trouvez vouloir abandonner et revenir à Singletons, et rendre simplement cet objet timer globalement accessible.
Heureusement, c’est exactement le problème qui est résolu par un framework d’injection de dépendance. Vous pouvez simplement dire au framework quelles classes il doit créer, et il utilise la réflexion pour déterminer le graphe de dépendances à votre place et construit automatiquement chaque objet quand il le souhaite.
En résumé, les singletons sont mauvais et l’alternative consiste à utiliser un framework d’injection de dépendance.
J'utilise par hasard Castle Windsor, mais vous avez l'embarras du choix. Voir cette page depuis 2008 pour une liste des frameworks disponibles.
la source
Tout d'abord, pour que l'injection de dépendance soit "avec état", vous devez utiliser des singletons, de sorte que les gens qui disent que c'est en quelque sorte une alternative se trompent. Les personnes utilisent des objets de contexte globaux tout le temps ... Même l'état de session, par exemple, est essentiellement une variable globale. Tout transférer, que ce soit par injection de dépendance ou non, n'est pas toujours la meilleure solution. Je travaille actuellement sur une très grosse application qui utilise beaucoup d'objets de contexte global (singletons injectés via un conteneur IoC) et le débogage n'a jamais posé de problème. Surtout avec une architecture pilotée par les événements, il peut être préférable d'utiliser des objets de contexte global plutôt que de transmettre ce qui a changé. Cela dépend de qui vous demandez.
Tout peut être abusé et cela dépend aussi du type d'application. L'utilisation de variables statiques dans une application Web, par exemple, est complètement différente d'une application de bureau. Si vous pouvez éviter les variables globales, faites-le, mais elles ont parfois leur utilité. Au minimum, assurez-vous que vos données globales se trouvent dans un objet contextuel clair. En ce qui concerne le débogage, rien ne peut être résolu par une pile d'appels et par certains points d'arrêt.
Je tiens à souligner que l'utilisation aveugle de variables globales est une mauvaise idée. Les fonctions doivent être réutilisables et ne pas se soucier de l'origine des données - la référence à des variables globales associe la fonction à une entrée de données spécifique. C'est pourquoi il devrait être passé et pourquoi l'injection de dépendance peut être utile, bien que vous ayez toujours affaire à un magasin de contexte centralisé (via singletons).
Btw ... Certaines personnes pensent que l'injection de dépendance est mauvaise, y compris le créateur de Linq, mais cela n'empêchera pas les gens de l'utiliser, y compris moi-même. En fin de compte, l'expérience sera votre meilleur professeur. Il y a des moments pour suivre les règles et des temps pour les enfreindre.
la source
Étant donné que certaines autres réponses font la distinction entre un état global mutable et immuable , je voudrais dire que même les variables / paramètres globaux immuables sont souvent gênants .
Considérez la question:
D'accord, pour un petit programme, ce n'est probablement pas un problème, mais dès que vous faites cela avec des composants dans un système légèrement plus grand, l'écriture de tests automatisés devient soudainement plus difficile car tous les tests (exécutés dans le même processus) doivent fonctionner avec la même valeur de configuration globale.
Si toutes les données de configuration sont transmises explicitement, les composants deviennent beaucoup plus faciles à tester et vous n'avez jamais à vous soucier de la procédure d'amorce d'une valeur de configuration globale pour plusieurs tests, peut-être même en parallèle.
la source
Tout d’abord, vous pouvez rencontrer exactement le même problème que vous pouvez utiliser avec des singletons. Ce qui ressemble aujourd’hui à une "chose globale dont je n’ai besoin que d’une" se transformera soudainement en une chose dont vous avez besoin de plus en plus.
Par exemple, vous créez aujourd'hui ce système de configuration global car vous souhaitez une configuration globale pour l'ensemble du système. Quelques années plus tard, vous passez à un autre système et quelqu'un dit: "Hé, vous savez, cela fonctionnerait mieux s'il y avait une configuration globale générale et une configuration spécifique à la plate-forme." Tout à coup, vous avez tout ce travail pour rendre vos structures globales non globales, vous pouvez donc en avoir plusieurs.
(Ce n'est pas un exemple aléatoire ... cela est arrivé avec notre système de configuration dans le projet dans lequel je suis actuellement.)
Considérant que le coût de la création de quelque chose de non global est généralement insignifiant, il est ridicule de le faire. Vous ne faites que créer des problèmes futurs.
la source
L’autre problème, curieusement, est qu’elles rendent l’application difficile à l’échelle, car elles ne sont pas assez «globales». La portée d'une variable globale est le processus.
Si vous souhaitez faire évoluer votre application en utilisant plusieurs processus ou en s'exécutant sur plusieurs serveurs, vous ne pouvez pas. Du moins pas avant de factoriser tous les globaux et de les remplacer par un autre mécanisme.
la source
Un état mondial modifiable est mauvais car il est très difficile pour notre cerveau de prendre en compte plus de quelques paramètres à la fois et de déterminer comment ils se combinent à la fois du point de vue du timing et du point de vue de la valeur pour influer sur quelque chose.
Par conséquent, nous sommes très mal à déboguer ou à tester un objet dont le comportement a plus que quelques raisons externes à modifier lors de l'exécution d'un programme. Laissons seul lorsque nous devons raisonner à propos de dizaines d'objets pris ensemble.
la source
Les langues conçues pour la conception de systèmes sécurisés et robustes éliminent souvent complètement l'état mutable global . (On peut dire que cela ne signifie pas de globaux, car les objets immuables ne sont pas vraiment statiques, car ils n'ont jamais de transition d'état.)
Joe-E est un exemple, et David Wagner explique la décision ainsi:
Donc, une façon de penser est
Par conséquent, l'état mutable à l'échelle mondiale rend plus difficile la
L'état mutable global est similaire à l' enfer de la DLL . Au fil du temps, différentes parties d'un grand système nécessiteront un comportement légèrement différent de celui des parties partagées de l'état mutable. La résolution des incohérences entre l'enfer de la DLL et l'état mutable partagé nécessite une coordination à grande échelle entre des équipes disparates. Ces problèmes ne se produiraient pas si l’état global avait été correctement défini pour commencer.
la source
Les globales ne sont pas si mauvaises. Comme indiqué dans plusieurs autres réponses, le vrai problème avec ces problèmes est que votre chemin de dossier global peut être, demain, l'un des plusieurs, voire des centaines. Si vous écrivez un programme rapide et ponctuel, utilisez globals si c'est plus facile. En règle générale, toutefois, il est préférable d’envisager des multiples, même lorsque vous pensez en avoir besoin. Il n'est pas agréable de devoir restructurer un programme complexe de grande taille qui doit soudainement se connecter à deux bases de données.
Mais ils ne nuisent pas à la fiabilité. Toute donnée référencée à de nombreux endroits dans votre programme peut causer des problèmes si elle change de manière inattendue. Les énumérateurs s'étranglent lorsque la collection qu'ils énumèrent est modifiée en cours d'énumération. Les événements en file d'attente peuvent se jouer des tours. Les fils peuvent toujours faire des ravages. Tout ce qui n'est pas une variable locale ou un champ non modifiable pose un problème. Les problèmes globaux sont ce genre de problème, mais vous ne pourrez pas résoudre ce problème en les rendant non globaux.
Si vous êtes sur le point d'écrire dans un fichier et que le chemin du dossier est modifié, la modification et l'écriture doivent être synchronisées. (Comme l’un des milliers de problèmes, vous pouvez saisir le chemin, puis ce répertoire est supprimé, puis le chemin du dossier est remplacé par un bon répertoire, puis vous essayez d’écrire dans le répertoire supprimé.) Le problème existe si le chemin du dossier est global ou est l’un des milliers que le programme utilise actuellement.
Il existe un réel problème avec les champs auxquels différents événements d'une file d'attente, différents niveaux de récursivité ou différents threads peuvent accéder. Pour faire simple (et simpliste): les variables locales sont bonnes et les champs sont mauvais. Mais les anciens globaux seront toujours des champs, de sorte que cette question (si importante soit-elle) ne s’applique pas au statut Bon ou Mauvais des champs Global.
Ajout: Problèmes de multithreading:
(Notez que vous pouvez avoir des problèmes similaires avec une file d'attente d'événements ou des appels récursifs, mais le multithreading est de loin le pire.) Considérez le code suivant:
Si
filePath
est une variable locale ou une sorte de constante, votre programme ne va pas échouer lors de son exécution car sa valeurfilePath
est null. Le chèque fonctionne toujours. Aucun autre thread ne peut changer sa valeur. Sinon , il n'y a aucune garantie. Quand j'ai commencé à écrire des programmes multithreads en Java, j'ai toujours eu des exceptions NullPointerExceptions. Toutles autres threads peuvent changer la valeur à tout moment, et ils le font souvent. Comme plusieurs autres réponses le soulignent, cela crée de sérieux problèmes de test. La déclaration ci-dessus peut fonctionner un milliard de fois, en faisant l’objet de tests approfondis et complets, puis en explosant une fois en production. Les utilisateurs ne pourront pas reproduire le problème et cela ne se reproduira plus tant qu'ils ne se seront pas convaincus qu'ils voyaient des choses et les ont oubliés.Les problèmes globaux ont définitivement ce problème, et si vous pouvez les éliminer complètement ou les remplacer par des constantes ou des variables locales, c'est une très bonne chose. Si du code sans état est exécuté sur un serveur Web, vous le pouvez probablement. En règle générale, tous vos problèmes de multithreading peuvent être résolus par la base de données.
Mais si votre programme doit se souvenir d'éléments d'une action utilisateur à l'autre, vous aurez des champs accessibles par tous les threads en cours d'exécution. Basculer un champ global vers un champ aussi non global n’améliorera pas la fiabilité.
la source
L'état est inévitable dans toute application réelle. Vous pouvez le résumer comme bon vous semble, mais une feuille de calcul doit contenir des données dans des cellules. Vous pouvez créer des objets de cellule avec uniquement des fonctions en tant qu'interface, mais cela ne limite pas le nombre d'emplacements pouvant appeler une méthode sur la cellule et modifier les données. Vous créez des hiérarchies d'objet entières pour essayer de masquer des interfaces afin que d'autres parties du code ne puissent paschanger les données par défaut. Cela n'empêche pas qu'une référence à l'objet contenant soit transmise de manière arbitraire. Rien de tout cela n'élimine par lui-même les problèmes de concurrence. Il est difficile de multiplier les accès aux données, mais cela n'élimine pas les problèmes perçus avec les globals. Si quelqu'un souhaite modifier un élément d'état, il le fait qu'il soit global ou via une API complexe (cette dernière ne fera que décourager, pas empêcher).
La vraie raison de ne pas utiliser le stockage global est d'éviter les conflits de noms. Si vous chargez plusieurs modules déclarant le même nom global, vous avez soit un comportement indéfini (très difficile à déboguer car les tests unitaires vont réussir), soit une erreur de l'éditeur de liens (je pense que C - Votre éditeur de liens vous avertit-il ou échoue-t-il?).
Si vous voulez réutiliser du code, vous devez être capable de récupérer un module d'un autre endroit et de ne pas l'avoir accidentellement placé sur votre global car ils en ont utilisé un du même nom. Ou si vous avez de la chance et obtenez une erreur, vous ne voulez pas avoir à changer toutes les références dans une section de code pour éviter la collision.
la source
Quand il est facile de voir et d'accéder à tout l'état global, les programmeurs finissent toujours par le faire. Ce que vous obtenez est non-dit et difficile à suivre les dépendances (int blahblah signifie que array foo est valable dans tout ce que vous voulez). En gros, il est presque impossible de maintenir des invariants de programme car tout peut être tourné indépendamment. someInt a une relation entre otherInt, difficile à gérer et à prouver si vous pouvez changer directement à tout moment.
Cela dit, cela peut être fait (il y a bien longtemps, dans certains systèmes), mais ces compétences sont perdues. Ils tournent principalement autour des conventions de codage et de nommage - le domaine a évolué pour de bonnes raisons. Votre compilateur et votre éditeur de liens effectuent un meilleur travail de vérification des invariants dans les données protégées / privées de classes / modules que de s’en remettre aux humains pour suivre un plan directeur et lire la source.
la source
Je ne vais pas dire si les variables globales sont bonnes ou mauvaises, mais ce que je vais ajouter à la discussion, c'est de dire que si vous n'utilisez pas l'état global, vous perdez probablement beaucoup de mémoire, surtout lorsque vous utilisez classess pour stocker leurs dépendances dans des champs.
Pour l’état global, il n’ya pas de problème de ce type, tout est global.
Par exemple: imaginons un scénario suivant: vous avez une grille de 10 x 10 composée de classes "Conseil" et "Mosaïque".
Si vous voulez le faire à la POO, vous passerez probablement l'objet "Board" à chaque "Tile". Disons maintenant que "Mosaïque" a 2 champs de type "octet" stockant sa coordonnée. La mémoire totale qu’elle prendrait sur une machine 32 bits pour une tuile serait de (1 + 1 + 4 = 6) octets: 1 pour x coord, 1 pour y coord et 4 pour un pointeur sur le tableau. Cela donne un total de 600 octets pour la configuration de tuiles 10x10
Dans le cas où la carte a une portée globale, un seul objet accessible à partir de chaque mosaïque ne devrait obtenir que 2 octets de mémoire pour chaque mosaïque, c’est-à-dire les octets de coordonnées x et y. Cela donnerait seulement 200 octets.
Donc, dans ce cas, vous obtenez 1/3 de l'utilisation de la mémoire si vous utilisez uniquement l'état global.
Ceci, outre d’autres choses, j’imagine que c’est une raison pour laquelle la portée globale reste encore dans des langages (relativement) de bas niveau comme le C ++.
la source
Plusieurs facteurs doivent être pris en compte avec l'état global:
Plus vous avez de globaux, plus vous avez de chances d'introduire des doublons, et donc de casser des choses lorsque les doublons ne sont plus synchronisés. Garder tous les globals dans votre mémoire humaine défaillante est à la fois nécessaire et douloureux.
Immutables / write once sont généralement acceptables, mais faites attention aux erreurs de séquence d'initialisation.
Les globals mutables sont souvent confondus avec des globals immuables…
Une fonction qui utilise efficacement les globales possède des paramètres "cachés" supplémentaires, ce qui rend la refactorisation plus difficile.
L’état global n’est pas mauvais, mais il a un coût certain: utilisez-le lorsque le bénéfice est supérieur au coût.
la source