>백엔드 개발 >PHP 튜토리얼 >긴 메소드 분석 및 추출된 레거시 코드 리팩토링 수행 - 10부

긴 메소드 분석 및 추출된 레거시 코드 리팩토링 수행 - 10부

PHPz
PHPz원래의
2023-09-02 12:45:091487검색

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

시리즈 6부에서는 쌍 프로그래밍을 활용하고 다양한 수준에서 코드를 확인하여 긴 방법을 공격하는 방법에 대해 논의했습니다. 우리는 명명, 형태, 들여쓰기와 같은 작은 것들을 계속해서 확대하고 축소하고 있습니다.

오늘 우리는 또 다른 접근 방식을 취하겠습니다. 우리는 혼자이고 우리를 도와줄 동료나 파트너가 없다고 가정하겠습니다. 코드를 매우 작은 조각으로 나누기 위해 "삭제할 때까지 추출"이라는 기술을 사용할 것입니다. 우리는 미래 세대나 다른 프로그래머가 쉽게 이해할 수 있도록 이 섹션을 가능한 한 이해하기 쉽게 만들기 위해 모든 노력을 기울일 것입니다.


떨어질 때까지 추출

저는 이 개념에 대해 Robert C. Martin에게서 처음 들었습니다. 그는 이해하기 어려운 코드를 리팩토링하는 간단한 방법으로 자신의 비디오 중 하나에서 이 아이디어를 제안했습니다.

기본 아이디어는 작고 이해하기 쉬운 코드 조각을 추출하는 것입니다. 추출할 수 있는 4줄 또는 4문자를 식별하는지 여부는 중요하지 않습니다. 보다 명확한 개념으로 압축할 수 있는 콘텐츠를 식별하면 추출을 진행할 수 있습니다. 개념으로 캡슐화할 수 있는 코드 조각을 찾을 수 없을 때까지 원래 메서드와 새로 추출된 조각에 대해 이 프로세스를 계속합니다.

이 기술은 혼자 일할 때 특히 유용합니다. 이는 작은 코드 청크와 큰 코드 청크를 모두 생각하게 만듭니다. 또 다른 좋은 효과도 있습니다. 코드에 대해 많이 생각하게 만듭니다! 위에서 언급한 추출 방법이나 변수 리팩토링 외에도 변수, 함수, 클래스 등의 이름을 바꾸는 방법도 있습니다.

인터넷에서 무작위 코드의 예를 살펴보겠습니다. Stackoverflow는 작은 코드 조각을 찾기에 좋은 장소입니다. 숫자가 소수인지 확인하는 방법은 다음과 같습니다.

으아아아

이 시점에서는 이 코드가 어떻게 작동하는지 전혀 모릅니다. 이 글을 쓰던 중 방금 온라인에서 발견했는데, 여러분과 함께 찾아보겠습니다. 다음 프로세스는 가장 깨끗하지 않을 수 있습니다. 대신, 사전 계획 없이 나의 추론과 재구성을 반영할 것입니다.

리팩터링된 소수 검사기

위키피디아에 따르면:

소수(또는 소수)는 1과 자기 자신 외에 양의 인수가 없는 1보다 큰 자연수입니다. 块引用>

보시다시피 이것은 간단한 수학 문제를 푸는 쉬운 방법입니다. truefalse가 반환되므로 테스트하기도 쉬울 것입니다.

으아아아

샘플 코드만 사용할 때 가장 쉬운 방법은 모든 것을 테스트 파일에 넣는 것입니다. 이렇게 하면 어떤 파일을 생성할지, 어떤 디렉터리에 속할지, 다른 디렉터리에 어떻게 포함할지 고민할 필요가 없습니다. 이것은 퀴즈 게임 방법 중 하나에 적용하기 전에 기술에 익숙해질 수 있도록 간단한 예일 뿐입니다. 따라서 모든 것이 테스트 파일에 저장되며 원하는 대로 이름을 지정할 수 있습니다. 저는 IsPrimeTest.php를 선택했습니다.

테스트가 통과되었습니다. 나의 다음 본능은 소수가 아닌 다른 테스트를 작성하는 대신 더 많은 소수를 추가하는 것이었습니다.

으아아아

그렇습니다. 하지만 그래서 어쩌죠?

으아아아

예기치 않게 실패합니다. 6은 소수가 아닙니다. 메소드 복귀를 기대하고 있어요false。我不知道该方法是如何工作的,也不知道 $pf 参数的目的 - 我只是希望它根据其名称和描述返回 false . 왜 작동하지 않는지, 어떻게 해결하는지 모르겠습니다.

이것은 다소 혼란스러운 딜레마입니다. 우리는 무엇을 해야 합니까? 가장 좋은 대답은 큰 숫자를 통과하는 테스트를 작성하는 것입니다. 우리는 시도하고 추측해야 할 수도 있지만 적어도 이 방법이 무엇을 하는지에 대한 아이디어를 갖게 될 것입니다. 그런 다음 재구성을 시작할 수 있습니다.

으아아아

흥미로운 내용 출력:

으아아아

여기서 패턴이 나타나기 시작합니다. 9까지는 모두 참이고 19까지는 번갈아 나타납니다. 그런데 이 패턴이 반복될까요? 100개의 숫자를 실행해 보면 그렇지 않다는 것을 즉시 알 수 있습니다. 실제로 40에서 99 사이의 숫자에서 작동하는 것 같습니다. 30-39 사이에서는 35를 소수로 지정하면 한 번 실패합니다. 20~29대에서도 마찬가지다. 25는 소수로 간주됩니다.

이 연습은 기술을 시연하기 위해 간단한 코드로 시작했지만 예상보다 훨씬 어려웠습니다. 전형적인 방식으로 실제 생활을 반영하기 때문에 유지하기로 결정했습니다.

겉으로는 간단해 보이는 일을 시작했지만 극도로 어려워진 적이 몇 번이나 있었나요?

우리는 코드를 수정하고 싶지 않습니다. 어떤 방법을 사용하든 계속 그렇게 해야 합니다. 우리는 다른 사람들이 더 잘 이해할 수 있도록 리팩터링하기를 희망합니다.

소수를 제대로 알려주지 않기 때문에 첫 번째 강의에서 배웠던 골든마스터 방식을 그대로 사용하겠습니다.

으아아아

한번 실행하면 골든마스터가 생성됩니다. 빠르게 실행되어야 합니다. 다시 실행해야 하는 경우 테스트를 실행하기 전에 파일을 삭제하는 것을 잊지 마세요. 그렇지 않으면 출력이 이전 콘텐츠에 추가됩니다.

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 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.