États mondiaux: pourquoi et comment les éviter


Conditions mondiales. Cette phrase provoque la peur et la douleur au cœur de chaque développeur qui a eu le malheur de faire face à ce phénomène. Avez-vous déjà rencontré un comportement d'application inattendu, ne comprenant pas ses raisons, comme un malheureux chevalier essayant de tuer Hydra avec plusieurs têtes? Vous vous retrouvez dans un cycle sans fin d'essais et d'erreurs, 90% du temps vous vous demandez ce qui se passe?

Tout cela peut être des conséquences gênantes des globales: des variables cachées qui changent leur état dans des endroits inconnus, pour des raisons que vous n'avez pas encore compris.

Aimez-vous vous promener dans le noir pendant que vous essayez de changer l'application? Bien sûr, je ne l'aime pas. Heureusement, j'ai des bougies pour vous:

  1. Tout d'abord, je décrirai ce que nous appelons le plus souvent des États mondiaux. Ce terme n'est pas toujours utilisé avec précision, il doit donc être clarifié.
  2. Ensuite, nous découvrirons pourquoi les globaux sont nuisibles à notre base de code.
  3. Ensuite, je vais expliquer comment couper la portée des globales pour les transformer en variables locales.
  4. Enfin, je parlerai de l'encapsulation et pourquoi la croisade contre les variables globales n'est qu'une partie du gros problème.

J'espère que cet article explique tout ce que vous devez savoir sur les états mondiaux. Si vous pensez que j'ai beaucoup manqué et que vous me détestez pour cela et que vous ne voulez plus me voir, écrivez à ce sujet dans les commentaires. Ce sera agréable pour moi, mes lecteurs et tous ceux qui sont soudainement apparus sur cette page.

Êtes-vous prêt, cher lecteur, à sauter sur un cheval et à connaître votre ennemi? Allez trouver ces globales et faites-leur goûter l'acier de nos épées!

Qu'est-ce qu'une condition?




Commençons par les bases pour que les développeurs se comprennent.

Un état est une définition d'un système ou d'une entité. Les états se trouvent dans la vraie vie:

  • Lorsque l'ordinateur est éteint, son état est désactivé.
  • Quand une tasse de thé est chaude, son état est chaud.

Dans le développement de logiciels, certaines constructions (telles que des variables) peuvent avoir des états. Disons que la chaîne «bonjour» ou le nombre 11 ne sont pas considérés comme des états, ce sont des valeurs. Ils deviennent état lorsqu'ils sont attachés à une variable et placés en mémoire.

<?php echo "hello"; // No state here! $lala = "hello"; // The variable $lala has the state 'hello'. 

On distingue deux types d'états:

États variables: après leur initialisation, ils peuvent évoluer à tout moment lors de l'exécution de votre application.

 <?php $lala = "hello"; // Initialisation of the variable. $lala = "hallo"; // The state of the variable $lala can be changed at runtime. 

États immuables: ne peut pas changer pendant l'exécution. Vous affectez le premier état à votre variable et sa valeur ne change pas par la suite. Les "constantes" de la vie quotidienne sont appelées des exemples d'états immuables:

 <?php define("GREETING", "hello"); // Constant definition. echo GREETING; GREETING = "hallo"; // This line will produce an error! 

Écoutons maintenant une conversation hypothétique entre Denis et Vasily, vos collègues développeurs:

- Dan! Vous avez créé des variables globales partout! Ils ne peuvent pas être modifiés sans que tout se brise! Je vais te tuer!
- Nifiga, Vasek! Mes fortunes mondiales sont impressionnantes! J'y mets mon âme, ce sont des chefs-d'œuvre! J'adore mes globales!

Le plus souvent, les développeurs appellent des états globaux, des variables globales ou des globaux ce qu'ils devraient appeler des états mutables globaux. Autrement dit, les états qui peuvent être modifiés dans la plus grande des étendues à votre disposition: dans l'application entière.

Lorsqu'une variable n'a pas la totalité de l'application comme étendue, nous parlons de variables locales ou locales. Ils existent dans certaines zones de visibilité données, moins que la zone de l'application entière.

 <?php namespace App\Ecommerce; $global = "I'm a mutable global variable!"; // global variable class Shipment { public $warehouse; // local variable existing in the whole class public function __construct() { $info = "You're creating a shipment object!"; // local variable bound to the constructor scope echo $info; } } class Product { public function __construct() { global $global; $global = "I change the state now 'cause I can!"; echo "You're creating a product object!"; // no state here } } 

