在我们系列的第六部分中,我们讨论了通过利用结对编程和从不同级别查看代码来攻击长方法。我们不断地放大和缩小,观察命名、形式和缩进等小事情。
今天,我们将采取另一种方法:我们假设我们独自一人,没有同事或搭档来帮助我们。我们将使用一种名为“Extract Until you Drop”的技术,将代码分解为非常小的片段。我们将尽一切努力使这些部分尽可能容易理解,以便未来的我们或任何其他程序员将能够轻松理解它们。
我第一次从 Robert C. Martin 那里听说这个概念。他在他的一个视频中提出了这个想法,作为一种重构难以理解的代码的简单方法。
基本思想是获取小的、可理解的代码片段并提取它们。如果您识别出可以提取的四行或四个字符,这并不重要。当您确定可以封装在更清晰的概念中的内容时,您就可以进行提取。您在原始方法和新提取的片段上继续此过程,直到找不到可以封装为概念的代码片段。
当您独自工作时,此技术特别有用。它迫使您同时考虑小代码块和大代码块。它还有另一个很好的效果:它让你思考代码——很多!除了上面提到的提取方法或变量重构之外,您还会发现自己重命名变量、函数、类等等。
让我们看一个来自互联网的随机代码的示例。 Stackoverflow 是查找小代码片段的好地方。这是确定数字是否为质数的方法:
//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; } }
此时,我不知道这段代码是如何工作的。我在写这篇文章的时候刚刚在网上找到了它,我会和你一起发现它。接下来的过程可能不是最干净的。相反,它将反映我的推理和重构,而无需预先规划。
根据维基百科:
素数(或素数)是大于 1 的自然数,除了 1 和它本身之外没有正因数。 块引用>正如您所看到的,这是解决简单数学问题的简单方法。它返回
true
或false
,所以它也应该很容易测试。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 }当我们只是使用示例代码时,最简单的方法是将所有内容放入测试文件中。这样我们就不必考虑要创建哪些文件,它们属于哪个目录,或者如何将它们包含在另一个目录中。这只是一个简单的示例,以便我们在将其应用于其中一种问答游戏方法之前熟悉该技术。因此,所有内容都放在一个测试文件中,您可以根据需要命名。我选择了
IsPrimeTest.php
。该测试通过。我的下一个直觉是添加更多的素数,而不是用非素数编写另一个测试。
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)); }就这么过去了。但这又如何呢?
function testItCanRecognizeNonPrimes() { $this->assertFalse(isPrime(6)); }这意外失败:6 不是素数。我期待该方法返回
false
。我不知道该方法是如何工作的,也不知道$pf
参数的目的 - 我只是希望它根据其名称和描述返回false
。我不知道为什么它不起作用也不知道如何修复它。这是一个相当令人困惑的困境。我们应该做什么?最好的答案是编写能够通过大量数字的测试。我们可能需要尝试和猜测,但至少我们会对这个方法的作用有一些了解。然后我们就可以开始重构它了。
function testFirst20NaturalNumbers() { for ($i=1;$i<20;$i++) { echo $i . ' - ' . (isPrime($i) ? 'true' : 'false') . "\n"; } }输出一些有趣的东西:
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 - true这里开始出现一种模式。直到 9 为止全部为真,然后交替直到 19。但是这种模式会重复吗?尝试运行 100 个数字,您会立即发现它不是。实际上,它似乎适用于 40 到 99 之间的数字。在 30-39 之间,它通过指定 35 作为质数而失败了一次。在 20-29 范围内也是如此。 25 被认为是素数。
这个练习最初是用一个简单的代码来演示一种技术,但事实证明比预期的要困难得多。我决定保留它,因为它以典型的方式反映了现实生活。
有多少次你开始做一项看起来很简单的任务,却发现它极其困难?我们不想修复代码。无论该方法做什么,它都应该继续这样做。我们希望重构它以使其他人更好地理解它。
由于它不能以正确的方式告诉质数,我们将使用我们在第一课中学到的相同的 Golden Master 方法。
function testGenerateGoldenMaster() { for ($i=1;$i<10000;$i++) { file_put_contents(__DIR__ . '/IsPrimeGoldenMaster.txt', $i . ' - ' . (isPrime($i) ? 'true' : 'false') . "\n", FILE_APPEND); } }运行一次即可生成 Golden Master。它应该跑得很快。如果您需要重新运行它,请不要忘记在执行测试之前删除该文件。否则输出将附加到之前的内容。
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.'); } }现在为金牌大师编写测试。这个解决方案可能不是最快的,但它很容易理解,并且如果破坏某些东西,它会准确地告诉我们哪个数字不匹配。但是我们可以将两个测试方法提取到
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"; } }现在我们可以移至生产代码了。该测试在我的计算机上运行大约两秒钟,因此是可以管理的。
竭尽全力提取
首先我们可以在代码的第一部分提取一个
isDivisible()
方法。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; }当我们开始使用这段代码时,我们发现它是粗心地对齐的。大括号有时位于行的开头,有时位于行的末尾。
有时,制表符用于缩进,有时使用空格。有时操作数和运算符之间有空格,有时没有。不,这不是专门创建的代码。这就是现实生活。真实的代码,而不是一些人为的练习。
//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; } }看起来好多了。两个
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; }提取整个
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; }这是一种方法。我们在检查方法中添加了第四个可选参数。如果它有值,我们就使用它,否则我们使用
$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)); }这样不是更好吗?有些。如果后面的人不知道
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]这更好,因为它解释了我们为什么停在那里。也许将来我们可以发明一个不同的公式来确定这个数字。命名也带来了一点不一致。我们将这些数字称为因子,它是除数的同义词。也许我们应该选择一个并只使用它。我会让您将重命名重构作为练习。
问题是,我们还能进一步提取吗?好吧,我们必须努力直到失败。我在上面几段提到了 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; }但是我们为什么要经历如此复杂的思考过程呢?最烦人的原因是这个方法做了两个不同的事情:循环和决定。我只想让它循环并将决定留给另一种方法。一个方法应该总是只做一件事并且做得很好。
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; }我们的第一次尝试是将条件和返回语句提取到变量中。目前,这是本地的。但代码不起作用。实际上
for
循环使事情变得相当复杂。我有一种感觉,一点递归会有所帮助。function checkRecursiveDivisibility($current, $end, $num, $divisor) { if($current == $end) { return true; } }当我们考虑递归性时,我们必须始终从特殊情况开始。我们的第一个例外是当我们到达递归末尾时。
function checkRecursiveDivisibility($current, $end, $num, $divisor) { if($current == $end) { return true; } if (isDivisible($num, $divisor)) { return false; } }我们会破坏递归的第二个例外情况是当数字可整除时。我们不想继续了。这就是所有例外情况。
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); }这是使用递归来解决我们的问题的另一次尝试,但不幸的是,在 PHP 中重复 10.000 次会导致我的系统上的 PHP 或 PHPUnit 崩溃。所以这似乎又是一个死胡同。但如果它能发挥作用,那将是对原始逻辑的一个很好的替代。
挑战
我在写《金主》的时候,故意忽略了一些东西。假设测试没有涵盖应有的代码。你能找出问题所在吗?如果是,您会如何处理?
最终想法
“提取直到放弃”是剖析长方法的好方法。它迫使您思考小段代码,并通过将它们提取到方法中来赋予这些代码段目的。我发现令人惊奇的是,这个简单的过程加上频繁的重命名,可以帮助我发现某些代码可以完成我从未想过的事情。
在我们的下一个也是最后一个关于重构的教程中,我们将把这种技术应用到问答游戏中。我希望您喜欢这个有点不同的教程。我们没有谈论教科书上的示例,而是使用了一些真实的代码,并且我们必须与每天面临的实际问题作斗争。
以上是解剖长方法并进行提取的遗留代码重构 - 第10部分的详细内容。更多信息请关注PHP中文网其他相关文章!