Dans la sixième partie de notre série, nous avons discuté de l'attaque des méthodes longues en tirant parti de la programmation en binôme et en visualisant le code à partir de différents niveaux. Nous effectuons constamment des zooms avant et arrière, en examinant de petites choses comme le nom, la forme et l'indentation.
Aujourd'hui, nous adopterons une autre approche : nous supposerons que nous sommes seuls et que nous n'avons aucun collègue ou partenaire pour nous aider. Nous utiliserons une technique appelée « Extraire jusqu'à ce que vous le laissiez tomber » pour diviser le code en très petits morceaux. Nous ferons tout notre possible pour rendre ces sections aussi faciles à comprendre que possible afin que les générations futures d'entre nous ou tout autre programmeur puissent les comprendre facilement.
J'ai entendu parler de ce concept pour la première fois par Robert C. Martin. Il a proposé cette idée dans l'une de ses vidéos comme moyen simple de refactoriser du code difficile à comprendre.
L'idée de base est de prendre de petits extraits de code compréhensibles et de les extraire. Peu importe si vous identifiez quatre lignes ou quatre caractères que vous pouvez extraire. Lorsque vous identifiez un contenu pouvant être encapsulé dans un concept plus clair, vous pouvez procéder à l'extraction. Vous continuez ce processus sur la méthode d'origine et les fragments nouvellement extraits jusqu'à ce que vous ne trouviez pas un morceau de code pouvant être encapsulé en tant que concept.
Cette technique est particulièrement utile lorsque vous travaillez seul. Cela vous oblige à réfléchir à la fois aux petits et aux gros morceaux de code. Cela a également un autre effet sympa : cela vous fait réfléchir - beaucoup au code ! En plus de la méthode d'extraction ou de refactorisation de variables mentionnée ci-dessus, vous vous retrouverez également à renommer des variables, des fonctions, des classes, etc.
Regardons un exemple de code aléatoire provenant d'Internet. Stackoverflow est un excellent endroit pour trouver de petits extraits de code. Voici comment déterminer si un nombre est premier :
//Check if a number is prime function isPrime($num, $pf = null) { if(!is_array($pf)) { for($i=2;$i<intval(sqrt($num));$i++) { if($num % $i==0) { return false; } } return true; } else { $pfCount = count($pf); for($i=0;$i<$pfCount;$i++) { if($num % $pf[$i] == 0) { return false; } } return true; } }
À ce stade, je n'ai aucune idée du fonctionnement de ce code. Je viens de le trouver en ligne au moment où j'écrivais ceci et je vais le découvrir avec vous. Le processus qui suit n’est peut-être pas le plus propre. Au lieu de cela, cela reflétera mon raisonnement et ma reconstruction sans planification préalable.
Selon Wikipédia :
Un nombre premier (ou nombre premier) est un nombre naturel supérieur à 1 qui n'a pas d'autre facteur positif que 1 et lui-même. 块引用>Comme vous pouvez le voir, c'est un moyen simple de résoudre des problèmes mathématiques simples. Il revient
true
或false
, il devrait donc également être facile à tester.class IsPrimeTest extends PHPUnit_Framework_TestCase { function testItCanRecognizePrimeNumbers() { $this->assertTrue(isPrime(1)); } } // Check if a number is prime function isPrime($num, $pf = null) { // ... the content of the method as seen above }Copier après la connexionLorsque nous utilisons simplement un exemple de code, le moyen le plus simple est de tout mettre dans un fichier de test. De cette façon, nous n'avons pas à penser aux fichiers à créer, au répertoire auquel ils appartiennent ou à la manière de les inclure dans un autre répertoire. Ceci n'est qu'un exemple simple afin que nous puissions nous familiariser avec la technique avant de l'appliquer à l'une des méthodes du jeu-questionnaire. Donc tout est mis dans un fichier de test et vous pouvez le nommer comme vous le souhaitez. J'ai choisi
IsPrimeTest.php
.Ce test a réussi. Mon prochain instinct a été d'ajouter plus de nombres premiers au lieu d'écrire un autre test avec des non-premiers.
function 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)); }Copier après la connexionC'est tout. Mais et alors ?
function testItCanRecognizeNonPrimes() { $this->assertFalse(isPrime(6)); }Copier après la connexionCela échoue de manière inattendue : 6 n’est pas un nombre premier. J'attends le retour de la méthode
false
。我不知道该方法是如何工作的,也不知道$pf
参数的目的 - 我只是希望它根据其名称和描述返回false
. Je ne sais pas pourquoi cela ne fonctionne pas ni comment y remédier.C'est un dilemme plutôt déroutant. Que devons-nous faire ? La meilleure réponse est d’écrire des tests qui réussissent un grand nombre. Nous devrons peut-être essayer de deviner, mais au moins nous aurons une idée de ce que fait cette méthode. Nous pourrons alors commencer à le reconstruire.
function testFirst20NaturalNumbers() { for ($i=1;$i<20;$i++) { echo $i . ' - ' . (isPrime($i) ? 'true' : 'false') . "\n"; } }Copier après la connexionSortez quelque chose d'intéressant :
1 - true 2 - true 3 - true 4 - true 5 - true 6 - true 7 - true 8 - true 9 - true 10 - false 11 - true 12 - false 13 - true 14 - false 15 - true 16 - false 17 - true 18 - false 19 - trueCopier après la connexionUne tendance commence à émerger ici. Tous vrais jusqu'à 9, puis alternés jusqu'à 19. Mais ce schéma va-t-il se répéter ? Essayez d'exécuter 100 nombres et vous verrez immédiatement que ce n'est pas le cas. En fait, cela semble fonctionner pour les nombres compris entre 40 et 99. Entre 30 et 39, il échoue une fois en spécifiant 35 comme nombre premier. La même chose est vraie dans la fourchette 20-29. 25 est considéré comme un nombre premier.
Cet exercice a commencé comme une simple démonstration de code d'une technique, mais s'est avéré beaucoup plus difficile que prévu. J'ai décidé de le garder car il reflète la vraie vie de manière typique.
Combien de fois avez-vous commencé à accomplir une tâche apparemment simple, pour ensuite la trouver extrêmement difficile ?Nous ne voulons pas corriger le code. Quoi que fasse la méthode, elle devrait continuer à le faire. Nous espérons le refactoriser afin que d'autres puissent mieux le comprendre.
Comme il ne donne pas les nombres premiers de la bonne manière, nous utiliserons la même méthode Golden Master que nous avons apprise dans la première leçon.
function testGenerateGoldenMaster() { for ($i=1;$i<10000;$i++) { file_put_contents(__DIR__ . '/IsPrimeGoldenMaster.txt', $i . ' - ' . (isPrime($i) ? 'true' : 'false') . "\n", FILE_APPEND); } }Copier après la connexionExécutez-le une fois pour générer Golden Master. Cela 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 ajoutée au contenu précédent.
function testMatchesGoldenMaster() { $goldenMaster = file(__DIR__ . '/IsPrimeGoldenMaster.txt'); for ($i=1;$i<10000;$i++) { $actualResult = $i . ' - ' . (isPrime($i) ? 'true' : 'false'). "\n"; $this->assertTrue(in_array($actualResult, $goldenMaster), 'The value ' . $actualResult . ' is not in the golden master.'); } }Copier après la connexion现在为金牌大师编写测试。这个解决方案可能不是最快的,但它很容易理解,并且如果破坏某些东西,它会准确地告诉我们哪个数字不匹配。但是我们可以将两个测试方法提取到
private
方法中,有一点重复。class IsPrimeTest extends PHPUnit_Framework_TestCase { function testGenerateGoldenMaster() { $this->markTestSkipped(); for ($i=1;$i<10000;$i++) { file_put_contents(__DIR__ . '/IsPrimeGoldenMaster.txt', $this->getPrimeResultAsString($i), FILE_APPEND); } } function testMatchesGoldenMaster() { $goldenMaster = file(__DIR__ . '/IsPrimeGoldenMaster.txt'); for ($i=1;$i<10000;$i++) { $actualResult = $this->getPrimeResultAsString($i); $this->assertTrue(in_array($actualResult, $goldenMaster), 'The value ' . $actualResult . ' is not in the golden master.'); } } private function getPrimeResultAsString($i) { return $i . ' - ' . (isPrime($i) ? 'true' : 'false') . "\n"; } }Copier après la connexion现在我们可以移至生产代码了。该测试在我的计算机上运行大约两秒钟,因此是可以管理的。
竭尽全力提取
首先我们可以在代码的第一部分提取一个
isDivisible()
方法。if(!is_array($pf)) { for($i=2;$i<intval(sqrt($num));$i++) { if(isDivisible($num, $i)) { return false; } } return true; }Copier après la connexion这将使我们能够重用第二部分中的代码,如下所示:
} else { $pfCount = count($pf); for($i=0;$i<$pfCount;$i++) { if(isDivisible($num, $pf[$i])) { return false; } } return true; }Copier après la connexion当我们开始使用这段代码时,我们发现它是粗心地对齐的。大括号有时位于行的开头,有时位于行的末尾。
有时,制表符用于缩进,有时使用空格。有时操作数和运算符之间有空格,有时没有。不,这不是专门创建的代码。这就是现实生活。真实的代码,而不是一些人为的练习。
//Check if a number is prime function isPrime($num, $pf = null) { if (!is_array($pf)) { for ($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; } }Copier après la connexion看起来好多了。两个
if
语句立即看起来非常相似。但由于return
语句,我们无法提取它们。如果我们不回来,我们就会破坏逻辑。如果提取的方法返回一个布尔值,并且我们比较它来决定是否应该从
isPrime()
返回,那根本没有帮助。可能有一种方法可以通过使用 PHP 中的一些函数式编程概念来提取它,但也许稍后。我们可以先做一些简单的事情。function isPrime($num, $pf = null) { if (!is_array($pf)) { return checkDivisorsBetween(2, intval(sqrt($num)), $num); } else { $pfCount = count($pf); for ($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; }Copier après la connexion提取整个
for
循环要容易一些,但是当我们尝试在if
的第二部分重用提取的方法时,我们可以看到它不起作用。有一个神秘的$pf
变量,我们对此几乎一无所知。它似乎检查该数字是否可以被一组特定除数整除,而不是将所有数字达到由
intval(sqrt($num))
确定的另一个神奇值。也许我们可以将$pf
重命名为$divisors
。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($start, $end, $num, $divisors = null) { for ($i = $start; $i < $end; $i++) { if (isDivisible($num, $divisors ? $divisors[$i] : $i)) { return false; } } return true; }Copier après la connexion这是一种方法。我们在检查方法中添加了第四个可选参数。如果它有值,我们就使用它,否则我们使用
$i
。我们还能提取其他东西吗?这段代码怎么样:
intval(sqrt($num))
?function 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)); }Copier après la connexion这样不是更好吗?有些。如果后面的人不知道
intval()
和sqrt()
在做什么,那就更好了,但这无助于让逻辑更容易理解。为什么我们在该特定数字处结束for
循环?也许这就是我们的函数名称应该回答的问题。[PHP]//Check if a number is prime function isPrime($num, $divisors = null) { if (!is_array($divisors)) { return checkDivisorsBetween(2, highestPossibleFactor($num), $num); } else { return checkDivisorsBetween(0, count($divisors), $num, $divisors); } } function highestPossibleFactor($num) { return intval(sqrt($num)); }[PHP]Copier après la connexion这更好,因为它解释了我们为什么停在那里。也许将来我们可以发明一个不同的公式来确定这个数字。命名也带来了一点不一致。我们将这些数字称为因子,它是除数的同义词。也许我们应该选择一个并只使用它。我会让您将重命名重构作为练习。
问题是,我们还能进一步提取吗?好吧,我们必须努力直到失败。我在上面几段提到了 PHP 的函数式编程方面。我们可以在 PHP 中轻松应用两个主要的函数式编程特性:一等函数和递归。每当我在
for
循环中看到带有return
的if
语句,就像我们的checkDivisorsBetween()
方法一样,我就会考虑应用一种或两种技术。function checkDivisorsBetween($start, $end, $num, $divisors = null) { for ($i = $start; $i < $end; $i++) { if (isDivisible($num, $divisors ? $divisors[$i] : $i)) { return false; } } return true; }Copier après la connexion但是我们为什么要经历如此复杂的思考过程呢?最烦人的原因是这个方法做了两个不同的事情:循环和决定。我只想让它循环并将决定留给另一种方法。一个方法应该总是只做一件事并且做得很好。
function checkDivisorsBetween($start, $end, $num, $divisors = null) { $numberIsNotPrime = function ($num, $divisor) { if (isDivisible($num, $divisor)) { return false; } }; for ($i = $start; $i < $end; $i++) { $numberIsNotPrime($num, $divisors ? $divisors[$i] : $i); } return true; }Copier après la connexion我们的第一次尝试是将条件和返回语句提取到变量中。目前,这是本地的。但代码不起作用。实际上
for
循环使事情变得相当复杂。我有一种感觉,一点递归会有所帮助。function checkRecursiveDivisibility($current, $end, $num, $divisor) { if($current == $end) { return true; } }Copier après la connexion当我们考虑递归性时,我们必须始终从特殊情况开始。我们的第一个例外是当我们到达递归末尾时。
function checkRecursiveDivisibility($current, $end, $num, $divisor) { if($current == $end) { return true; } if (isDivisible($num, $divisor)) { return false; } }Copier après la connexion我们会破坏递归的第二个例外情况是当数字可整除时。我们不想继续了。这就是所有例外情况。
ini_set('xdebug.max_nesting_level', 10000); function checkDivisorsBetween($start, $end, $num, $divisors = null) { return checkRecursiveDivisibility($start, $end, $num, $divisors); } function checkRecursiveDivisibility($current, $end, $num, $divisors) { if($current == $end) { return true; } if (isDivisible($num, $divisors ? $divisors[$current] : $current)) { return false; } checkRecursiveDivisibility($current++, $end, $num, $divisors); }Copier après la connexion这是使用递归来解决我们的问题的另一次尝试,但不幸的是,在 PHP 中重复 10.000 次会导致我的系统上的 PHP 或 PHPUnit 崩溃。所以这似乎又是一个死胡同。但如果它能发挥作用,那将是对原始逻辑的一个很好的替代。
挑战
我在写《金主》的时候,故意忽略了一些东西。假设测试没有涵盖应有的代码。你能找出问题所在吗?如果是,您会如何处理?
Pensées finales
« Extraire jusqu'à ce que vous tombiez » est un excellent moyen de décortiquer 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 étonnant de voir comment ce processus simple associé à des changements de nom fréquents m'aide à découvrir que certains codes peuvent faire des choses que je n'aurais jamais cru possibles.
Dans notre prochain et dernier tutoriel sur le refactoring, nous appliquerons cette technique à un jeu-questionnaire. J'espère que vous avez apprécié ce tutoriel un peu différent. Nous ne parlons pas d'exemples de manuels, nous utilisons du vrai code et nous devons lutter contre les vrais problèmes auxquels nous sommes confrontés chaque jour.
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!