Code du remaniement en place - Partie 10 Méthodes de dissection longues avec extractions

Dans la sixième partie de notre série, nous avons parlé de l’attaque des méthodes longues en utilisant la programmation par paires et la visualisation de code à différents niveaux. Nous avons fait des zooms avant et arrière continus et avons observé de petites choses comme nommer, ainsi que la forme et l’indentation.

Aujourd’hui, nous adopterons une autre approche: nous supposerons que nous sommes seuls, pas de collègues ni de paires pour nous aider. Nous allons utiliser une technique appelée "Extraire jusqu'à ce que vous laissiez tomber" qui décompose le code en très petits morceaux. Nous ferons tous les efforts possibles pour rendre ces morceaux aussi faciles à comprendre que possible afin que notre avenir, ou tout autre programmeur, puisse les comprendre facilement..


Extrait jusqu'à ce que vous laissiez tomber

J'ai d'abord entendu parler de ce concept de Robert C. Martin. Il a présenté l’idée dans l’une de ses vidéos comme un moyen simple de modifier le code difficile à comprendre..

L'idée de base est de prendre de petits morceaux de code compréhensibles et de les extraire. Peu importe si vous identifiez quatre lignes ou quatre caractères pouvant être extraits. Lorsque vous identifiez quelque chose qui peut être encapsulé dans un concept plus clair, vous extrayez. Vous continuez ce processus à la fois sur la méthode d'origine et sur les éléments récemment extraits jusqu'à ce que vous ne puissiez plus trouver de code pouvant être encapsulé en tant que concept..

Cette technique est particulièrement utile lorsque vous travaillez seul. Cela vous oblige à penser à la fois aux petits et aux plus gros morceaux de code. Cela a un autre effet intéressant: cela vous fait penser au code - beaucoup! Outre la méthode d'extraction ou le refactoring de variable mentionné ci-dessus, vous devrez renommer des variables, des fonctions, des classes, etc..

Voyons un exemple sur du code aléatoire provenant d’Internet. Stackoverflow est un bon endroit pour trouver de petits morceaux de code. En voici un qui détermine si un nombre est premier:

// Vérifie si un nombre est une fonction première isPrime ($ num, $ pf = null) if (! Is_array ($ pf)) pour ($ i = 2; $ i

À ce stade, je n'ai aucune idée du fonctionnement de ce code. Je viens de le trouver sur Internet en écrivant cet article et je vais le découvrir avec vous. Le processus qui suit peut ne pas être le plus propre. Au lieu de cela, cela reflètera mon raisonnement et ma refactorisation au fur et à mesure, sans planification préalable.

Refactoriser le vérificateur de nombres premiers

Selon Wikipedia:

Un nombre premier (ou un nombre premier) est un nombre naturel supérieur à 1 qui n'a pas de diviseur positif autre que 1 et lui-même.. 

Comme vous pouvez le constater, cette méthode est simple pour un problème mathématique simple. Il retourne vrai ou faux, il devrait donc aussi être facile de tester.

La classe IsPrimeTest étend PHPUnit_Framework_TestCase function testItCanRecognizePrimeNumbers () $ this-> assertTrue (isPrime (1));  // Vérifie si un nombre est une fonction première isPrime ($ num, $ pf = null) //… le contenu de la méthode, comme indiqué ci-dessus

Lorsque nous jouons avec un exemple de code, le moyen le plus simple consiste à tout placer dans un fichier test. De cette façon, nous n'avons pas à penser aux fichiers à créer, aux répertoires auxquels ils appartiennent, ni à la manière de les inclure dans l'autre. Ceci est juste un exemple simple à utiliser pour se familiariser avec la technique avant de l’appliquer à l’une des méthodes de jeu-questionnaire. Donc, tout va dans un fichier test, vous pouvez nommer comme vous le souhaitez. j'ai choisi IsPrimeTest.php.

Ce test réussit. Mon instinct suivant est d’ajouter quelques nombres premiers de plus et d’écrire un autre test avec des nombres non premiers..

fonction testItCanRecognizePrimeNumbers () $ this-> assertTrue (isPrime (1)); $ this-> assertTrue (isPrime (2)); $ this-> assertTrue (isPrime (3)); $ this-> assertTrue (isPrime (5)); $ this-> assertTrue (isPrime (7)); $ this-> assertTrue (isPrime (11)); 

Ça passe. Mais qu'en est-il?

fonction testItCanRecognizeNonPrimes () $ this-> assertFalse (isPrime (6)); 

Cela échoue de manière inattendue: 6 n'est pas un nombre premier. Je m'attendais à la méthode pour revenir faux. Je ne sais pas comment fonctionne la méthode, ni le but de la $ pf paramètre - je m'attendais simplement à ce qu'il revienne faux basé sur son nom et sa description. Je ne sais pas pourquoi cela ne fonctionne pas ni comment y remédier.

