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: "N'utilisez pas de variables globales" ou "Les singletons sont mauvais parce qu'ils sont globaux". Mais qu'est-ce vraiment si mauvais à propos de l'état global inquiétant?
Disons que j'ai besoin d'une configuration globale pour mon application, par exemple des chemins de dossier système ou des informations d'identification de base de données à l'échelle de l'application.
Dans ce cas, je ne vois aucune 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 en abuser , mais est-ce vraiment l'espace global [~ # ~] que [~ # ~] mal? Et si c'est le cas, quelles sont les bonnes alternatives?
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 de hasard n'importe où dans l'un ou l'autre module, 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 d'exécuter la méthode.
Cependant, si une méthode dans l'un des objets déclenche un effet secondaire qui modifie 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 désormais 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 peut ne pas sembler si grave, mais être capable de tester le code unitaire est une étape majeure dans le processus de preuve de son exactitude (ou du moins de son adéquation à l'objectif).
Dans le monde réel, cela peut avoir des conséquences très graves. Supposons que vous ayez une classe qui remplit une structure de données globale et une classe différente qui consomme les données dans cette structure de données, modifiant son état ou les détruisant dans le processus. Si la classe de processeur exécute une méthode avant la fin de la classe de populateur, le résultat est que la classe de processeur aura probablement des données incomplètes à traiter, et la structure de données sur laquelle la classe de populateur travaillait pourrait être corrompue ou détruite. Le comportement du programme dans ces circonstances devient complètement imprévisible et entraînera probablement une perte épique.
De plus, l'état global nuit à la lisibilité de votre code. Si votre code a une dépendance externe qui n'est pas explicitement introduite dans le code, quiconque obtient le travail de maintenance de votre code devra aller le chercher pour comprendre d'où il vient.
Quant aux alternatives existantes, il est impossible de n'avoir aucun état global, mais dans la pratique, il est généralement possible de restreindre l'état global à un seul objet qui enveloppe tous les autres, et qui ne doit jamais être référencé en s'appuyant sur les règles de portée 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 setter. C'est ce qu'on appelle l'injection de dépendance.
Il peut sembler idiot 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 isolément, 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 donc les possibilités de le réutiliser dans différents contextes. Si l'état est transmis et que les modifications apportées à l'état sont locales au bloc de code, vous pouvez passer dans n'importe quel état de votre choix (s'il s'agit du type de données correct) et demander à votre code de le traiter. Le code écrit dans ce style a tendance à avoir l'apparence d'une collection de composants peu associés qui peuvent être facilement échangés. Le code d'un module ne devrait pas se soucier d'où vient l'état, juste comment le traiter. Si vous passez l'état dans un bloc de code, ce bloc de code peut exister de manière isolée, ce n'est pas le cas si vous comptez sur l'état global.
Il existe de nombreuses autres raisons pour lesquelles le passage d'un État à un autre est largement supérieur à un État mondial. Cette réponse n'est nullement complète. Vous pourriez probablement écrire un livre entier sur les raisons pour lesquelles l'état mondial est mauvais.
L'état mondial mutable est mauvais pour de nombreuses raisons:
Alternatives à un état global mutable:
grep
pour le nom du type pour savoir quelles fonctions l'utilisent.Passez simplement une référence dans les fonctions qui en ont besoin. Ce n'est pas si dur.
Si vous dites "état", cela signifie généralement "état mutable". Et l'état mutable global est totalement mauvais, car cela signifie que n'importe quelle partie du programme peut influencer n'importe quelle autre partie (en changeant l'état global).
Imaginez le débogage d'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 ...
Vous avez en quelque sorte répondu à votre propre question. Ils sont difficiles à gérer lorsqu'ils sont "maltraités", mais peuvent être utiles et [quelque peu] prévisibles lorsqu'ils sont utilisés correctement, par quelqu'un qui sait comment les contenir. La maintenance et les modifications de/sur les globales sont généralement un cauchemar, aggravées par l'augmentation de la taille de l'application.
Les programmeurs expérimentés qui peuvent faire la différence entre les globaux étant la seule option et eux étant la solution facile, peut ont un minimum de problèmes à les utiliser. Mais les problèmes possibles sans fin qui peuvent survenir avec leur utilisation nécessitent des conseils contre leur utilisation.
edit: Pour clarifier ce que je veux dire, les globaux sont imprévisibles par nature. Comme pour tout ce qui est imprévisible, vous pouvez prendre des mesures pour contenir l'imprévisibilité, mais il y a toujours des limites à ce qui peut être fait. Ajoutez à cela les tracas des nouveaux développeurs rejoignant le projet devant faire face à des variables relativement inconnues, les recommandations contre l'utilisation de globaux devraient être compréhensibles.
Il y a beaucoup de problèmes avec les Singletons - voici les deux plus gros problèmes dans mon esprit.
Cela rend les tests unitaires problématiques. L'état mondial peut être contaminé d'un test à l'autre
Il applique la règle stricte "Un et un seul", ce qui, même s'il ne pouvait pas changer, le fait soudainement. Un tas de code utilitaire qui utilisait l'objet accessible globalement doit ensuite être modifié.
Cela dit, la plupart des systèmes ont besoin de Big Global Objects. Ce sont des éléments volumineux et coûteux (par exemple, les gestionnaires de connexion de base de données), ou qui contiennent des informations d'état omniprésentes (par exemple, des informations de verrouillage).
L'alternative à un singleton consiste à créer ces gros objets globaux au démarrage et à les transmettre en tant que paramètres à toutes les classes ou méthodes qui ont 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 tas de composants de dépendance juste 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: tout à coup, votre "WidgetFactory", composant au plus profond du graphique a besoin d'un objet timer que vous souhaitez simuler. Cependant, "WidgetFactory" est créé par "WidgetBuilder" qui fait partie de "WidgetCreationManager", et vous devez avoir trois classes connaissant cet objet timer même si une seule l'utilise réellement. Vous vous retrouvez à vouloir abandonner et revenir à Singletons, et juste rendre cet objet timer globalement accessible.
Heureusement, c'est exactement le problème qui est résolu par un cadre d'injection de dépendance. Vous pouvez simplement indiquer au framework les classes dont il a besoin pour créer, et il utilise la réflexion pour comprendre le graphique de dépendance pour vous, et construit automatiquement chaque objet quand il en a besoin.
Donc, en résumé, les singletons sont mauvais, et l'alternative est d'utiliser un cadre d'injection de dépendance.
Il se trouve que j'utilise Castle Windsor, mais vous avez l'embarras du choix. Voir cette page à partir de 2008 pour une liste des frameworks disponibles.
Tout d'abord, pour que l'injection de dépendance soit "avec état", vous devez utiliser des singletons, donc les gens qui disent que c'est en quelque sorte une alternative se trompent. Les gens utilisent des objets de contexte global tout le temps ... Même l'état de session par exemple est par essence une variable globale. Tout passer, que ce soit par injection de dépendance ou non, n'est pas toujours la meilleure solution. Je travaille actuellement sur une très grande application qui utilise beaucoup d'objets de contexte global (singletons injectés via un conteneur IoC) et cela n'a jamais été un problème de débogage. 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 faire circuler 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 par exemple dans une application Web 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é. Assurez-vous à tout le moins que vos données globales se trouvent dans un objet contextuel clair. En ce qui concerne le débogage, rien qu'une pile d'appels et certains points d'arrêt ne peuvent résoudre.
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 la provenance des données - la référence aux variables globales couple la fonction à une entrée de données spécifique. C'est pourquoi il doit être transmis et pourquoi l'injection de dépendances peut être utile, bien que vous ayez toujours affaire à un magasin de contexte centralisé (via des 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.
Étant donné que d'autres réponses ici rendent la distinction entre mutable et immuable globale état, je voudrais dire que même immuable les variables/paramètres globaux sont souvent une gêne.
Considérez la question:
... Disons que j'ai besoin d'une configuration globale pour mon application, par exemple des chemins de dossier système ou des informations d'identification de base de données à l'échelle de l'application. ...
Bon, pour un petit programme, ce n'est probablement pas un problème, mais dès que vous le faites avec des composants dans un système légèrement plus grand , écriture automatisée les tests deviennent soudainement plus difficiles 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 comment bootstrap une valeur de configuration globale pour plusieurs tests, peut-être même en parallèle.
Pourquoi Global State est-il si mal?
Mutable l'état global est mauvais parce qu'il est très difficile pour notre cerveau de prendre en compte plus de quelques paramètres à la fois et de comprendre comment ils se combinent à la fois dans une perspective temporelle et une perspective de valeur pour affecter quelque chose.
Par conséquent, nous sommes très mauvais pour déboguer ou tester un objet dont le comportement a plus de quelques raisons externes à modifier pendant l'exécution d'un programme. Encore moins quand nous devons raisonner sur des dizaines de ces objets pris ensemble.
L'état est inévitable dans toute application réelle. Vous pouvez le terminer comme vous le souhaitez, mais une feuille de calcul doit contenir des données dans les cellules. Vous pouvez créer des objets de cellule avec uniquement des fonctions d'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'objets entiers pour essayer de masquer les interfaces afin que d'autres parties du code ne peuvent pas modifier les données par défaut. Cela n'empêche pas une référence arbitraire à l'objet contenant. Rien de tout cela n'élimine les problèmes de concurrence par lui-même. Cela rend plus difficile la prolifération de l'accès aux données, mais cela n'élimine pas réellement les problèmes perçus avec les globaux. Si quelqu'un veut modifier un morceau d'état, il va le faire qu'il soit global ou via une API complexe (la dernière ne fera que décourager, pas empêcher).
La vraie raison de ne pas utiliser le stockage global est d'éviter les collisions de noms. Si vous chargez plusieurs modules qui déclarent le même nom global, vous avez soit un comportement indéfini (très difficile à déboguer car les tests unitaires passeront), 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 pouvoir récupérer un module à un autre endroit et ne pas le faire accidentellement marcher sur votre global car ils en ont utilisé un avec le même nom. Ou si vous êtes chanceux et obtenez une erreur, vous ne voulez pas avoir à modifier toutes les références dans une section de code pour éviter la collision.
Les langages conçus pour la conception de systèmes sécurisés et robustes se débarrassent souvent de l'état global mutable . (Cela signifie sans doute pas de globaux, car les objets immuables ne sont en quelque sorte pas vraiment dynamiques car ils n'ont jamais de transition d'état.)
Joe-E est un exemple, et David Wagner explique la décision ainsi:
L'analyse de qui a accès à un objet et le principe du moindre privilège sont tous deux inversés lorsque les capacités sont stockées dans des variables globales et sont donc potentiellement lisibles par n'importe quelle partie du programme. Une fois qu'un objet est globalement disponible, il n'est plus possible de limiter la portée de l'analyse: l'accès à l'objet est un privilège qui ne peut être refusé à aucun code du programme. Joe-E évite ces problèmes en vérifiant que la portée globale ne contient aucune capacité, uniquement des données immuables.
Donc, une façon d'y penser est
Par conséquent, un état mutable à l'échelle mondiale rend plus difficile
L'état mutable global est similaire à enfer DLL . Au fil du temps, différents morceaux d'un grand système nécessiteront un comportement subtilement différent des morceaux partagés d'état mutable. La résolution de DLL enfer et incohérences d'état mutables partagées 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 au départ.
Eh bien, pour commencer, vous pouvez rencontrer exactement le même problème que vous pouvez avec les singletons. Ce qui ressemble aujourd'hui à "une chose globale dont je n'ai besoin que d'une" se transformera soudainement en quelque chose dont vous aurez plus besoin en cours de route.
Par exemple, aujourd'hui, vous créez ce système de configuration global car vous souhaitez une configuration globale pour l'ensemble du système. Quelques années plus tard, vous portez vers un autre système et quelqu'un dit "hé, vous savez, cela pourrait mieux fonctionner s'il y avait une configuration globale générale et une configuration spécifique à la plate-forme". Soudain, 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 s'est produit avec notre système de configuration dans le projet dans lequel je suis actuellement.)
Étant donné que le coût de fabrication de quelque chose de non global est généralement insignifiant, il est stupide de le faire. Vous créez simplement des problèmes futurs.
L'autre problème est curieusement qu'elles rendent une application difficile à mettre à 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 exécutant sur plusieurs serveurs, vous ne pouvez pas. Du moins pas avant d'avoir pris en compte tous les globaux et les avoir remplacés par un autre mécanisme.
Les globaux ne sont pas si mauvais. Comme indiqué dans plusieurs autres réponses, le vrai problème avec elles est que quel est, aujourd'hui, votre chemin d'accès au dossier global peut, demain, être l'un des nombreux, voire des centaines. Si vous écrivez un programme rapide et unique, utilisez les globaux si c'est plus facile. En règle générale, cependant, autoriser les multiples même si vous pensez que vous n'en avez besoin que d'une est la voie à suivre. Ce n'est pas agréable d'avoir à restructurer un grand programme complexe qui a soudainement besoin de parler à deux bases de données.
Mais ils ne nuisent pas à la fiabilité. Toutes les données référencées à de nombreux endroits de votre programme peuvent provoquer des problèmes si elles changent de façon inattendue. Les énumérateurs s'étouffent lorsque la collection qu'ils énumèrent est modifiée au milieu de l'énumération. Les événements de file d'attente d'événements peuvent se jouer des tours. Les discussions peuvent toujours provoquer des havoks. Tout ce qui n'est pas une variable locale ou un champ immuable est un problème. Les globaux sont ce genre de problème, mais vous n'allez pas y remédier en les rendant non globaux.
Si vous êtes sur le point d'écrire dans un fichier et que le chemin d'accès au dossier change, la modification et l'écriture doivent être synchronisées. (Comme l'une des mille choses qui pourraient mal se passer, disons que vous prenez le chemin, puis ce répertoire est supprimé, puis le chemin du dossier est changé en 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 mille que le programme utilise actuellement.
Il existe un réel problème avec les champs accessibles par différents événements d'une file d'attente, différents niveaux de récursivité ou différents threads. Pour faire simple (et simpliste): les variables locales sont bonnes et les champs sont mauvais. Mais les anciens globaux vont toujours être des champs, donc ce problème (aussi important soit-il) ne s'applique pas au statut Bon ou Mauvais des champs Globaux.
Ajout: Problèmes de multithreading:
(Notez que vous pouvez rencontrer 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:
if (filePath != null) text = filePath.getName();
Si filePath
est une variable locale ou une sorte de constante, votre programme ne va pas échouer lors de l'exécution parce que filePath
est nul. 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 multithread en Java, j'ai toujours eu des NullPointerExceptions sur des lignes comme celle-ci. Tout autre thread peut changer la valeur à tout moment, et c'est souvent le cas. Comme le soulignent plusieurs autres réponses, cela crée de sérieux problèmes de test. La déclaration ci-dessus peut fonctionner un milliard de fois, l'obtenir à travers des tests approfondis et complets, puis exploser une fois en production. Les utilisateurs ne seront pas en mesure de reproduire le problème, et cela ne se reproduira pas tant qu'ils ne se seront pas convaincus qu'ils voyaient des choses et l'avaient oublié.
Les globaux ont certainement 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 vous avez du code sans état exécuté sur un serveur Web, vous le pouvez probablement. En règle générale, tous vos problèmes de multithreading peuvent être pris en charge par la base de données.
Mais si votre programme doit se souvenir des choses d'une action utilisateur à l'autre, vous aurez des champs accessibles par tous les threads en cours d'exécution. Passer d'un global à un tel domaine non global n'aidera pas la fiabilité.
Je ne vais pas dire si les variables globales sont bonnes ou mauvaises, mais ce que je vais ajouter à la discussion 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 mondial, il n'y a pas un tel problème, tout est mondial.
Par exemple: imaginez un scénario suivant: Vous avez une grille 10x10 qui est faite de "Board" et de "Tile" sans classe.
Si vous voulez le faire de la manière OOP vous passerez probablement l'objet "Board" à chaque "Tile". Disons maintenant que "Tile" a 2 champs de type "byte" stockant ses coordonnées . La mémoire totale qu'il faudrait 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 vers la carte. Cela donne un total de 600 octets pour la configuration de 10 x 10 tuiles
Maintenant, dans le cas où la carte est de portée globale, un seul objet accessible à partir de chaque tuile ne devrait obtenir que 2 octets de mémoire par chaque tuile, c'est-à-dire les octets de coordonnées x et y. Cela ne donnerait que 200 octets.
Donc, dans ce cas, vous obtenez 1/3 de l'utilisation de la mémoire si vous n'utilisez que l'état global.
Ceci, entre autres choses, je suppose que c'est une raison pour laquelle la portée globale reste toujours dans des langages de niveau (relativement) bas comme C++
Lorsqu'il est facile de voir et d'accéder à tout l'état global, les programmeurs finissent invariablement par le faire. Ce que vous obtenez est tacite et difficile à suivre les dépendances (int blahblah signifie que le tableau foo est valide dans tout). Essentiellement, il est presque impossible de maintenir les invariants du programme car tout peut être manipulé indépendamment. someInt a une relation entre otherInt, qui est difficile à gérer et plus difficile à prouver si vous pouvez changer directement l'un ou l'autre à tout moment.
Cela dit, cela peut être fait (il y a longtemps, c'était le seul moyen dans certains systèmes), mais ces compétences sont perdues. Ils tournent principalement autour des conventions de codage et de dénomination - le domaine a évolué pour une bonne raison. Votre compilateur et votre éditeur de liens font un meilleur travail de vérification des invariants dans les données protégées/privées des classes/modules que de compter sur les humains pour suivre un plan directeur et lire la source.
Il y a plusieurs facteurs à considérer 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 sont désynchronisés. Garder tous les globaux dans votre mémoire humaine faillible devient à la fois nécessaire et douloureux.
Immutables/écriture une fois sont généralement OK, mais attention aux erreurs de séquence d'initialisation.
Les globaux mutables sont souvent confondus avec des globaux immuables…
Une fonction qui utilise efficacement les globaux a des paramètres supplémentaires "cachés", ce qui rend la refactorisation plus difficile.
L'état mondial n'est pas mauvais, mais il a un coût certain - utilisez-le lorsque les avantages l'emportent sur les coûts.