Vous pourriez penser: combien il est pratique d'avoir des variables auxquelles vous pouvez accéder de partout et de les changer! Je peux transférer des états d'une partie de l'application à une autre! Pas besoin de les passer par des fonctions et d'écrire autant de code! Salut, état mutable mondial!

Si vous le pensez vraiment, je vous recommande fortement de continuer la lecture.

Des États mondiaux pires que la peste et le choléra?


Le plus grand diagramme de liens


Réalité: il vous sera plus facile de créer un diagramme de connexion d'application précis s'il ne contient que des paramètres régionaux avec des étendues petites et définies, sans globaux.

Pourquoi?

Disons que vous avez une grande application avec des variables globales. Chaque fois que vous devez changer quelque chose, vous devez:

  • Rappelons que ces états mondiaux mutables existent.
  • Estimez si elles affecteront la portée que vous allez modifier.

Habituellement, vous n'avez pas besoin de penser aux variables locales qui se trouvent dans d'autres étendues, mais quoi que vous fassiez, vous devez toujours garder dans votre cerveau fatigué un endroit pour les états mutables globaux, car ils peuvent affecter toutes les étendues.

De plus, vos états mutables globaux peuvent changer n'importe où dans l'application. Il faut généralement se demander quel est leur état actuel. Cela signifie que vous êtes obligé de rechercher dans l'application, en essayant de calculer les valeurs des globales dans la portée modifiable.

Ce n'est pas tout. Si vous devez changer l'état des globales, vous n'imaginerez pas quelle portée cela affectera. Cela conduira-t-il à un comportement inattendu d'une autre classe, méthode ou fonction? Succès dans la recherche.

En bref, vous combinez toutes les classes, méthodes et fonctions qui utilisent le même état global. N'oubliez pas: les dépendances augmentent considérablement la complexité . Cela vous fait-il peur? Ça devrait l'être. Les petites zones de visibilité spécifiques sont très utiles: vous n'avez pas besoin de garder à l'esprit l'ensemble de l'application, il suffit de ne se souvenir que des zones avec lesquelles vous travaillez.

Les gens ne suivent pas immédiatement une grande quantité d'informations. Lorsque nous essayons de le faire, nous épuisons rapidement l'offre de capacités cognitives, il devient difficile pour nous de nous concentrer et nous commençons à créer des bugs et des choses stupides. C'est pourquoi il est si désagréable d'agir dans le cadre global de votre application.

Collisions de noms mondiaux


Il est difficile d'utiliser des bibliothèques tierces. Imaginez que vous souhaitez utiliser cette bibliothèque super cool qui colore au hasard chaque personnage avec un effet de scintillement. Le rêve de tout développeur! Si cette bibliothèque utilise également des globales qui ont les mêmes noms que les vôtres, vous apprécierez les collisions de noms. Votre application va planter et vous en devinerez les raisons, probablement pour longtemps:

  • Tout d'abord, vous devrez découvrir que votre bibliothèque utilise des variables globales.
  • Deuxièmement, vous devez calculer quelle variable a été utilisée pendant l'exécution - la vôtre ou les bibliothèques? Ce n'est pas si simple, les noms sont les mêmes!
  • Troisièmement, puisque vous ne pouvez pas modifier la bibliothèque vous-même, vous devez renommer vos variables mutables globales. S'il est utilisé tout au long de l'application, vous pleurerez.

À chaque étape, vous arracherez vos cheveux de rage et de désespoir. Bientôt, vous n'aurez plus besoin d'un peigne. Il est peu probable que ce scénario vous séduise. Peut-être que quelqu'un se souviendra que les bibliothèques JavaScript Mootools, Underscore et jQuery se heurtaient toujours si elles n'étaient pas placées dans des portées plus petites. Oh, et le célèbre objet global $ dans jQuery!

Les tests se transformeront en cauchemar


Si je ne vous ai pas encore convaincu, regardons la situation du point de vue des tests unitaires: comment écrire des tests en présence de variables globales? Étant donné que les tests peuvent changer les globaux, vous ne savez pas dans quel test se trouvait l’état. Vous devez isoler les tests les uns des autres et les états globaux les lient ensemble.

L'avez-vous déjà eu pour que les tests d'isolement fonctionnent correctement et que lorsque vous exécutez le package entier, échouent-ils? Non? Et je l'avais. Chaque fois que je m'en souviens, je souffre.

Problèmes de concurrence


Les états globaux variables peuvent causer beaucoup de problèmes si vous avez besoin de simultanéité. Lorsque vous modifiez l'état des globaux dans plusieurs threads d'exécution, alors dirigez-vous sur les talons dans un état puissant de la course .