C'est un dilemme assez déroutant. Que devrions nous faire? La meilleure réponse est d’écrire des tests qui passent pour un volume décent de nombres. Nous devrons peut-être essayer de deviner, mais au moins nous aurons une idée de ce que fait la méthode. Ensuite, nous pouvons commencer à le refactoriser.

fonction testFirst20NaturalNumbers () pour ($ i = 1; $ i<20;$i++)  echo $i . ' - ' . (isPrime($i) ? 'true' : 'false') . "\n";  

Cela produit quelque chose d'intéressant:

1 - vrai 2 - vrai 3 - vrai 4 - vrai 5 - vrai 6 - vrai 7 - vrai 8 - vrai 9 - vrai 10 - faux 11 - vrai 12 - faux 13 - vrai 14 - faux 15 - vrai 16 - faux 17 - vrai 18 - faux 19 - vrai

Un modèle commence à émerger ici. Tous vrais jusqu'à 9, puis en alternance jusqu'à 19. Mais ce modèle se répète-t-il? Essayez de l'exécuter pour 100 numéros et vous verrez immédiatement que ce n'est pas le cas. Il semble en fait que cela fonctionne pour les nombres entre 40 et 99. Il échoue une fois entre 30 et 39 en nommant 35 comme premier. La même chose est vraie dans la gamme 20-29. 25 est considéré comme primordial.

Cet exercice qui a commencé comme un simple code pour démontrer une technique s'avère beaucoup plus difficile que prévu. J'ai décidé de le garder car il reflète la vie réelle de manière typique.

Combien de fois avez-vous commencé à travailler sur une tâche qui vous semblait facile, juste pour découvrir que c'est extrêmement difficile?

Nous ne voulons pas réparer le code. Quelle que soit la méthode utilisée, elle devrait continuer à le faire. Nous voulons le refactoriser pour que les autres le comprennent mieux.

Comme il ne dit pas les nombres premiers de manière correcte, nous utiliserons la même approche Golden Master que celle que nous avons apprise à la leçon 1..

fonction testGenerateGoldenMaster () pour ($ i = 1; $ i<10000;$i++)  file_put_contents(__DIR__ . '/IsPrimeGoldenMaster.txt', $i . ' - ' . (isPrime($i) ? 'true' : 'false') . "\n", FILE_APPEND);  

Exécutez cette fois pour générer le Golden Master. Il devrait courir vite. Si vous devez le réexécuter, n'oubliez pas de supprimer le fichier avant d'exécuter le test. Sinon, la sortie sera attachée au contenu précédent..

