Code de refactoring Legacy Partie 3 - Conditions conditionnelles complexes

Ancien code. Code laid. Code compliqué. Code spaghetti. Gibberish absurdité. En deux mots, Code hérité. C’est une série qui vous aidera à travailler et à vous en occuper.

J'aime penser au code comme à la prose. Les phrases longues, nichées et composées avec des mots exotiques sont difficiles à comprendre. De temps en temps, vous en avez besoin d'un, mais la plupart du temps, vous pouvez simplement utiliser des mots simples et simples dans des phrases courtes. Ceci est très vrai pour le code source également. Les conditions complexes sont difficiles à comprendre. Les méthodes longues sont comme des phrases sans fin.

De la prose au code

Voici un exemple "prosaïque" pour vous remonter le moral. Tout d'abord, la phrase tout-en-un. Le vilain.

Si la température dans la salle des serveurs est inférieure à cinq degrés et que l'humidité augmente de plus de cinquante pour cent mais reste inférieure à quatre-vingts, et que la pression atmosphérique est stable, le technicien principal John, possédant au moins trois ans d'expérience professionnelle de la gestion de réseau et des serveurs, devrait: il doit se réveiller au milieu de la nuit, s'habiller, sortir, prendre sa voiture ou appeler un taxi s'il n'a pas de voiture, se rendre au bureau, entrer dans le bâtiment, mettre en marche la climatisation et attendre que la température augmente de plus de dix degrés et que l'humidité tombe en dessous de vingt pour cent.

Si vous pouvez comprendre, comprendre et vous rappeler ce paragraphe sans le relire, je vous remettrai une médaille (virtuelle bien sûr). Les longs paragraphes enchevêtrés écrits en une seule phrase compliquée sont difficiles à comprendre. Malheureusement, je ne connais pas assez de mots anglais exotiques pour rendre cela encore plus difficile à comprendre.

Simplification

Trouvons un moyen de le simplifier un peu. Toute sa première partie, jusqu'au "alors" est une condition. Oui, c'est compliqué mais on pourrait le résumer comme ceci: Si les conditions environnementales représentent un risque… … Alors quelque chose devrait être fait. L'expression compliquée dit que nous devrions avertir quelqu'un qui remplit beaucoup de conditions: puis notifiez le support technique de niveau trois. Enfin, tout un processus est décrit depuis le réveil du technicien jusqu’à ce que tout soit réglé: et s'attendre à ce que l'environnement soit restauré dans les paramètres normaux. Mettons tout cela ensemble.

Si les conditions environnementales représentent un risque, informez le support technique de niveau trois et attendez que l'environnement soit restauré dans les paramètres normaux..

Cela représente seulement environ 20% de la longueur par rapport au texte original. Nous ne connaissons pas les détails et dans la grande majorité des cas, nous ne nous en soucions pas. Et ceci est également vrai pour le code source. Combien de fois vous êtes-vous préoccupé des détails de mise en œuvre d'un logInfo ("Un message"); méthode? Probablement une fois, si et quand vous l'avez implémenté. Ensuite, il connecte simplement le message dans la catégorie "info". Ou lorsqu'un utilisateur achète l'un de vos produits, vous souciez-vous de la facturation? Non, tout ce que vous voulez vous soucier est si le produit a été acheté, éliminez-le de l'inventaire et facturez-le à l'acheteur. Les exemples pourraient être sans fin. Ils sont essentiellement comment nous écrivons un logiciel correct.

Conditionnels complexes

Dans cette section, nous allons essayer d'appliquer la philosophie de la prose à notre jeu-questionnaire. Un pas après l'autre. Commençant par les conditions complexes. Commençons par un code simple. Juste pour se réchauffer.

Ligne vingt du GameRunner.php le fichier se lit comme ceci:

if (rand ($ minAnswerId, $ maxAnswerId) == $ falseAnswerId)

Comment cela sonnerait-il en prose? Si un nombre aléatoire entre l'ID de réponse minimum et l'ID de réponse maximum est égal à l'ID de la mauvaise réponse, alors…

Ce n'est pas très compliqué, mais on peut quand même le simplifier. Et ça? Si la mauvaise réponse est sélectionnée, alors… Mieux, n'est ce pas?

La méthode d'extraction Refactoring

Nous avons besoin d'un moyen, d'une procédure, d'une technique pour déplacer cette instruction conditionnelle ailleurs. Cette destination peut facilement être une méthode. Ou dans notre cas, puisque nous ne sommes pas dans une classe ici, une fonction. Ce déplacement de comportement dans une nouvelle méthode ou fonction s'appelle le refactoring "Extract Method". Voici les étapes, telles que définies par Martin Fowler dans son excellent ouvrage intitulé Refactoring: Améliorer la conception du code existant. Si vous n'avez pas lu ce livre, vous devriez le mettre sur votre liste "À lire" maintenant. C'est l'un des livres les plus essentiels pour un programmeur moderne.