Si vous êtes un développeur PHP, cela ne vous dérange pas, sauf si vous utilisez des bibliothèques qui vous permettent de créer du parallélisme. Cependant, lorsque vous apprenez une nouvelle langue dans laquelle le parallélisme est facile à mettre en œuvre, j'espère que vous vous souviendrez de ma prose.

Éviter les états mutables mondiaux




Bien que les États mutables mondiaux puissent causer une tonne de problèmes, ils sont parfois difficiles à éviter.

Prenez l'API REST: les points de terminaison reçoivent une sorte de requêtes HTTP avec des paramètres et envoient des réponses. Ces paramètres HTTP envoyés au serveur peuvent être demandés à plusieurs niveaux de votre application. Il est très tentant de rendre ces paramètres globaux lors de la réception d'une requête HTTP, en les modifiant avant d'envoyer une réponse. Ajoutez du parallélisme au-dessus de chaque demande et la recette du sinistre est prête.

Les états mutables globaux peuvent également être directement pris en charge dans les implémentations de langage. Par exemple, en PHP, il y a des superglobaux .

Si les globaux viennent de quelque part, comment y faire face? Comment refactoriser l'application de Denis, votre collègue développeur qui a créé des globaux partout où c'est possible, car il n'a rien lu sur le développement depuis 20 ans?

Arguments de fonction


Le moyen le plus simple d'éviter les globaux est de passer des variables à l'aide d'arguments de fonction. Prenons un exemple simple:

 <?php namespace App; use Router\HttpRequest; use App\Product\ProductData; use App\Exceptions; class ProductController { public function createAction(HttpRequest $httpReq) { $productData = $httpReq->get("productData"); if (!$this->productModel->validateProduct($productData)) { return ValidationException(sprintf("The product %d is not valid", $productData["id"])); } $product = $this->productModel->createProduct($productData); } } class Product { public function createProduct(array $productData): Product { $productData["name"] = "SuperProduct".$productData["name"]; // This is not what you should do; I talk about it later in the article. try { $product = $this->productDao->find($productData["id"]); return product; } catch (NotFoundException $e) { $product = $this->productDao->save($productData); return $product; } } } class ProductDao { private $db; public function find(int $id): array { return $this->db->find(['product' => $id]); } public function save(array $productData): array { return $this->db->saveProduct($productData); } } 

Comme vous pouvez le voir, le $productData du contrôleur, via une requête HTTP, passe par différents niveaux:

  1. Le contrôleur a reçu une demande HTTP.
  2. Paramètres passés au modèle.
  3. Paramètres passés au DAO .
  4. Les paramètres sont enregistrés dans la base de données d'application.

Nous pourrions rendre ce tableau de paramètres global lorsque nous le récupérons de la requête HTTP. Cela semble si simple: pas besoin de transférer des données vers 4 fonctions différentes. Cependant, en passant des paramètres comme arguments aux fonctions:

  • Cela montrera évidemment que ces fonctions utilisent le $productData .
  • Il montrera évidemment quelles fonctions utilisent quels paramètres. On peut voir que pour ProductDao::find partir du $productData , seul $id nécessaire, et pas tout.

Les globaux rendent le code moins compréhensible et associent les méthodes entre elles, ce qui est un prix très élevé pour un manque presque complet d'avantages.

Vous entendez déjà Denis protester: «Et si une fonction a trois arguments ou plus? Si vous devez en ajouter encore plus, la complexité de la fonction augmentera! Et qu'en est-il des variables, des objets et des autres constructions nécessaires partout? Les transmettrez-vous à chaque fonction de l'application? »

Les questions sont justes, cher lecteur. En bon développeur , vous devez expliquer à Denis, en utilisant vos compétences en communication, voici ce que:

«Denis, si vos fonctions ont trop d'arguments, alors les fonctions elles-mêmes peuvent être un problème. Ils en font probablement trop, sont responsables de trop de choses. Vous ne pensiez pas à les diviser en fonctions plus petites? " .

Se sentant comme un orateur à l'Acropole d'Athènes, vous continuez:

«Si vous avez besoin de variables dans de nombreux domaines de visibilité, alors c'est un problème, et bientôt nous en parlerons. Mais si vous en avez vraiment besoin, qu'y a-t-il de mal à les passer par des arguments de fonction? Oui, vous devrez les taper au clavier, mais nous sommes des développeurs, c'est notre travail d'écrire du code. »

Cela peut sembler plus compliqué lorsque vous avez plus d'arguments (c'est peut-être le cas), mais je le répète, les avantages l'emportent sur les inconvénients: il est préférable que le code soit aussi clair que possible et n'utilise pas d'états mutables globaux cachés.

Objets de contexte


Les objets contextuels sont ceux qui contiennent des données définies par un certain contexte. En règle générale, ces données sont stockées sous la forme d'une construction de paires de clés, comme un tableau associatif en PHP. Un tel objet n'a aucun comportement, seulement des données, semblable à un objet de valeur .

Un objet de contexte peut remplacer n'importe quel état mutable global. Revenez à l'exemple de code précédent. Au lieu de transmettre des données de la demande à travers les niveaux, nous pouvons utiliser un objet qui encapsule ces données.

Le contexte sera la requête elle-même: une autre requête - un autre contexte - un autre ensemble de données. Ensuite, l'objet contextuel sera transmis à toute méthode qui a besoin de ces données.

Vous dites: "C'est génial et tout ça, mais qu'est-ce que ça donne?"

  • Les données sont encapsulées dans un objet. Le plus souvent, votre tâche consistera à rendre les données immuables, c'est-à-dire afin que vous ne puissiez pas changer l'état - la valeur des données dans l'objet après l'initialisation.
  • Évidemment, le contexte a besoin des données de l'objet contextuel, car il est transféré à toutes les fonctions (ou méthodes) qui ont besoin de ces données.
  • Cela résout le problème de concurrence: si chaque demande a son propre objet contextuel, vous pouvez les écrire ou les lire en toute sécurité dans leurs propres threads d'exécution.

Mais tout dans le développement a un prix. Les objets contextuels peuvent être nuisibles:

  • En regardant les arguments de la fonction, vous ne saurez pas quelles sont les données dans l'objet contextuel.
  • Vous pouvez mettre n'importe quoi dans un objet contextuel. Veillez à ne pas en mettre trop, par exemple, toute la session utilisateur, ou même la plupart de vos données d'application. Et cela peut arriver: $context->getSession()->getUser()->getProfil()->getUsername() . Brisez la loi de Déméter et votre malédiction deviendra une complexité insensée.
  • Plus l'objet contextuel est grand, plus il est difficile de savoir quelles données et dans quelle portée il les utilise.

En général, j'éviterais autant que possible d'utiliser des objets contextuels. Ils peuvent provoquer beaucoup de doutes. L'immuabilité des données est un gros plus, mais il ne faut pas oublier les lacunes. Si vous utilisez un objet contextuel, assurez-vous qu'il est suffisamment petit et passez-le dans une portée petite et soigneusement définie.

Si, avant d'exécuter un programme, vous n'avez aucune idée du nombre d'états qui seront transmis à vos fonctions (par exemple, les paramètres d'une requête HTTP), les objets de contexte peuvent être utiles. Par conséquent, certains d'entre eux les utilisent, rappelez-vous, par exemple, l'objet Request dans Symfony.

