Code de l'héritage de refactoring Partie 1 - Le maître d'or

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

Dans un monde idéal, vous n’écririez que du nouveau code. Vous écririez beau et parfait. Vous n'auriez jamais à revoir votre code et vous ne devrez jamais maintenir vos projets âgés de dix ans. Dans un monde idéal…

Malheureusement, nous vivons dans une réalité qui n’est pas idéale. Nous devons comprendre, modifier et améliorer un code séculaire. Nous devons travailler avec le code hérité. Alors qu'est-ce que tu attends? Entrons dans ce premier tutoriel, récupérons le code, comprenons-le un peu et créons un filet de sécurité pour nos futures modifications.

Définition du code hérité

Le code hérité a été défini de tant de manières qu'il est impossible de trouver une définition unique, communément acceptée. Les quelques exemples présentés au début de ce didacticiel ne sont que la partie visible de l'iceberg. Je ne vous donnerai donc aucune définition officielle. Au lieu de cela, je vais vous citer mon préféré.

Pour moi, code hérité est simplement du code sans tests. ~ Michael Feathers

Eh bien, c'est la première définition formelle de l'expression code hérité, publié par Michael Feathers dans son livre Working Effectiveively with Legacy Code. Bien entendu, l’industrie a utilisé l’expression pendant des années, essentiellement pour tout code difficile à modifier. Cependant, cette définition a quelque chose de différent à dire. Cela explique très clairement le problème, de sorte que la solution devient évidente. "Difficile de changer" est si vague. Que devrions-nous faire pour faciliter les changements? Nous n'avons aucune idée! "Code sans tests" est en revanche très concret. Et la réponse à notre question précédente est simple: testez le code et testez-le. Alors, commençons.

Obtenir notre code d'héritage

Cette série sera basée sur l'exceptionnel jeu de questions par J.B. Rainsberger conçu pour les événements Legacy Code Retreat. Il est conçu pour ressembler à un véritable code hérité et offre également la possibilité d'une grande variété de refactoring, à un niveau de difficulté décent..

Vérifiez le code source

Le jeu-questionnaire est hébergé sur GitHub et est sous licence GPLv3, vous pouvez donc jouer librement avec. Nous allons commencer cette série en consultant le référentiel officiel. Le code est également attaché à ce didacticiel avec toutes les modifications que nous allons apporter. Par conséquent, si vous vous trompez, vous pourrez jeter un coup d'œil au résultat final..

 $ git clone https://github.com/jbrains/trivia.git Clonage dans 'trivia'… remote: Comptage d'objets: 429, terminé. remote: Compression d'objets: 100% (262/262), terminé. à distance: Total 429 (delta 100), réutilisé 419 (delta 93) Réception d'objets: 100% (429/429), 848,33 Ko | 305.00 KiB / s, fait. Résolution des deltas: 100% (100/100), terminé. Vérification de la connectivité… terminée.

Lorsque vous ouvrez le trivia répertoire, vous verrez notre code dans plusieurs langages de programmation. Nous allons travailler en PHP, mais vous êtes libre de choisir votre logiciel préféré et d'appliquer les techniques présentées ici..

Comprendre le code

Par définition, le code hérité est difficile à comprendre, surtout si nous ne savons même pas ce qu’il est censé faire. Donc, la première étape consiste à exécuter le code et à faire une sorte de raisonnement, de quoi il s'agit.

Nous avons deux fichiers dans notre répertoire.

$ cd php / $ ls -al total 20 drwxr-xr-x 2 csaba csaba 4096 mars 10 21:05. drwxr-xr-x 26 csaba csaba 4096 10 mars 21h05… -rw-r - r-- 1 csaba csaba 5568 mars 10 21:05 Game.php -rw-r - r-- 1 csaba csaba 410 mars 10 21:05 GameRunner.php

GameRunner.php semble être un bon candidat pour notre tentative d'exécuter le code.