Pour notre tutoriel, j’ai pris les étapes originales et les a simplifiées un peu pour mieux répondre à nos besoins et à notre type de tutoriel..

  1. Créez une nouvelle méthode et nommez-la après ce qu'elle fait, pas comment elle le fait.
  2. Copiez le code de l'endroit extrait dans la méthode. S'il vous plaît noter, c'est copie, ne pas encore effacer le code original.
  3. Analyser le code extrait pour toutes les variables qui sont locales. Ils doivent être des paramètres pour la méthode.
  4. Voir si des variables temporaires sont utilisées dans la méthode extraite. Si c'est le cas, déclarez-les à l'intérieur et supprimez le paramètre supplémentaire.
  5. Passer à la méthode cible les variables en tant que paramètres.
  6. Remplacer le code extrait par l'appel de la méthode cible.
  7. Exécutez vos tests.

Maintenant c'est assez compliqué. Cependant, la méthode d'extraction est sans doute la refactorisation la plus utilisée, à l'exception peut-être du changement de nom. Donc, vous devez comprendre ses mécanismes.

Heureusement pour nous, les IDE modernes tels que PHPStorm fournissent d'excellents outils de refactoring, comme nous l'avons vu dans le tutoriel PHPStorm: Quand l'IDE compte vraiment. Nous allons donc utiliser les fonctionnalités dont nous disposons, au lieu de tout faire à la main. C'est moins sujet aux erreurs et beaucoup, beaucoup plus rapide.

Il suffit de sélectionner la partie souhaitée du code et clic-droit il.

L'EDI comprendra automatiquement qu'il nous faut trois paramètres pour exécuter notre code et proposera la solution suivante.

//… // $ minAnswerId = 0; $ maxAnswerId = 9; $ falseAnswerId = 7; function isCurrentAnswerWrong ($ minAnswerId, $ maxAnswerId, $ falseAnswerId) return rand ($ minAnswerId, $ maxAnswerId) == $ falseAnswerId;  faire $ dés = rand (0, 5) + 1; $ aGame-> roll ($ dés); if (isCurrentAnswerWrong ($ minAnswerId, $ maxAnswerId, $ falseAnswerId)) $ notAWinner = $ aGame-> wrongAnswer ();  else $ notAWinner = $ aGame-> wasCorrectlyAnswered ();  tant que ($ notAWinner);

Bien que ce code soit syntaxiquement correct, il rompra nos tests. Parmi tous les bruits qui nous sont présentés en couleurs rouge, bleue et noire, nous pouvons en comprendre la raison:

Erreur fatale: Impossible de redéclarer isCurrentAnswerWrong () (précédemment déclaré dans / home / csaba / Personnel / Programmation / NetTuts / Code de refactoring ancien - Partie 3: méthodes complexes et méthodes longues /Source/trivia/php/GameRunner.php:16) in / home / csaba / Personnel / Programmation / NetTuts / Code ancien de refactoring - Partie 3: Conditions complexes et méthodes longues /Source/trivia/php/GameRunner.php on line 18

Il dit essentiellement que nous voulons déclarer la fonction deux fois. Mais comment cela peut-il arriver? Nous ne l'avons qu'une fois dans notre GameRunner.php!

Regardez les tests. Il y a un generateOutput () méthode qui fait un exiger() sur notre GameRunner.php. On l'appelle au moins deux fois. Voici la source de l'erreur.

Nous avons maintenant un dilemme. En raison de l'ensemencement du générateur aléatoire, nous devons appeler ce code avec des valeurs contrôlées.

fonction privée generateOutput ($ seed) ob_start (); srand ($ graine); nécessite __DIR__. '/… /Trivia/php/GameRunner.php'; $ output = ob_get_contents (); ob_end_clean (); return $ output; 

Mais il n’ya aucun moyen de déclarer une fonction deux fois en PHP, nous avons donc besoin d’une autre solution. Nous commençons à ressentir le fardeau de nos tests Golden Master. Lancer 20000 fois, chaque fois que nous modifions un élément de code, peut ne pas être une solution à long terme. Outre le fait qu'il faut du temps pour fonctionner, cela nous oblige à modifier notre code pour l'adapter à la façon dont nous le testons. Ceci est généralement un signe de mauvais tests. Le code devrait changer et continuer à faire passer le test, mais les modifications devraient avoir des raisons de changer, venant uniquement du code source.