Injection de dépendance


Une autre bonne alternative aux états mutables globaux serait d'incorporer directement les données dont vous avez besoin dans l'objet dès que vous le créez. C'est la définition de l'injection de dépendance: un ensemble de techniques pour incorporer des objets dans vos composants (classes).

Pourquoi exactement l'injection de dépendance?


Le but est de limiter l'utilisation de vos variables, objets ou autres constructions et de les placer dans une portée limitée. Si vous avez des dépendances qui sont incorporées et ne peuvent donc agir que dans le cadre d'un objet, il vous sera plus facile de découvrir dans quel contexte elles sont utilisées et pourquoi. Pas d'angoisse ni de tourment!

L'injection de dépendance divise le cycle de vie de l'application en deux phases importantes:

  1. Création d'objets d'application et implémentation de leurs dépendances.
  2. Utilisez des objets pour atteindre vos objectifs.

Cette approche rend le code plus clair; vous n'avez pas besoin d'instancier tout à des endroits aléatoires ou, pire encore, d'utiliser des objets globaux partout.

De nombreux frameworks utilisent l'injection de dépendances, parfois dans des schémas assez complexes, avec des fichiers de configuration et un conteneur d'injection de dépendances (DIC). Mais ce n'est pas du tout nécessaire de compliquer les choses. Vous pouvez simplement créer des dépendances à un niveau et les implémenter à un niveau inférieur. Par exemple, dans le monde de Go, je ne connais personne qui utiliserait DIC. Vous créez simplement les dépendances dans le fichier principal avec le code (main.go), puis les transférez au niveau suivant. Vous pouvez également tout instancier dans différents packages afin d'indiquer clairement que la «phase d'injection de dépendance» ne doit être effectuée qu'à ce niveau spécifique. Dans Go, la portée des packages peut rendre les choses plus faciles qu'en PHP, dans lequel les DIC sont largement utilisés dans tous les frameworks que je connais, y compris Symfony et Laravel.

