重构遗留代码(10):剖析长方法

旧代码,丑陋的代码,复杂的代码,意大利面条似的代码,鬼话废话……就是四个字:遗留代码。这是一个系列文章,将有助于你处理并解决它。

在我们本系列的第六部分,我们谈到了通过结对编程和从不同层次看代码来处理长方法。我们连续地放大与缩小,既观察了命名,也观察了表单和缩进这样细小的方面。

今天,我们将继续另一个方法:我们将假设我们是孤独的,没有同事或者结对者帮助我们。我们将使用一种称为“提取直到你放弃”的技术,将代码分割成非常小块。我们将尽一切努力使得这些代码块尽可能得容易理解,以便将来我们,或者任何其他的程序员能够很容易得理解。

 

提取直到你放弃

我第一次听说这个概念是从Robert C. Martin那儿。在他的一段视频中表述了这个概念,作为重构很难理解的代码的一种简单方式。

基本思想是选取小段的,能够理解的代码片段然后提取它们。你确定了能被提取的四行或者四个字符是没关系的。当你确定了那些能被封装成比较清晰的抽象概念时,你就提取。在原方法侧和新提取的片段侧你持续这个过程,直到你发现没有代码块能被封装。

这个技术在你独自工作时是特别有用的。它迫使你去思考小段代码和更大段的代码。它还有另一个精妙的作用:它让你思考代码-很多!除了上面提到的提取方法或变量重构,你将发现自己在重命名变量,函数,类和更多。

让我们看个来自互联网的一些随机代码的例子。Stackoverflow是个发现小段代码的好地方。这是一个确定数字是否为素数的例子。

此刻,我不知道这段代码是如何工作的。我只是在写这篇文章的时候在互联网上找到了它,并且我将和你一起发现它。接下来的过程可能不是最清晰的。相反,当它没有事先计划得发生时,它将反映我的推理和重构。

 

重构素数检查

根据Wikipedia:素数是比1大并且除了1和它本身外没有正约数的自然数。

正如你看到的,这是个关于简单的数学问题的简单的方法。它将返回true或者false,所以它也该容易测试。

当我们只处理示例代码时,进行下去最简单的方式是将所有东西放入测试文件。这种方式,我们不必考虑创建什么文件,它们所属的方向,或者怎么将它们包含在其他文件中。这只是个简单的例子用来为我们自己熟悉之前我们在问答游戏方法之一上所采用的技术。所以,所有都放入测试文件,你可以起你希望的名字。我已经选择了IsPrimeTest.php.

测试通过了。我下一个直觉是增加更多些素数然后写另一个没有素数的测试。

这个通过了。但下面这个怎么样?

这不出人意料:6不是素数。我期望方法返回false。我不知道方法如何工作的,或者$pf参数的目的-基于它的名字和描述,我只是希望它返回false。我不知道它为什么没起作用或者如何修正它。

这是个相当令人费解的难题。我们该怎么做?最佳答案是写测试程序传递大量数字。我们可能需要试试并且猜测,但至少我们将对方法做了什么有些概念。然后我们可以开始重构它了。

输出了写有趣的内容:

在这儿开始出现了一种模式。直到9都为true,之后直到19,true和false交替出现。但这个模式是重复的吗?试着运行100个数字,你将立刻发现它不是的。它实际上似乎对在40到99之间的数字起作用。在30-39之间它失误了一次,把35作为了素数。同样在20-29范围内也有个true,25被认为是素数。

这个练习开始作为简单代码来演示一项技术证明是比预期难得多。我决定依然保持这个练习,因为它以典型的方式反映了真实的生活。

有多少次你开始一个看起来简单只是发现它是十分困难的任务?

我们不想修正代码。无论方法做了什么,它应该继续去做。我们想要重构它以使得其他人更好地理解。

因为它没有以正确的方式打印素数,我们将使用在教程1中学到的金牌大师方法

运行一次这个程序生成金牌大师。它运行起来应该很快。如果你需要重新运行它,别忘了在你执行测试程序之前删除文件。否则输出将被附加到之前的内容。

现在为金牌大师写测试程序。这个解决方案可能不是最快的,但它容易理解并且它将准确地告诉我们如果破坏了什么那个数字不能匹配。但在两个测试程序中有个可以将其提取到一个私有方法中的小段重复。

现在我们可以继续看产品代码了。在我的电脑上,测试运行了大约2秒,所以它是可控的。

 