$ php ./GameRunner.php Chet a été ajouté Ils sont joueur n ° 1 Pat a été ajouté Ils sont joueur n ° 2 Sue a été ajouté Ils sont joueur n ° 3 Chet est le joueur actuel Ils ont obtenu un 4 Le nouvel emplacement de Chet est 4 La catégorie est Pop Pop Question 0 Answer was corrent !!!! Chet a maintenant 1 pièces d'or. Pat est le joueur actuel. Le résultat est 2 La nouvelle catégorie est Sports La question 0 La réponse était corrent !!!! Pat a maintenant 1 pièce d'or. Sue est le joueur actuel. Ils ont obtenu un 1 Le nouvel emplacement de Sue est 1 La catégorie est Science Science Question 0 La réponse était corrent !!!! Sue a maintenant 1 pièce d'or. Chet est le joueur actuel. Ils ont obtenu un 4 ## Quelques lignes supprimées pour garder ## le tutoriel à une taille raisonnable. La réponse était correcte !!!! Sue a maintenant 5 pièces d'or. Chet est le joueur actuel. Ils ont obtenu un 3 Chet est en train de sortir de la surface de réparation Le nouvel emplacement de Chet est 11 La catégorie est Rock Rock Question 5 La réponse était correcte !!!! Chet a maintenant 5 pièces d'or. Pat est le joueur actuel. Ils ont obtenu un 1 Le nouvel emplacement de Pat est 10 La catégorie est Sports Sports Question 1 La réponse était correcte !!!! Pat a maintenant 6 pièces d'or.

D'ACCORD. Notre estimation était correcte. Notre code a fonctionné et produit une sortie. L’analyse de cette sortie nous aidera à déduire une idée de base de ce que le code fait..

  1. Nous savons que c'est un jeu-questionnaire. Nous le savions quand nous avons vérifié le code source.
  2. Notre exemple a trois joueurs: Chet, Pat et Sue.
  3. Il y a une sorte de roulement de dés ou un concept similaire.
  4. Il y a un emplacement actuel pour un joueur. Peut-être sur une sorte de tableau?
  5. Les questions sont posées dans différentes catégories.
  6. Les utilisateurs répondent aux questions.
  7. Les réponses correctes donnent aux joueurs de l'or.
  8. Les mauvaises réponses envoient les joueurs à la surface de réparation.
  9. Les joueurs peuvent sortir de la surface de réparation en se basant sur une logique pas très claire.
  10. Il semble que l'utilisateur qui atteint d'abord six pièces d'or gagne.

Maintenant, c'est beaucoup de connaissances. Nous pourrions comprendre l'essentiel du comportement de base de l'application simplement en regardant la sortie. Dans les applications réelles, la sortie peut ne pas être du texte à l'écran, mais il peut s'agir d'une page Web, d'un journal d'erreurs, d'une base de données, d'une communication réseau, d'un fichier de vidage, etc. Dans d'autres cas, le module que vous devez modifier ne peut pas être exécuté de manière isolée. Si tel est le cas, vous devrez l'exécuter via d'autres modules de la plus grande application. Essayez simplement d’ajouter le minimum pour obtenir une sortie raisonnable de votre code hérité.

Scanner le code

Maintenant que nous avons une idée de ce que le code génère, nous pouvons commencer à l'examiner. Nous allons commencer avec le coureur.

Le coureur de jeu

J'aime commencer par exécuter tout le code à travers le formateur de mon IDE. Cela améliore grandement la lisibilité en rendant le formulaire du code familier avec ce à quoi je suis habitué. Donc ça:

… Deviendra ceci:

… Qui est un peu mieux. Ce n'est peut-être pas une énorme différence avec cette petite quantité de code, mais ce sera dans notre prochain fichier..

En regardant notre GameRunner.php fichier, nous pouvons facilement identifier certains aspects clés observés dans la sortie. Nous pouvons voir les lignes qui ajoutent les utilisateurs (9-11), qu’une méthode roll () est appelée et un gagnant est sélectionné. Bien sûr, ceux-ci sont loin des secrets de la logique du jeu, mais au moins, nous pourrions commencer par identifier des méthodes clés qui nous aideront à découvrir le reste du code..

Le fichier de jeu

Nous devrions faire la même mise en forme sur le Game.php déposer aussi.

Ce fichier est beaucoup plus volumineux. Environ 200 lignes de code. La plupart des méthodes sont correctement dimensionnées, mais certaines d'entre elles sont assez volumineuses et, après le formatage, nous pouvons constater qu'à deux endroits, l'indentation du code dépasse quatre niveaux. Des niveaux élevés d'indentation impliquent généralement de nombreuses décisions complexes. Pour le moment, nous pouvons donc supposer que ces points de notre code seront plus complexes et plus sensibles au changement..

Le maître d'or

