Rumah  >  Artikel  >  pembangunan bahagian belakang  >  Membedah kaedah yang panjang dan melaksanakan pemfaktoran semula kod warisan yang diekstrak - Bahagian 10

Membedah kaedah yang panjang dan melaksanakan pemfaktoran semula kod warisan yang diekstrak - Bahagian 10

PHPz
PHPzasal
2023-09-02 12:45:091376semak imbas

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

Dalam bahagian enam siri kami, kami membincangkan menyerang kaedah yang panjang dengan memanfaatkan pengaturcaraan pasangan dan melihat kod dari tahap yang berbeza. Kami sentiasa mengezum masuk dan keluar, melihat perkara kecil seperti penamaan, bentuk dan lekukan.

Hari ini kami akan mengambil pendekatan lain: kami akan menganggap bahawa kami bersendirian dan tidak mempunyai rakan sekerja atau rakan kongsi untuk membantu kami. Kami akan menggunakan teknik yang dipanggil "Ekstrak Sehingga Anda Jatuhkan" untuk memecahkan kod kepada kepingan yang sangat kecil. Kami akan berusaha sedaya upaya untuk menjadikan bahagian ini semudah mungkin untuk difahami supaya generasi akan datang kita atau mana-mana pengaturcara lain dapat memahaminya dengan mudah.


Ekstrak sehingga jatuh

Saya mula-mula mendengar tentang konsep ini daripada Robert C. Martin. Dia mencadangkan idea ini dalam salah satu videonya sebagai cara mudah untuk memfaktorkan semula kod yang sukar difahami.

Idea asas ialah mengambil coretan kod yang kecil dan mudah difahami dan mengekstraknya. Tidak kira jika anda mengenal pasti empat baris atau empat aksara yang boleh anda ekstrak. Apabila anda mengenal pasti kandungan yang boleh dirangkumkan dalam konsep yang lebih jelas, anda boleh meneruskan pengekstrakan. Anda meneruskan proses ini pada kaedah asal dan serpihan yang baru diekstrak sehingga anda tidak dapat mencari sekeping kod yang boleh dikapsulkan sebagai konsep.

Teknik ini amat berguna apabila anda bekerja sendirian. Ia memaksa anda untuk memikirkan kedua-dua ketulan kod kecil dan ketulan kod besar. Ia juga mempunyai satu lagi kesan bagus: ia membuatkan anda berfikir tentang kod - banyak! Selain kaedah pengekstrakan atau pemfaktoran semula pembolehubah yang dinyatakan di atas, anda juga akan mendapati diri anda menamakan semula pembolehubah, fungsi, kelas dan banyak lagi.

Mari kita lihat contoh kod rawak daripada internet. Stackoverflow ialah tempat yang bagus untuk mencari coretan kod kecil. Berikut ialah cara untuk menentukan sama ada nombor adalah perdana:

//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;
    }
}

Pada ketika ini, saya tidak tahu cara kod ini berfungsi. Saya baru sahaja menemuinya dalam talian semasa saya menulis ini dan saya akan menemuinya bersama anda. Proses yang berikut mungkin bukan yang paling bersih. Sebaliknya, ia akan mencerminkan penaakulan dan pembinaan semula saya tanpa perancangan awal.

Pemeriksa nombor perdana difaktor semula

Menurut Wikipedia:

Nombor perdana (atau nombor perdana) ialah nombor asli lebih besar daripada 1 yang tidak mempunyai faktor positif selain 1 dan dirinya sendiri. 块引用>

Seperti yang anda lihat, ini adalah cara mudah untuk menyelesaikan masalah matematik yang mudah. Ia mengembalikan true atau false, jadi ia juga harus mudah untuk diuji. 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";
	}
}

Apabila kita hanya menggunakan kod sampel, cara paling mudah ialah memasukkan semuanya ke dalam fail ujian. Dengan cara ini kita tidak perlu memikirkan fail mana yang hendak dibuat, direktori mana yang dimilikinya, atau cara memasukkannya ke dalam direktori lain. Ini hanyalah contoh mudah supaya kita dapat membiasakan diri dengan teknik tersebut sebelum mengaplikasikannya pada salah satu kaedah permainan trivia. Jadi semuanya dimasukkan ke dalam fail ujian dan anda boleh menamakannya apa sahaja yang anda mahu. Saya memilih IsPrimeTest.php.

Ujian lulus. Naluri saya seterusnya adalah untuk menambah lebih banyak nombor perdana dan bukannya menulis ujian lain dengan bukan nombor perdana.

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