尽我所能提取

首先我们可以在代码的第一部分提取一个isDivisible()方法。

这将使我们在第二部分像这样重用这段代码:

一旦我们开始处理这段代码,我们能看到它粗心大意的对齐方式。大括号有时在行首,有时在末尾。有时,tabs被用来做缩进,有时做为空格。有时在操作数和操作符之间有空格,有时没有。不,这不是特别制作的代码。这是真实的生活。真实的代码,而不是一些人造的练习。

这看起来好多了。立即地,两个if表达式看起来很熟悉。但我们不能提取它们,因为return表达式。如果我们不返回我们将打破逻辑。

如果提取的方法会返回个布尔值并且我们比较它来决定是否应该或者不该从isPrime()返回,那将完全没帮助。也许有种方式通过某些在PHP中的函数式编程概念来提取它,但也许稍晚些。首先我们可以做些更简单的事。

提取整个for循环更容易些,但当我们试图在第二部分的if中重用我们提取的方法时,我们看到它不起作用了。有这个我们几乎一无所知的$pf变量存在。

它似乎通过一组特定的因子来检查数字是否可整除,而不是把所有数字放入由intval(sqrt($num))决定的另一个神奇的值中。也许我们可以将$pf重命名为$divisors。

这是做到这点的方法之一。我们增加了第四个,可选的参数到检查方法中。如果它有值,我们就用它,否则我们就用$i。

我们还能提取其他什么吗?这段代码怎么样:intval(sqrt($num))?

这不是更好吗?有点。如果后来的人不知道intval()和sqrt()在做什么,这样就更好点,但这样并没有是的逻辑更容易理解。为什么我们在特定数字上结束for循环?也许这是函数名应该回答的问题。

这更好了,因为它解释了为什么我们停在这儿。也许将来我们会发明一个不同的公式来决定那个数字。命名也引入了一点不一致。我们调用数字因素,就是除数的同义词。也许我们应该选择一个并且只使用它。我将让你把重命名重构作为练习。

问题是,我们能进一步提取吗?好了,我们必须试试直到我们放弃。在几段之前我提到PHP中的函数式编程。在PHP中有两个我们能很容易得应用的主要的函数式编程特性:第一类函数与递归。无论什么时候我看到在for循环中带return的if表达式,就像checkDivisorsBetween()方法,我想着适用一个或两个技术都用

但为什么我们要通过这样一个复杂的思维过程?最烦人的原因是这个方法有两个不同的东西:它循环了,并且它确定结果了。我想要它值循环而将确定结果留给另一个方法。一个方法始终应该只做一件事并且做好它。

我们第一个尝试是提取条件和return表达式到一个变量中。目前来说,这是本地的。但代码不工作了。事实上,for循环把事情弄得有点复杂。我有种感觉,一小段递归会有帮助。

当我们考虑递归的时候我们必须开始考虑异常情况。我们的第一个异常是当到达递归的末尾时。

我们第二个会打断递归的例外情况是当数字是可整除的时,我们不希望继续。这就是所有的例外情况。

这是对我们的问题做的另一种使用递归的尝试,但不幸的是,在PHP中递归10.000次将导致我系统中PHP或者PHPUnit崩溃。所以这看起来是另一个死胡同。但如果它可以一直工作,本是对原逻辑一个很好的替换。

 

挑战

当我写金牌大师测试程序的时候,我刻意得忽略了些东西。让我们只说是测试程序没有尽可能得覆盖它们应该覆盖的代码。你能发现这个问题吗?如果能,你该怎么处理它?

 

最后的思考

“提取直到你放弃”是一个解析长方法的好方式。它迫使你考虑小段代码并且通过将它们提取到方法中来给予那段代码以目的。这一伴随着频繁重命名的简单程序,如何帮助我发现某些代码做了我认为不可能的事,这本身是令我惊异的。

在我们的下一篇也是最后一篇关于重构的教程中,我们将对问答游戏使用这种技术。我希望你喜欢这个原来是有一点不同的教程。不是谈论教科书的例子,我们处理了一些真实的代码并且我们必须与每天面对的真实问题战斗。

收藏 评论

关于作者:EluQ

南京土著,程序员。(新浪微博:@EluQ) 个人主页 · 我的文章 · 11

相关文章

可能感兴趣的话题



直接登录
跳到底部
返回顶部