Et la pensée du changement nous conduit à notre manque de tests. Les méthodes que nous avons vu dans Game.php sont assez complexes. Ne vous inquiétez pas si vous ne les comprenez pas. À ce stade, ils sont aussi un mystère pour moi. Le code hérité est un mystère que nous devons résoudre et comprendre. Nous avons fait notre premier pas pour le comprendre et le moment est venu pour notre deuxième.

Alors, quel est ce maître d'or?

Lorsque vous travaillez avec du code hérité, il est presque impossible de le comprendre et d'écrire du code qui exercera sûrement tous les chemins logiques à travers le code. Pour ce type de test, nous aurions besoin de comprendre le code, mais ce n'est pas encore le cas. Nous devons donc adopter une autre approche.

Plutôt que d'essayer de trouver ce qu'il faut tester, nous pouvons tout tester, souvent, pour obtenir une quantité énorme de résultats, sur lesquels nous pouvons presque certainement supposer que cela a été produit en exerçant toutes les composantes de notre héritage. code. Il est recommandé d'exécuter le code au moins 10 000 (dix mille) fois. Nous allons écrire un test pour l'exécuter deux fois plus et sauvegarder la sortie.

Écrire le Golden Master Generator

Nous pouvons penser à l’avenir et commencer par créer un générateur et un test sous la forme de fichiers distincts pour les tests ultérieurs, mais est-ce vraiment nécessaire? Nous ne le savons pas encore avec certitude. Alors pourquoi ne pas commencer avec un fichier de test de base qui exécutera notre code une fois et construira notre logique à partir de là.

Vous trouverez dans l'archive de code ci-jointe, à l'intérieur du la source dossier mais en dehors du trivia dossier notre Tester dossier. Dans ce dossier, nous créons un fichier: GoldenMasterTest.php.

La classe GoldenMasterTest étend PHPUnit_Framework_TestCase function testGenerateOutput () ob_start (); require_once __DIR__. '/… /Trivia/php/GameRunner.php'; $ output = ob_get_contents (); ob_end_clean (); var_dump ($ output); 

Nous pourrions faire cela de plusieurs façons. Nous pourrions, par exemple, exécuter notre code à partir de la console et rediriger sa sortie vers un fichier. Cependant, le faire dans un test qui est facilement exécuté dans notre IDE est un avantage que nous ne devrions pas ignorer..

Le code est assez simple, il tampon la sortie et le met dans le $ sortie variable. le Demandez une fois() sera également exécuter tout le code à l'intérieur du fichier inclus. Dans notre var dump, nous verrons des sorties déjà familières.

Cependant, lors d'une deuxième manche, nous pouvons observer quelque chose d'étrange:

… Les sorties diffèrent. Même si nous avons exécuté le même code, la sortie est différente. Les numéros obtenus sont différents, les positions des joueurs sont différentes.

Semer le générateur aléatoire

faire $ aGame-> roll (rand (0, 5) + 1); if (rand (0, 9) == 7) $ notAWinner = $ aGame-> wrongAnswer ();  else $ notAWinner = $ aGame-> wasCorrectlyAnswered ();  tant que ($ notAWinner);

En analysant le code essentiel du coureur, nous pouvons voir qu’il utilise la fonction rand() générer des nombres aléatoires. Notre prochain arrêt est la documentation officielle de PHP pour rechercher cette rand() une fonction.

Le générateur de nombres aléatoires est ensemencé automatiquement.

La documentation nous dit que l'ensemencement se fait automatiquement. Nous avons maintenant une autre tâche. Nous devons trouver un moyen de contrôler la graine. le srand () fonction peut aider avec ça. Voici sa définition de la documentation.

Graine le générateur de nombre aléatoire avec graine ou avec une valeur aléatoire si aucune graine n'est donnée.

Il nous dit que si nous courons cela avant tout appel à rand(), nous devrions toujours nous retrouver avec les mêmes résultats.

fonction testGenerateOutput () ob_start (); srand (1); require_once __DIR__. '/… /Trivia/php/GameRunner.php'; $ output = ob_get_contents (); ob_end_clean (); var_dump ($ output); 

nous mettons srand (1) avant notre Demandez une fois(). Maintenant, la sortie est toujours la même.

Mettre la sortie dans un fichier

La classe GoldenMasterTest étend PHPUnit_Framework_TestCase function testGenerateOutput () file_put_contents ('/ tmp / gm.txt', $ this-> generateOutput ()); $ file_content = file_get_contents ('/ tmp / gm.txt'); $ this-> assertEquals ($ fichier_content, $ this-> generateOutput ());  fonction privée generateOutput () ob_start (); srand (1); require_once __DIR__. '/… /Trivia/php/GameRunner.php'; $ output = ob_get_contents (); ob_end_clean (); return $ output; 