function testMatchesGoldenMaster () $ goldenMaster = fichier (__DIR__. '/IsPrimeGoldenMaster.txt'); pour ($ i = 1; $ i<10000;$i++)  $actualResult = $i . ' - ' . (isPrime($i) ? 'true' : 'false'). "\n"; $this->assertTrue (in_array ($ actualResult, $ goldenMaster), 'La valeur'. $ actualResult. 'n'est pas dans le maître doré.'); 

Maintenant, écrivez le test pour le maître d'or. Cette solution n'est peut-être pas la plus rapide, mais elle est facile à comprendre et nous dira exactement quel numéro ne correspond pas si quelque chose se casse. Mais il y a un peu de duplication dans les deux méthodes de test que nous pourrions extraire dans un privé méthode.

La classe IsPrimeTest étend PHPUnit_Framework_TestCase function testGenerateGoldenMaster () $ this-> markTestSkipped (); pour ($ i = 1; $ i<10000;$i++)  file_put_contents(__DIR__ . '/IsPrimeGoldenMaster.txt', $this->getPrimeResultAsString ($ i), FILE_APPEND);  function testMatchesGoldenMaster () $ goldenMaster = fichier (__DIR__. '/IsPrimeGoldenMaster.txt'); pour ($ i = 1; $ i<10000;$i++)  $actualResult = $this->getPrimeResultAsString ($ i); $ this-> assertTrue (in_array ($ actualResult, $ goldenMaster), 'La valeur'. $ actualResult. 'n'est pas dans le maître d'or.');  fonction privée getPrimeResultAsString ($ i) return $ i. '-' (isPrime ($ i)? 'true': 'false'). "\ n"; 

Nous pouvons maintenant passer à notre code de production. Le test dure environ deux secondes sur mon ordinateur, il est donc gérable..

Extraire tout ce que nous pouvons

Tout d'abord, nous pouvons extraire un isDivisible () méthode dans la première partie du code.

if (! is_array ($ pf)) pour ($ i = 2; $ i

Cela nous permettra de réutiliser le code dans la deuxième partie comme ceci:

 else $ pfCount = count ($ pf); pour ($ i = 0; $ i<$pfCount;$i++)  if(isDivisible($num, $pf[$i]))  return false;   return true; 

Et dès que nous avons commencé à travailler avec ce code, nous avons constaté qu’il était mal aligné. Les accolades sont parfois au début de la ligne, d'autres fois à la fin. 

Parfois, les tabulations sont utilisées pour l'indentation, parfois les espaces. Parfois, il y a des espaces entre l'opérande et l'opérateur, parfois non. Et non, ce n'est pas un code spécialement créé. Ceci est la vraie vie. Code réel, pas un exercice artificiel.

// Vérifie si un nombre est une fonction première isPrime ($ num, $ pf = null) if (! Is_array ($ pf)) pour ($ i = 2; $ i < intval(sqrt($num)); $i++)  if (isDivisible($num, $i))  return false;   return true;  else  $pfCount = count($pf); for ($i = 0; $i < $pfCount; $i++)  if (isDivisible($num, $pf[$i]))  return false;   return true;  

Cela semble mieux. Immédiatement les deux si les déclarations sont très similaires. Mais nous ne pouvons pas les extraire à cause de la revenir déclarations. Si nous ne revenons pas, nous briserons la logique. 

Si la méthode extraite renvoie un booléen et que nous la comparons pour décider si nous devons ou non revenir de isPrime (), cela n'aiderait pas du tout. Il peut y avoir un moyen de l'extraire en utilisant des concepts de programmation fonctionnels en PHP, mais peut-être plus tard. Nous pouvons faire quelque chose de plus simple en premier.

fonction isPrime ($ num, $ pf = null) if (! is_array ($ pf)) return checkDivisorsBetween (2, intval (sqrt ($ num)), $ num);  else $ pfCount = count ($ pf); pour ($ i = 0; $ i < $pfCount; $i++)  if (isDivisible($num, $pf[$i]))  return false;   return true;   function checkDivisorsBetween($start, $end, $num)  for ($i = $start; $i < $end; $i++)  if (isDivisible($num, $i))  return false;   return true; 

Extraire le pour la boucle dans son ensemble est un peu plus facile, mais lorsque nous essayons de réutiliser notre méthode extraite dans la deuxième partie de la si nous pouvons voir que cela ne fonctionnera pas. Il y a ce mystérieux $ pf variable à propos de laquelle nous ne savons presque rien. 

Il semble que cela vérifie si le nombre est divisible par un ensemble de diviseurs spécifiques au lieu de prendre tous les nombres jusqu'à l'autre valeur magique déterminée par intval (sqrt ($ num)). Peut-être pourrions-nous renommer $ pf dans diviseurs $.

function isPrime ($ num, $ divisors = null) if (! is_array ($ divisors)) return checkDivisorsBetween (2, intval (sqrt ($ num)), $ num);  else return checkDivisorsBetween (0, count ($ divisors), $ num, $ divisors);  function checkDivisorsBetween ($ début, $ fin, $ num, $ diviseurs = null) pour ($ i = $ début; $ i < $end; $i++)  if (isDivisible($num, $divisors ? $divisors[$i] : $i))  return false;   return true; 

C'est une façon de le faire. Nous avons ajouté un quatrième paramètre, facultatif, à notre méthode de vérification. S'il a une valeur, nous l'utilisons, sinon nous utilisons $ i.

Pouvons-nous extraire autre chose? Qu'en est-il de ce morceau de code: intval (sqrt ($ num))?

fonction isPrime ($ num, $ divisors = null) if (! is_array ($ divisors)) return checkDivisorsBetween (2, integerRootOf ($ num), $ num);  else return checkDivisorsBetween (0, count ($ divisors), $ num, $ divisors);  function integerRootOf ($ num) return intval (sqrt ($ num)); 

N'est-ce pas mieux? Quelque peu. C'est mieux si la personne qui vient après nous ne sait pas quoi intval () et sqrt () font, mais cela n’aide pas à rendre la logique plus facile à comprendre. Pourquoi finissons-nous notre pour boucle à ce nombre spécifique? C’est peut-être la question à laquelle notre nom de fonction devrait répondre.

[PHP] // Vérifie si un nombre est une fonction première isPrime ($ num, $ divisors = null) if (! Is_array ($ divisors)) return checkDivisorsBetween (2, mostPossibleFactor ($ num), $ num);  else return checkDivisorsBetween (0, count ($ divisors), $ num, $ divisors);  function mostPossibleFactor ($ num) return intval (sqrt ($ num));  [PHP]

C’est mieux, car cela explique pourquoi nous nous arrêtons là. Peut-être pourrions-nous, à l'avenir, inventer une formule différente pour déterminer ce nombre. La dénomination introduit également une petite incohérence. Nous avons appelé les facteurs de nombres, synonyme de diviseurs. Peut-être devrions-nous en choisir un et l'utiliser uniquement. Je vais vous laisser faire la refactorisation du renommage comme un exercice.

La question est, pouvons-nous extraire davantage? Eh bien, nous devons essayer jusqu'à ce que nous tombions. J'ai mentionné quelques paragraphes ci-dessus du côté de la programmation fonctionnelle de PHP. Il existe deux principales caractéristiques fonctionnelles de programmation que nous pouvons facilement appliquer en PHP: les fonctions de première classe et la récursion. Chaque fois que je vois un si déclaration avec un revenir à l'intérieur d'un pour boucle, comme dans notre checkDivisorsBetween () méthode, je pense appliquer une ou les deux techniques.

function checkDivisorsBetween ($ début, $ fin, $ num, $ diviseurs = null) pour ($ i = $ début; $ i < $end; $i++)  if (isDivisible($num, $divisors ? $divisors[$i] : $i))  return false;   return true; 

Mais pourquoi devrions-nous passer par un processus de pensée aussi complexe? La raison la plus agaçante est que cette méthode fait deux choses distinctes: elle fait un cycle et décide. Je le veux seulement faire un cycle et laisser la décision à une autre méthode. Une méthode devrait toujours faire une seule chose et bien le faire.

function checkDivisorsBetween ($ start, $ end, $ num, $ divisors = null) $ numberIsNotPrime = fonction ($ num, $ divisor) if (isDivisible ($ num, $ divisor))) return false; ; pour ($ i = $ start; $ i < $end; $i++)  $numberIsNotPrime($num, $divisors ? $divisors[$i] : $i);  return true; 

Notre première tentative a été d'extraire la condition et l'instruction return dans une variable. C'est local, pour le moment. Mais le code ne fonctionne pas. En fait le pour La boucle complique un peu les choses. J'ai le sentiment qu'un peu de récursivité aidera.

fonction checkRecursiveDivisibility ($ current, $ end, $ num, $ diviseur) if ($ current == $ end) return true; 

Lorsque nous pensons à la récursivité, nous devons toujours commencer par les cas exceptionnels. Notre première exception est lorsque nous atteignons la fin de notre récursion.

fonction checkRecursiveDivisibility ($ current, $ end, $ num, $ diviseur) if ($ current == $ end) return true;  if (isDivisible ($ num, $ divisor)) return false; 

Notre deuxième cas exceptionnel qui rompra la récursivité est celui où le nombre est divisible. Nous ne voulons pas continuer. Et c'est à peu près tous les cas exceptionnels.

ini_set ('xdebug.max_nesting_level', 10000); function checkDivisorsBetween ($ start, $ end, $ num, $ divisors = null) return checkRecursiveDivisibility ($ start, $ end, $ num, $ divisors);  function checkRecursiveDivisibility ($ actuel, $ fin, $ num, $ diviseurs) if ($ actuel == $ fin) return true;  if (isDivisible ($ num, $ divisors? $ divisors [$ current]: $ current)) return false;  checkRecursiveDivisibility ($ current ++, $ end, $ num, $ divisors); 

Ceci est une autre tentative d'utilisation de la récursivité pour notre problème, mais malheureusement, une répétition de 10.000 fois en PHP entraîne un crash de PHP ou PHPUnit sur mon système. Donc, cela semble être une autre impasse. Mais si cela avait fonctionné, cela aurait été un bon remplacement de la logique originale.


Défi

Quand j'ai écrit le Golden Master, j'ai délibérément oublié quelque chose. Disons simplement que les tests ne couvrent pas autant de code qu'ils le devraient. Pouvez-vous repérer le problème? Si oui, comment l'aborderiez-vous??


Dernières pensées

"Extraire jusqu'à épuisement" est un bon moyen de disséquer les méthodes longues. Cela vous oblige à réfléchir à de petits morceaux de code et à leur donner un but en les extrayant dans des méthodes. Je trouve incroyable de voir comment cette procédure simple, associée à un changement de nom fréquent, peut m'aider à découvrir que certains codes font des choses que je n'aurais jamais imaginées possibles..

Dans notre prochain et dernier tutoriel sur la refactorisation, nous appliquerons cette technique au jeu-questionnaire. J'espère que vous avez aimé ce tutoriel qui s'est avéré un peu différent. Au lieu de parler d'exemples de manuels, nous avons pris du vrai code et nous avons dû nous battre avec les vrais problèmes auxquels nous sommes confrontés chaque jour.