ホームページ  >  記事  >  バックエンド開発  >  長いメソッドを分析し、抽出されたレガシー コードのリファクタリングを実行する - パート 10

長いメソッドを分析し、抽出されたレガシー コードのリファクタリングを実行する - パート 10

PHPz
PHPzオリジナル
2023-09-02 12:45:091469ブラウズ

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

シリーズのパート 6 では、ペア プログラミングを活用し、さまざまなレベルからコードを表示することによる、長いメソッドの攻撃について説明しました。私たちは常にズームインしたりズームアウトしたりして、名前、形式、インデントなどの小さな点に注目します。

今日は別のアプローチをとります。私たちは孤独で、助けてくれる同僚やパートナーがいないと仮定します。 「削除するまで抽出」と呼ばれる手法を使用して、コードを非常に小さな部分に分割します。私たちは、将来の世代の私たちや他のプログラマーがこれらのセクションを簡単に理解できるように、これらのセクションをできるだけ理解しやすくするためにあらゆる努力を払っていきます。


諦めるまで抽出する

私がこのコンセプトについて初めて聞いたのは、ロバート C. マーティン氏でした。彼は、理解しにくいコードをリファクタリングする簡単な方法として、このアイデアをビデオの 1 つで提案しました。

基本的な考え方は、小さくてわかりやすいコード スニペットを取り出して抽出することです。抽出できる行が 4 行であるか、文字が 4 文字であるかは問題ではありません。より明確な概念でカプセル化できるコンテンツを特定したら、抽出に進むことができます。概念としてカプセル化できるコード部分が見つからなくなるまで、元のメソッドと新しく抽出されたフラグメントに対してこのプロセスを続けます。

このテクニックは、一人で作業している場合に特に役立ちます。コードの小さな部分と大きな部分の両方について考える必要があります。これにはもう 1 つの素晴らしい効果もあります。コードについてよく考えるようになります。前述の抽出方法や変数リファクタリングに加えて、変数、関数、クラスなどの名前を変更することもあります。

インターネットからのランダム コードの例を見てみましょう。 Stackoverflow は、小さなコード スニペットを見つけるのに最適な場所です。数値が素数かどうかを判断する方法は次のとおりです:

リーリー

現時点では、このコードがどのように機能するのかわかりません。これを書いているときにオンラインで見つけたので、一緒に見つけていきます。この後のプロセスは、最もクリーンなものではない可能性があります。代わりに、事前計画なしで私の推論と再構成が反映されます。

素数チェッカーのリファクタリング

ウィキペディアによると:

素数 (または素数) は、1 とそれ自体以外に正の因数を持たない、1 より大きい自然数です。 块引用>

ご覧のとおり、これは単純な数学の問題を解決する簡単な方法です。 true または false を返すため、テストも簡単です。

リーリー

サンプル コードを使用するだけの場合、最も簡単な方法は、すべてをテスト ファイルに入れることです。こうすることで、どのファイルを作成するか、それらがどのディレクトリに属する​​か、またはそれらを別のディレクトリに含める方法について考える必要がなくなります。これは、トリビア ゲームの手法の 1 つに適用する前に、この手法に慣れるための単純な例です。したがって、すべてがテスト ファイルに入れられ、任意の名前を付けることができます。私は IsPrimeTest.php を選択しました。

テストは成功しました。次に直感的に思ったのは、素数以外を使用して別のテストを作成する代わりに、さらに素数を追加することでした。

リーリー

それはちょうど過ぎました。でも、だから何?

リーリー

これは予期せず失敗します。6 は素数ではありません。メソッドが false を返すことを期待しています。このメソッドがどのように機能するのか、$pf パラメーターの目的もわかりません。名前と説明に基づいて false を返したいだけです。なぜ機能しないのか、どうすれば修正できるのかわかりません。

これはかなりややこしいジレンマです。私たちは何をすべきか?最善の答えは、多数の値を渡すテストを作成することです。試して推測する必要があるかもしれませんが、少なくともこのメソッドが何をするのかについてはある程度理解できるでしょう。その後、再構築を開始できます。

リーリー

何か興味深いものを出力します:

リーリー

ここでパターンが現れ始めています。 9 まではすべて true、その後は 19 まで交互になります。しかし、このパターンは繰り返されるのでしょうか? 100 個の数字を実行してみると、そうではないことがすぐにわかります。実際には、40 から 99 までの数値で機能するようです。 30 ~ 39 の間では、素数として 35 を指定すると 1 回失敗します。 20~29の範囲でも同様です。 25 は素数とみなされます。

この演習は、テクニックの簡単なコードのデモンストレーションとして開始されましたが、予想よりもはるかに難しいことが判明しました。典型的な方法で現実の生活を反映しているため、これを保持することにしました。

一見簡単そうに見えるタスクを始めたものの、非常に難しいと感じたことは何度ありますか?

コードを修正するつもりはありません。どのような方法であっても、それを継続する必要があります。他の人がよりよく理解できるように、それをリファクタリングしたいと考えています。

素数を正しい方法で伝えることができないため、最初のレッスンで学んだのと同じゴールデン マスター メソッドを使用します。

リーリー

これを 1 回実行して、ゴールデン マスターを生成します。高速に実行できるはずです。再実行する必要がある場合は、テストを実行する前にファイルを削除することを忘れないでください。それ以外の場合、出力は前のコンテンツに追加されます。

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 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。