Ce changement semble raisonnable. Droite? Nous avons extrait la génération de code dans une méthode, l'avons exécutée deux fois et nous nous attendions à ce que le résultat soit égal. Cependant, ils ne seront pas.

La raison en est que Demandez une fois() ne nécessitera pas le même fichier deux fois. Le deuxième appel à la generateOutput () La méthode produira une chaîne vide. Alors, que pourrions-nous faire? Et si nous simplement exiger()? Cela devrait être exécuté à chaque fois.

Eh bien, cela conduit à un autre problème: "Impossible de redéclarer echoln ()". Mais d'où vient-il? C'est juste au début de la Game.php fichier. La raison pour laquelle cette erreur se produit est parce que dans GameRunner.php on a inclure __DIR__. '/Game.php';, qui essaie d'inclure le fichier de jeu deux fois, chaque fois que nous appelons le generateOutput () méthode.

include_once __DIR__. '/Game.php';

En utilisant include_once dans GameRunner.php va résoudre notre problème. Oui, nous devions modifier GameRunner.php sans avoir des tests pour le moment! Cependant, nous pouvons être sûrs à 99% que notre changement ne cassera pas le code lui-même. C'est un changement petit et assez simple pour ne pas nous effrayer beaucoup. Et surtout, il fait passer les tests.

Run It plusieurs fois

Maintenant que nous avons du code, nous pouvons exécuter plusieurs fois, il est temps de générer une sortie.

function testGenerateOutput () $ this-> generateMany (20, '/tmp/gm.txt'); $ this-> generateMany (20, '/tmp/gm2.txt'); $ file_content_gm = file_get_contents ('/ tmp / gm.txt'); $ file_content_gm2 = file_get_contents ('/ tmp / gm2.txt'); $ this-> assertEquals ($ file_content_gm, $ file_content_gm2);  fonction privée generateMany ($ times, $ fileName) $ first = true; while ($ times) if ($ first) fichier_put_contents ($ fileName, $ this-> generateOutput ()); $ premier = faux;  else contenu_fichier_fichier ($ nomfichier, $ ceci-> generateOutput (), FILE_APPEND);  $ times--; 

Nous avons extrait une autre méthode ici: generateMany (). Il a deux paramètres. Une pour le nombre de fois où nous voulons utiliser notre générateur, l'autre est un fichier de destination. Il mettra la sortie générée dans les fichiers. À la première exécution, il vide les fichiers et, pour le reste des itérations, ajoute les données. Vous pouvez regarder dans le fichier pour voir la sortie générée 20 fois.

Mais attendez! Le même joueur gagne à chaque fois? Est-ce possible?

cat /tmp/gm.txt | grep "a 6 pièces d'or." Chet a maintenant 6 pièces d'or. Chet a maintenant 6 pièces d'or. Chet a maintenant 6 pièces d'or. Chet a maintenant 6 pièces d'or. Chet a maintenant 6 pièces d'or. Chet a maintenant 6 pièces d'or. Chet a maintenant 6 pièces d'or. Chet a maintenant 6 pièces d'or. Chet a maintenant 6 pièces d'or. Chet a maintenant 6 pièces d'or. Chet a maintenant 6 pièces d'or. Chet a maintenant 6 pièces d'or. Chet a maintenant 6 pièces d'or. Chet a maintenant 6 pièces d'or. Chet a maintenant 6 pièces d'or. Chet a maintenant 6 pièces d'or. Chet a maintenant 6 pièces d'or. Chet a maintenant 6 pièces d'or. Chet a maintenant 6 pièces d'or. Chet a maintenant 6 pièces d'or.

Oui! C'est possible! C'est plus que possible. C'est une chose sûre. Nous avons la même graine pour notre fonction aléatoire. Nous jouons le même jeu encore et encore.

Exécuter différemment à chaque fois

Nous devons jouer à différents jeux, sinon il est presque certain que seule une petite partie de notre code hérité est exercée à maintes reprises. L'objectif du maître d'or est d'exercer autant que possible. Nous devons re-semer le générateur aléatoire à chaque fois, mais de manière contrôlée. Une option consiste à utiliser notre compteur comme valeur de départ.