Mais assez parlé, nous avons besoin d’une solution, même temporaire, pour le moment. La migration vers les tests unitaires commencera par notre prochaine leçon.

Une façon de résoudre notre problème est de prendre tout le reste du code GameRunner.php et le mettre dans une fonction. Disons courir()

include_once __DIR__. '/Game.php'; function isCurrentAnswerWrong ($ minAnswerId, $ maxAnswerId, $ falseAnswerId) return rand ($ minAnswerId, $ maxAnswerId) == $ falseAnswerId;  function run () $ notAWinner; $ aGame = new Game (); $ aGame-> add ("Chet"); $ aGame-> add ("Pat"); $ aGame-> add ("Sue"); $ minAnswerId = 0; $ maxAnswerId = 9; $ falseAnswerId = 7; est-ce que $ dés = rand (0, 5) + 1; $ aGame-> roll ($ dés); if (isCurrentAnswerWrong ($ minAnswerId, $ maxAnswerId, $ falseAnswerId)) $ notAWinner = $ aGame-> wrongAnswer ();  else $ notAWinner = $ aGame-> wasCorrectlyAnswered ();  tant que ($ notAWinner); 

Cela nous permettra de le tester, mais sachez que l’exécution du code à partir de la console n’exécutera pas le jeu. Nous avons légèrement changé de comportement. Nous avons gagné en testabilité au prix d'un changement de comportement, ce que nous ne voulions pas faire en premier lieu. Si vous voulez exécuter le code à partir de la console, vous aurez maintenant besoin d'un autre fichier PHP incluant ou nécessitant le coureur, puis appelant explicitement la méthode run dessus. Pas un si grand changement, mais une nécessité à retenir, surtout si des tiers utilisent votre code existant.

D'autre part, nous pouvons maintenant simplement inclure le fichier dans notre test.

nécessite __DIR__. '/… /Trivia/php/GameRunner.php';

Et puis appelez courir() à l'intérieur de la méthode generateOutput ().

fonction privée generateOutput ($ seed) ob_start (); srand ($ graine); courir(); $ output = ob_get_contents (); ob_end_clean (); return $ output; 

Structure du répertoire, fichiers et nommage

C’est peut-être une bonne occasion de réfléchir à la structure de nos répertoires et fichiers. Il n'y a plus de conditionnelles complexes dans notre GameRunner.php, mais avant de continuer à la Game.php fichier, nous ne devons pas laisser un désordre derrière nous. Notre GameRunner.php n’exécute plus rien, et nous devions pirater des méthodes pour la rendre testable, ce qui a cassé notre interface publique. La raison en est que nous testons peut-être la mauvaise chose.

Nos appels de test courir() dans le GameRunner.php fichier, qui comprend Game.php, joue le jeu et un nouveau fichier maître d’or est généré. Et si on introduisait un autre fichier? Nous faisons le GameRunner.php pour lancer le jeu en appelant courir() et rien d'autre. Alors que se passe-t-il s'il n'y a aucune logique là-dedans qui pourrait mal tourner et qu'aucun test n'est nécessaire, puis nous transférons notre code actuel dans un autre fichier?

Maintenant, c'est une toute autre histoire. Maintenant, nos tests accèdent au code juste sous le coureur. En gros, nos tests ne sont que des coureurs. Et bien sûr dans notre nouvelle GameRunner.php il n'y aura qu'un appel pour lancer le jeu. Ceci est un vrai coureur, il ne fait rien d'autre que d'appeler le courir() méthode. Aucune logique signifie qu'aucun test n'est nécessaire.

require_once __DIR__. '/RunnerFunctions.php'; courir();

Nous pourrions nous poser d’autres questions à ce stade. Avons-nous vraiment besoin d'un RunnerFunctions.php? Ne pourrions-nous pas simplement prendre les fonctions à partir de là et les déplacer vers Game.php? Nous pourrions probablement, mais avec notre compréhension actuelle de, quelle fonction appartient où? N'est pas assez. Nous allons trouver une place pour notre méthode dans une prochaine leçon.

Nous avons également essayé de nommer nos fichiers en fonction de leur code. L'une d'elles n'est qu'un groupe de fonctions pour le coureur, fonctions que nous considérons ici comme appartenant à l'ensemble, afin de satisfaire les besoins du coureur. Cela deviendra-t-il une classe à un moment donné dans le futur? Peut être. Peut être pas. Pour l'instant, c'est assez bon.

Nettoyage des fonctions du coureur

