Heim  >  Artikel  >  Backend-Entwicklung  >  Zerlegen langer Methoden und Durchführen eines Refactorings für extrahierten Legacy-Code – Teil 10

Zerlegen langer Methoden und Durchführen eines Refactorings für extrahierten Legacy-Code – Teil 10

PHPz
PHPzOriginal
2023-09-02 12:45:091375Durchsuche

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

Im sechsten Teil unserer Serie haben wir den Angriff auf lange Methoden durch den Einsatz von Paarprogrammierung und die Betrachtung des Codes aus verschiedenen Ebenen besprochen. Wir zoomen ständig hinein und heraus und achten auf Kleinigkeiten wie Benennung, Form und Einrückung.

Heute gehen wir einen anderen Weg: Wir gehen davon aus, dass wir alleine sind und keine Kollegen oder Partner haben, die uns helfen. Wir verwenden eine Technik namens „Extract Until you Drop“, um den Code in sehr kleine Teile zu zerlegen. Wir werden uns bemühen, diese Abschnitte so einfach wie möglich zu verstehen, damit zukünftige Generationen von uns oder andere Programmierer sie leicht verstehen können.


Extrahieren bis zum Umfallen

Ich habe zum ersten Mal von Robert C. Martin von diesem Konzept gehört. In einem seiner Videos schlug er diese Idee als einfache Möglichkeit vor, schwer verständlichen Code umzugestalten.

Die Grundidee besteht darin, kleine, verständliche Codeschnipsel zu extrahieren. Es spielt keine Rolle, ob Sie vier Zeilen oder vier Zeichen identifizieren, die Sie extrahieren können. Wenn Sie Inhalte identifizieren, die in einem klareren Konzept zusammengefasst werden können, können Sie mit der Extraktion fortfahren. Sie setzen diesen Prozess mit der ursprünglichen Methode und den neu extrahierten Fragmenten fort, bis Sie keinen Code mehr finden, der als Konzept gekapselt werden kann.

Diese Technik ist besonders nützlich, wenn Sie alleine arbeiten. Es zwingt Sie dazu, sowohl über kleine als auch große Codeblöcke nachzudenken. Es hat auch noch einen weiteren schönen Effekt: Es regt zum Nachdenken über Code an – und zwar sehr! Zusätzlich zu der oben erwähnten Extraktionsmethode oder dem Variablen-Refactoring müssen Sie auch Variablen, Funktionen, Klassen und mehr umbenennen.

Schauen wir uns ein Beispiel für Zufallscode aus dem Internet an. Stackoverflow ist ein großartiger Ort, um kleine Codefragmente zu finden. So bestimmen Sie, ob eine Zahl eine Primzahl ist:

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

Zu diesem Zeitpunkt habe ich keine Ahnung, wie dieser Code funktioniert. Ich habe es gerade online gefunden, als ich dies schrieb, und ich werde es mit Ihnen entdecken. Der folgende Prozess ist möglicherweise nicht der sauberste. Stattdessen wird es meine Überlegungen und Rekonstruktionen ohne Vorplanung widerspiegeln.

Überarbeiteter Primzahlenprüfer

Laut Wikipedia:

Eine Primzahl (oder Primzahl) ist eine natürliche Zahl größer als 1, die außer 1 und sich selbst keine positiven Faktoren hat. 块引用>

Wie Sie sehen, ist dies eine einfache Möglichkeit, einfache mathematische Probleme zu lösen. Es kehrt zurück truefalse, daher sollte es auch einfach zu testen sein.

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
}

Wenn wir nur Beispielcode verwenden, ist es am einfachsten, alles in eine Testdatei zu packen. Auf diese Weise müssen wir nicht darüber nachdenken, welche Dateien wir erstellen, zu welchem ​​Verzeichnis sie gehören oder wie wir sie in ein anderes Verzeichnis einbinden. Dies ist nur ein einfaches Beispiel, damit wir uns mit der Technik vertraut machen können, bevor wir sie auf eine der Quizspielmethoden anwenden. Also wird alles in einer Testdatei abgelegt und Sie können ihr einen beliebigen Namen geben. Ich habe mich für IsPrimeTest.php entschieden.