Fonction privée generateMany ($ times, $ fileName) $ first = true; while ($ times) if ($ first) file_put_contents ($ fileName, $ this-> generateOutput ($ times)); $ premier = faux;  else contenu_fichier_fichier ($ nomFichier, $ ceci-> generateOutput ($ fois), FILE_APPEND);  $ times--;  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; 

Notre test continue de passer, nous sommes donc sûrs de générer la même sortie complète à chaque fois, tandis que la sortie joue un jeu différent à chaque itération..

cat /tmp/gm.txt | grep "a 6 pièces d'or." Sue a maintenant 6 pièces d'or. Chet a maintenant 6 pièces d'or. Chet a maintenant 6 pièces d'or. Chet a maintenant 6 pièces d'or. Chet a maintenant 6 pièces d'or. Pat a maintenant 6 pièces d'or. Pat a maintenant 6 pièces d'or. Chet a maintenant 6 pièces d'or. Chet a maintenant 6 pièces d'or. Sue a maintenant 6 pièces d'or. Chet a maintenant 6 pièces d'or. Chet a maintenant 6 pièces d'or. Sue a maintenant 6 pièces d'or. Chet a maintenant 6 pièces d'or. Sue a maintenant 6 pièces d'or. Chet a maintenant 6 pièces d'or. Chet a maintenant 6 pièces d'or. Pat a maintenant 6 pièces d'or. Chet a maintenant 6 pièces d'or. Chet a maintenant 6 pièces d'or.

Il y a divers gagnants pour le jeu de manière aléatoire. Cela semble bon.

Arriver à 20 000

La première chose que vous pouvez essayer est d’exécuter notre code pour 20 000 itérations de jeu..

fonction testGenerateOutput () $ times = 20000; $ this-> generateMany ($ times, '/tmp/gm.txt'); $ this-> generateMany ($ times, '/tmp/gm2.txt'); $ file_content_gm = file_get_contents ('/ tmp / gm.txt'); $ file_content_gm2 = file_get_contents ('/ tmp / gm2.txt'); $ this-> assertEquals ($ file_content_gm, $ file_content_gm2); 

Cela fonctionnera presque. Deux fichiers de 55Mo seront générés.

ls-alh / tmp / gm * -rw-r - r-- 1 csaba csaba 55 mars 14 20:38 /tmp/gm2.txt -rw-r - r-- 1 csaba csaba 55 mars 14 20:38 /tmp/gm.txt

D'autre part, le test échouera avec une erreur de mémoire insuffisante. Peu importe combien de RAM vous avez, cela échouera. J'ai 8 Go plus un échange de 4 Go et cela échoue. Les deux chaînes sont simplement trop grandes pour être comparées dans notre affirmation.

En d’autres termes, nous générons de bons fichiers, mais PHPUnit ne peut pas les comparer. Nous avons besoin d'un work-around.

$ this-> assertFileEquals ('/ tmp / gm.txt', '/tmp/gm2.txt');

Cela semble être un bon candidat, mais cela échoue toujours. C'est dommage. Nous devons approfondir la recherche sur la situation.

$ this-> assertTrue ($ file_content_gm == $ file_content_gm2);

Cela fonctionne cependant.

Il peut comparer les deux chaînes et échouer si elles sont différentes. Il a cependant un petit prix. Il ne sera pas en mesure de dire exactement ce qui ne va pas lorsque les chaînes diffèrent. Il va simplement dire "Échec de l'affirmation que false est vrai.". Mais nous en traiterons dans un prochain tutoriel.

Dernières pensées

Nous avons terminé pour ce tutoriel. Nous avons beaucoup appris pour notre première leçon et nous sommes bien partis pour nos travaux futurs. Nous avons rencontré le code, nous l'avons analysé de différentes manières et nous avons surtout compris sa logique essentielle. Ensuite, nous avons créé un ensemble de tests pour nous assurer qu’il est exercé autant que possible. Oui. Les tests sont très lents. Il leur faut 24 secondes sur mon processeur Core i7 pour générer la sortie deux fois. Heureusement, dans notre développement futur, nous garderons le gm.txt fichier intacte et en générer un autre une seule fois par exécution. Mais 12 secondes, c’est toujours un temps énorme pour une base de code aussi réduite.

Lorsque nous aurons terminé cette série, nos tests devraient s’exécuter en moins d’une seconde et tester tout le code correctement. Alors, restez à l’écoute pour notre prochain tutoriel lorsque nous aborderons des problèmes tels que les constantes magiques, les chaînes magiques et les conditions complexes. Merci d'avoir lu.