Si on regarde le RunnerFunctions.php fichier, il y a un peu de gâchis que nous avons introduit.

Nous définissons:

$ minAnswerId = 0; $ maxAnswerId = 9; $ falseAnswerId = 7;

… à l'intérieur de courir() méthode. Ils ont une seule raison d'exister et un seul endroit où ils sont utilisés. Pourquoi ne pas simplement les définir dans cette méthode et se débarrasser des paramètres?

function isCurrentAnswerWrong () $ minAnswerId = 0; $ maxAnswerId = 9; $ falseAnswerId = 7; return rand ($ minAnswerId, $ maxAnswerId) == $ falseAnswerId; 

Ok, les tests passent et le code est bien meilleur. Mais pas assez bon.

Conditions négatives

Il est beaucoup plus facile, pour l’esprit humain, de comprendre le raisonnement positif. Donc, si vous pouvez éviter les conditions négatives, vous devriez toujours suivre cette voie. Dans notre exemple actuel, la méthode recherche une réponse incorrecte. Il serait beaucoup plus facile de comprendre une méthode qui vérifie la validité et nie que, au besoin.

function isCurrentAnswerCorrect () $ minAnswerId = 0; $ maxAnswerId = 9; $ falseAnswerId = 7; return rand ($ minAnswerId, $ maxAnswerId)! = $ falseAnswerId; 

Nous avons utilisé le refactoring de la méthode Rename. C’est encore une fois, assez compliqué s’il est utilisé à la main, mais dans tout IDE, c’est aussi simple que de frapper CTRL + r, ou en sélectionnant l'option appropriée dans le menu. Pour réussir nos tests, nous devons également mettre à jour notre déclaration conditionnelle avec une négation..

if (! isCurrentAnswerCorrect ()) $ notAWinner = $ aGame-> wrongAnswer ();  else $ notAWinner = $ aGame-> wasCorrectlyAnswered (); 

Cela nous rapproche un peu plus de notre compréhension du conditionnel. En utilisant ! dans un si() déclaration, aide réellement. Il se démarque et met en évidence le fait que quelque chose y est effectivement nié. Mais pouvons-nous inverser cette règle afin d'éviter complètement la négation? Oui nous pouvons.

if (isCurrentAnswerCorrect ()) $ notAWinner = $ aGame-> wasCorrectlyAnswered ();  else $ notAWinner = $ aGame-> wrongAnswer (); 

Maintenant, nous n’avons aucune négation logique en utilisant !, ni négation lexicale en nommant et en retournant les mauvaises choses. Toutes ces étapes ont rendu notre condition beaucoup, beaucoup plus facile à comprendre.

Conditionals dans Game.php

Nous avons simplifié à l'extrême, RunnerFunctions.php. Attaquons notre Game.php déposer maintenant. Vous pouvez rechercher des conditions de plusieurs manières. Si vous préférez, vous pouvez simplement scanner le code en le regardant simplement. Ceci est plus lent, mais a la valeur ajoutée de vous forcer à essayer de le comprendre séquentiellement.

Le deuxième moyen évident de rechercher des conditions est de rechercher simplement "if" ou "if (". Si vous avez formaté votre code avec les fonctionnalités intégrées de votre environnement de développement, vous pouvez être sûr que toutes les instructions conditionnelles ont la même forme spécifique. Dans mon cas, il y a un espace entre le "si" et la parenthèse. En outre, si vous utilisez la recherche intégrée, les résultats trouvés seront mis en surbrillance dans une couleur strident, dans mon cas jaune.

Maintenant que nous avons tous éclairé notre code comme un arbre de Noël, nous pouvons les prendre un à un. Nous connaissons la perceuse, nous connaissons les techniques que nous pouvons utiliser, il est temps de les appliquer.

if ($ this-> inPenaltyBox [$ this-> currentPlayer])

Cela semble assez raisonnable. Nous pourrions l'extraire dans une méthode, mais y aurait-il un nom pour cette méthode afin de clarifier la condition?

si ($ roll% 2! = 0) 

Je parie que 90% de tous les programmeurs peuvent comprendre le problème ci-dessus si déclaration. Nous essayons de nous concentrer sur ce que notre méthode actuelle fait. Et notre cerveau est connecté au domaine du problème. Nous ne voulons pas "démarrer un autre thread" pour calculer cette expression mathématique afin de comprendre qu'elle vérifie simplement si un nombre est impair. C'est l'une de ces petites distractions qui peuvent ruiner une difficile déduction logique. Alors je dis, extrayons-le.

si ($ this-> isOdd ($ roll))

C’est mieux parce qu’il s’agit du domaine du problème et qu’il ne nécessite aucun pouvoir cérébral supplémentaire.

if ($ this-> places [$ this-> currentPlayer]> $ lastPositionOnTheBoard)

Cela semble être un autre bon candidat. Ce n’est pas si difficile à comprendre en tant qu’expression mathématique, mais encore une fois, c’est une expression qui nécessite un traitement parallèle. Je me demande ce que cela signifie si la position du joueur actuel a atteint la fin du tableau. Ne pouvons-nous pas exprimer cet état de manière plus concise? Nous pouvons probablement.

if ($ this-> playerReachedEndOfBoard ($ lastPositionOnTheBoard))

C'est mieux. Mais que se passe-t-il réellement à l'intérieur du si? Le joueur est repositionné au début du tableau. Le joueur commence un nouveau "tour" dans la course. Et si à l'avenir, nous aurions une raison différente de commencer un nouveau tour? Si notre si changement d'instruction lorsque nous changeons la logique sous-jacente dans la méthode privée? Absolument pas! Alors, renommons cette méthode en ce que le si représente, dans ce qui se passe, pas ce que nous vérifions.

if ($ this-> playerShouldStartANewLap ($ lastPositionOnTheBoard))

Lorsque vous essayez de nommer des méthodes et des variables, réfléchissez toujours à ce que le code doit faire et non à l’état ou à la condition qu’il représente. Une fois que vous avez compris, les actions de changement de nom dans votre code seront considérablement réduites. Néanmoins, même un programmeur expérimenté doit renommer une méthode au moins trois à cinq fois avant de trouver son nom correct. Alors n'ayez pas peur de frapper CTRL + r et renommer fréquemment. N'engagez jamais vos modifications dans le système de contrôle de version du projet si vous n'avez pas analysé les noms des méthodes que vous venez d'ajouter et si votre code ne se lit pas comme une prose bien écrite. Renommer est tellement bon marché de nos jours, que vous pouvez renommer des choses simplement pour essayer différentes versions et revenir en arrière en un seul clic..

le si déclaration à la ligne 90 est la même que notre précédente. Nous pouvons simplement réutiliser notre méthode extraite. Voilà, la duplication éliminée! Et n'oubliez pas de lancer vos tests de temps en temps, même lorsque vous refactorisez en utilisant la magie de votre IDE. Ce qui nous amène à notre prochaine observation. La magie, parfois, échoue. Départ ligne 65.

$ lastPositionOnTheBoard = 11;

Nous déclarons une variable et ne l'utilisons qu'à un seul endroit, en tant que paramètre de notre méthode nouvellement extraite. Ceci suggère fortement que la variable devrait être à l'intérieur de la méthode.

fonction privée playerShouldStartANewLap () $ lastPositionOnTheBoard = 11; return $ this-> places [$ this-> currentPlayer]> $ lastPositionOnTheBoard; 

Et n'oubliez pas d'appeler la méthode sans aucun paramètre dans votre si des déclarations.

si ($ this-> playerShouldStartANewLap ())

le si déclarations dans le poser une question() la méthode semblent bien, ainsi que celles de currentCategory ().

if ($ this-> inPenaltyBox [$ this-> currentPlayer])

C'est un peu plus compliqué, mais dans le domaine et assez expressif.

if ($ this-> currentPlayer == count ($ this-> players))

Nous pouvons travailler sur celui-ci. Il est évident que la comparaison signifie que le joueur actuel est hors limites. Mais comme nous l'avons appris ci-dessus, nous voulons que l'intention ne soit pas énoncée.

if ($ this-> shouldResetCurrentPlayer ())

C’est bien mieux, et nous allons le réutiliser aux lignes 172, 189 et 203. La duplication, c’est-à-dire la duplication, c’est la quadruplication, éliminée!

Les tests passent et tout si les déclarations ont été évaluées pour la complexité.

Dernières pensées

Plusieurs leçons peuvent être tirées de la refonte des conditions. Tout d’abord, ils aident à mieux comprendre l’intention du code. Ensuite, si vous nommez la méthode extraite pour représenter l'intention correctement, vous éviterez les changements de nom futurs. Trouver des doublons en logique est plus difficile que de trouver des lignes dupliquées de code simple. Vous avez peut-être pensé que nous devrions faire une duplication consciente, mais je préfère traiter de la duplication lorsque j'ai des tests unitaires avec lesquels je peux faire confiance à ma vie. Le Golden Master est bon, mais c'est tout au plus un filet de sécurité, pas un parachute.

Merci d'avoir lu et restez à l'écoute pour notre prochain tutoriel lorsque nous présenterons nos premiers tests unitaires.