Der Test wurde bestanden. Mein nächster Instinkt war, weitere Primzahlen hinzuzufügen, anstatt einen weiteren Test mit Nicht-Primzahlen zu schreiben.

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

Das ist es. Aber was nun?

function testItCanRecognizeNonPrimes() {
    $this->assertFalse(isPrime(6));
}

Das schlägt unerwartet fehl: 6 ist keine Primzahl. Ich erwarte, dass die Methode zurückkehrt false。我不知道该方法是如何工作的,也不知道 $pf 参数的目的 - 我只是希望它根据其名称和描述返回 false . Ich weiß nicht, warum es nicht funktioniert oder wie ich es beheben kann.

Das ist ein ziemlich verwirrendes Dilemma. Was sollen wir tun? Die beste Antwort besteht darin, Tests zu schreiben, die große Zahlen bestehen. Wir müssen es vielleicht versuchen und raten, aber zumindest haben wir eine Vorstellung davon, was diese Methode bewirkt. Dann können wir mit der Rekonstruktion beginnen.

function testFirst20NaturalNumbers() {
    for ($i=1;$i<20;$i++) {
		echo $i . ' - ' . (isPrime($i) ? 'true' : 'false') . "\n";
	}
}

Etwas Interessantes ausgeben:

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

Hier zeichnet sich ein Muster ab. Alles wahr bis 9 Uhr, dann abwechselnd bis 19 Uhr. Aber wird sich dieses Muster wiederholen? Versuchen Sie, 100 Zahlen einzugeben, und Sie werden sofort feststellen, dass dies nicht der Fall ist. Tatsächlich scheint es für Zahlen zwischen 40 und 99 zu funktionieren. Zwischen 30 und 39 schlägt es einmal fehl, wenn 35 als Primzahl angegeben wird. Das Gleiche gilt im Bereich 20-29. 25 gilt als Primzahl.

Diese Übung begann als einfache Code-Demonstration einer Technik, erwies sich jedoch als viel schwieriger als erwartet. Ich habe mich entschieden, es zu behalten, weil es das wirkliche Leben auf typische Weise widerspiegelt.

Wie oft haben Sie mit einer scheinbar einfachen Aufgabe begonnen, nur um festzustellen, dass sie äußerst schwierig ist?

Wir wollen den Code nicht reparieren. Was auch immer die Methode tut, sie sollte dies auch weiterhin tun. Wir hoffen, es umgestalten zu können, damit andere es besser verstehen können.

Da die Primzahlen nicht richtig angegeben werden, verwenden wir dieselbe Golden-Master-Methode, die wir in der ersten Lektion gelernt haben.

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

Führen Sie es einmal aus, um den Golden Master zu generieren. Es sollte schnell laufen. Wenn Sie den Test erneut ausführen müssen, vergessen Sie nicht, die Datei zu löschen, bevor Sie den Test ausführen. Andernfalls wird die Ausgabe an den vorherigen Inhalt angehängt.

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 崩溃。所以这似乎又是一个死胡同。但如果它能发挥作用,那将是对原始逻辑的一个很好的替代。


挑战

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


Abschließende Gedanken

„Extrahieren bis zum Umfallen“ ist eine großartige Möglichkeit, lange Methoden zu analysieren. Es zwingt Sie dazu, über kleine Codeteile nachzudenken und diesen Teilen einen Sinn zu geben, indem Sie sie in Methoden extrahieren. Ich finde es erstaunlich, wie dieser einfache Prozess in Verbindung mit häufigem Umbenennen mir dabei hilft, herauszufinden, dass bestimmter Code Dinge tun kann, die ich nie für möglich gehalten hätte.

In unserem nächsten und letzten Tutorial zum Refactoring werden wir diese Technik auf ein Quizspiel anwenden. Ich hoffe, Ihnen hat dieses etwas andere Tutorial gefallen. Wir reden nicht über Beispiele aus Lehrbüchern, wir verwenden echten Code und müssen uns mit echten Problemen herumschlagen, mit denen wir jeden Tag konfrontiert sind.

Das obige ist der detaillierte Inhalt vonZerlegen langer Methoden und Durchführen eines Refactorings für extrahierten Legacy-Code – Teil 10. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Stellungnahme:
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn