解剖長方法並進行擷取的遺留程式碼重構 - 第10部分

PHPz
發布: 2023-09-02 12:46:01
原創
1465 人瀏覽過

解剖长方法并进行提取的遗留代码重构 - 第10部分

在我們系列的第六部分中,我們討論了透過利用結對程式設計和從不同層級查看程式碼來攻擊長方法。我們不斷地放大和縮小,觀察命名、形式和縮排等小事。

#

今天,我們將採取另一種方法:我們假設我們獨自一人,沒有同事或搭檔來幫助我們。我們將使用一種名為「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 和它本身之外沒有正因數。

正如您所看到的,這是解決簡單數學問題的簡單方法。它傳回 truefalse,所以它也應該很容易測試。

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 循环中看到带有 returnif 语句,就像我们的 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中文網其他相關文章!

來源:php.cn
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板