Itu sahaja. Tetapi jadi apa?

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

Ini gagal secara tidak dijangka: 6 bukan nombor perdana. Saya menjangkakan kaedah untuk mengembalikan false. Saya tidak tahu cara kaedah ini berfungsi, mahupun tujuan parameter $pf - Saya hanya mahu ia mengembalikan false berdasarkan nama dan penerangannya . Saya tidak tahu mengapa ia tidak berfungsi atau bagaimana untuk membetulkannya.

Ini adalah dilema yang agak mengelirukan. Apa yang patut kita buat? Jawapan terbaik ialah menulis ujian yang lulus nombor besar. Kita mungkin perlu mencuba dan meneka, tetapi sekurang-kurangnya kita akan mempunyai sedikit idea tentang kaedah ini. Kemudian kita boleh mula membina semula.
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.');
	}
}
Keluarkan sesuatu yang menarik:

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";
	}
}

Satu corak mula muncul di sini. Semua benar sehingga 9, kemudian silih berganti sehingga 19. Tetapi adakah corak ini akan berulang? Cuba jalankan 100 nombor dan anda akan segera melihat bahawa ia tidak. Malah, ia nampaknya berfungsi untuk nombor antara 40 dan 99. Antara 30-39 ia gagal sekali dengan menyatakan 35 sebagai nombor perdana. Perkara yang sama berlaku dalam julat 20-29. 25 dianggap sebagai nombor perdana.

Latihan ini bermula sebagai demonstrasi kod teknik yang mudah, tetapi ternyata lebih sukar daripada yang dijangkakan. Saya memutuskan untuk menyimpannya kerana ia mencerminkan kehidupan sebenar dengan cara yang biasa.

Berapa kali anda mula melakukan tugas yang kelihatan mudah, tetapi mendapati ia amat sukar? 🎜 🎜Kami tidak mahu membetulkan kod. Walau apa pun kaedah itu, ia harus terus melakukannya. Kami berharap dapat memfaktorkannya semula supaya orang lain dapat memahaminya dengan lebih baik. 🎜 🎜Memandangkan ia tidak memberitahu nombor perdana dengan cara yang betul, kami akan menggunakan kaedah Golden Master yang sama yang kami pelajari dalam pelajaran pertama. 🎜
if(!is_array($pf))
{
    for($i=2;$i<intval(sqrt($num));$i++) {
		if(isDivisible($num, $i)) {
			return false;
		}
	}
	return true;
}
🎜Jalankannya sekali untuk menjana Golden Master. Ia sepatutnya berjalan pantas. Jika anda perlu menjalankannya semula, jangan lupa padam fail sebelum melaksanakan ujian. Jika tidak, output akan ditambahkan pada kandungan sebelumnya. 🎜
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 崩溃。所以这似乎又是一个死胡同。但如果它能发挥作用,那将是对原始逻辑的一个很好的替代。


挑战

我在写《金主》的时候,故意忽略了一些东西。假设测试没有涵盖应有的代码。你能找出问题所在吗?如果是,您会如何处理?


Fikiran Akhir

"Ambil sehingga anda jatuh" ialah cara terbaik untuk membedah kaedah yang panjang. Ia memaksa anda untuk memikirkan cebisan kecil kod dan memberikan cebisan kod itu tujuan dengan mengekstraknya ke dalam kaedah. Saya rasa mengagumkan bagaimana proses mudah ini ditambah dengan penamaan semula yang kerap membantu saya mengetahui bahawa kod tertentu boleh melakukan perkara yang tidak pernah saya fikirkan.

Dalam tutorial seterusnya dan terakhir kami tentang pemfaktoran semula, kami akan menggunakan teknik ini pada permainan trivia. Saya harap anda menikmati tutorial ini yang sedikit berbeza. Kami tidak bercakap tentang contoh buku teks, kami menggunakan beberapa kod sebenar, dan kami perlu bergelut dengan masalah sebenar yang kami hadapi setiap hari.

Atas ialah kandungan terperinci Membedah kaedah yang panjang dan melaksanakan pemfaktoran semula kod warisan yang diekstrak - Bahagian 10. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!

Kenyataan:
Kandungan artikel ini disumbangkan secara sukarela oleh netizen, dan hak cipta adalah milik pengarang asal. Laman web ini tidak memikul tanggungjawab undang-undang yang sepadan. Jika anda menemui sebarang kandungan yang disyaki plagiarisme atau pelanggaran, sila hubungi admin@php.cn