Implémentation via constructeur ou setters


Il existe deux façons d'injecter des dépendances: via le constructeur ou les setters. Je conseille, si possible, de s'en tenir à la première méthode:

  • Si vous avez besoin de savoir ce que sont les dépendances de classe, il vous suffit de trouver un constructeur. Pas besoin de chercher des méthodes dispersées dans la classe.
  • La définition de dépendances lors de l'installation vous donnera confiance dans la sécurité d'utilisation de l'objet.

Parlons un peu du dernier point: cela s'appelle «forcer l'invariant». En créant une instance d'un objet et en implémentant ses dépendances, vous savez que peu importe ce dont votre objet a besoin, il est configuré correctement. Et si vous utilisez des setters, comment savez-vous que vos dépendances sont déjà définies au moment de l'utilisation de l'objet? Vous pouvez aller sur la pile et essayer de savoir si les setters ont été appelés, mais je suis sûr que vous ne voulez pas faire ça.

Violation d'encapsulation


Après tout, la seule différence entre les États locaux et mondiaux est leur portée. Ils sont limités pour les États locaux et pour le monde entier, l'application entière est disponible. Cependant, vous pouvez rencontrer des problèmes spécifiques aux états globaux si vous utilisez des états locaux. Pourquoi?

Vous avez dit encapsulation?


L'utilisation d'états globaux finira par rompre l'encapsulation, tout comme vous pouvez la rompre avec des états locaux.

Commençons par le début. Que nous dit Wikipedia sur la définition de l'encapsulation? Le mécanisme de langage pour restreindre l'accès direct à certains composants d'un objet. Restriction d'accès? Pourquoi?

Eh bien, comme nous l'avons vu ci-dessus, il est beaucoup plus facile de raisonner sur le plan local que sur le plan mondial. Les états mutables mondiaux sont, par définition, disponibles partout, et c'est contre l'encapsulation! Aucune restriction d'accès pour vous.

Portée croissante et fuite d'état




Imaginons un état dans sa petite portée. Malheureusement, au fil du temps, l'application grandit, cet état local est passé en argument à la fonction tout au long de l'application. Désormais, vos paramètres régionaux sont utilisés dans de nombreuses étendues et, dans tous, l'accès direct aux paramètres régionaux est autorisé. Il est désormais difficile pour vous de calculer l'état exact des paramètres régionaux sans examiner toutes les zones de visibilité dans lesquelles ils existent et où ils peuvent être modifiés. Tout cela, nous l'avons déjà vu avec les États mutables mondiaux.

Prenons un exemple: le modèle de domaine anémique peut augmenter la portée de vos modèles mutables. En fait, le modèle de domaine anémique divise les données et le comportement de vos objets de domaine en deux groupes: les modèles (objets avec données uniquement) et les services (objets uniquement avec comportement). Le plus souvent, ces modèles seront utilisés dans tous les services. Par conséquent, il est probable qu'une sorte de modèle augmentera constamment la portée. Vous ne comprendrez pas quel modèle est utilisé dans quel contexte, leur état changera et tous les mêmes problèmes vous tomberont dessus.

Je veux transmettre une idée importante: si vous évitez les états mutables mondiaux, cela ne signifie pas que vous pouvez vous détendre, tenir un cocktail dans une main et appuyer sur les boutons de l'autre, en profitant de la vie et de votre code légendaire. -, , -, , .

, . , .

? - . , , -.

, - , , . , , — . , : , . . .


. Product , , :

 class Product { public function createProduct(array $productData): Product { $productData["name"] = "SuperProduct".$productData["name"]; // This is not what you should do; I talk about it later in the article. try { $product = $this->productDao->find($productData["id"]); return product; } catch (NotFoundException $e) { $product = $this->productDao->save($productData); return $product; } } } 

$productData . , , , .

, . , - ? , . .

, , . .

:

 class Product { public function createProduct(array $productData): Product { // Since $productData is passed to other variable, it has to be immutable. $name = "SuperProduct".$productData["name"]; try { $product = $this->productDao->find($productData["id"]); return product; } catch (NotFoundException $e) { $product = $this->productDao->save($name, $productData); return $product; } } } 

, , $productData . , . $productData , , HTTP-.

, : «, ».

?




. , .

?

  • , , .
  • , , (, ) .

. , .

, ShipmentDelay , , , . , -, ShipmentDelay , , , . ? , DRY .

, , . : , , . , , , . , , .

?


, (, ), , , . , , , , .

. :

  • .
  • , .
  • — : , , .
  • , .

. , — . , .

, , .

, , . , . , , . , !

Source: https://habr.com/ru/post/fr454946/


All Articles