Nine Steps of Learning by Refactoring

The following text is a partial translation of the original English article, performed by ChatGPT (gpt-3.5-turbo) and this Jekyll plugin:

最近在Twitter上,有人问我如果不了解代码,怎么进行重构。我回答说这是“通过重构学习”。然后我尝试在Google上搜索,却什么都没有找到。我感到惊讶。对我来说,重构似乎是研究源代码最有效和明显的方法。下面是我通常按照九个面向对象的步骤进行的。

根据维基百科,代码重构是“在不改变其外部行为的情况下重组现有计算机代码的过程。”重构的目标是使代码更易读,适合进行修改。

Martin Fowler在他的著名书籍《重构:改善现有代码的设计》中提出了一些重构技术,这些技术有助于使代码更简单、更抽象、更易读等。其中一些从面向对象的角度来看是可疑的,比如“封装字段”(Encapsulate Field),但大多数是有效的。

当我不了解代码但需要修改它时,我通常会采取以下做法。这些技术按照复杂度的顺序排列,从最简单的开始。

当我在IntelliJ IDEA中使用我的自定义settings.jar打开Cactoos的源代码时,我看到类似于这样的内容:

当我打开,比如说,Spring Boot的源代码时,我看到类似于这样的内容(这是从成千上万个非常相似的类中随机选择的o.s.b.ImageBanner):

当我看到别人的代码时,我首先要做的是让它在我的IDE中没有”红点”。大多数红点很容易去除,而其他一些则需要一些时间进行重构。在这个过程中,我会对我必须处理的这个糟糕的程序有很多学习收获。

我前段时间写过一篇文章,提到方法体内的空行是不好的。它们是多余复杂性的明显指标。程序员倾向于在方法中添加这些空行以简化事情。

这是Apache Maven代码库中的一个方法(随机选择的RepositoryUtils类,但几乎所有其他类的格式都相同):

除了“全红”外,他们的代码中充满了空行。删除它们将使代码更易读,也有助于我理解它的工作原理。较大的方法将需要重构,因为没有空行,它们将变得几乎无法阅读。因此,我通过将它们拆分为较小的方法来压缩、理解和缩小它们的规模。

我一般倾向于使用一个名词来命名变量,使用一个动词来命名方法。我认为更长的“复合”名称是代码复杂性不必要的指标。

例如,在Spring Boot的o.s.b.w.e.u.UndertowServletWebServerFactory类中找到了这个方法registerServletContainerInitializerToDriveServletContextInitializers(69个字符!)。我想知道作者为什么省略了“couldYouPlease”前缀和“otherwiseThrowAnException”后缀。

开玩笑的,如此长的方法名明显表明代码太复杂,无法用简单的register或甚至registerContainer来解释。似乎有许多不同的容器、初始化器、servlet和其他需要以某种方式注册的实体。当我加入一个项目并看到这个名字的方法时,我已经准备好迎接大麻烦了。

当开始处理外部或遗留代码时,将名称变短是我进行强制重构的步骤。

大多数类(和方法)都没有任何文档,尤其是在谈论闭源商业代码时更是如此。如果这些类的命名相对描述性,并且小而紧密,那我们就算幸运了。

然而,与其依赖文档,我更喜欢使用单元测试。它们能更好地解释代码并证明其有效性。当我不理解一个类如何工作时,我会尝试为其编写一个单元测试。在许多情况下这是不可能的,原因有很多。在这种情况下,我会尝试运用我从Michael Feathers的《与既有代码协作的有效方法》和Steve Freeman与Nat Pryce的《Growing Object-Oriented Software, Guided by Tests》这两本书中了解到的一切。这两本书基本上都专注于这个非常问题:当你不知道该如何进行测试时应该怎么办。

我之前曾写过,单个方法中存在多个return语句并不是面向对象编程应该鼓励的。相反,一个方法必须始终有一个单一的出口点,就像函数式编程中的函数一样。

看一下Spring Boot的o.s.b.c.p.b.Binder类中的这个方法(那里有许多类似的例子,我随机选择了这个):

在这么小的方法中有五个return语句。对于面向对象的代码来说,这太多了。对于过程式代码来说没问题,有时我也会写。例如,我们的这个Groovy脚本也有五个return关键字:

但这是Groovy,并且它不是一个类。它只是一个过程,一个脚本。

重构和删除多个return语句肯定有助于使代码更清晰。主要是因为如果没有它们,就需要使用更深层次的if/then/else语句来嵌套,然后代码开始变得丑陋,除非你将其分解成更小的部分。

NULLs是邪恶的,这是一个众所周知的事实。然而,它们仍然无处不在。例如,在Spring Boot v2.0.0.RELEASE中有4,100个Java文件和243K行代码,其中包括7,055次使用null关键字。这意味着大约每35行代码中就有一个null

相反,我几年前创建的Takes Framework有771个Java文件,154K行代码,和58个null关键字。这大约是每2,700行代码中有一个null。看到区别了吗?

当你删除NULL时,代码会变得更清晰,但这并不容易做到。有时甚至是不可能的。这就是为什么我们在Takes中仍然有那58个null的情况。我们根本无法删除它们,因为它们来自JDK。

如我之前所示,不可变性有助于保持对象更小。我在处理的外部代码中看到的大多数类都是可变的。而且它们通常很大。

如果你查看jpeek分析的任何工件,你会发现其中大约80%的类都是可变的。从可变性转变为不可变性是面向对象编程中的一个巨大挑战,如果解决了这个问题,会导致更好的代码。

使事物变为不可变的这个重构步骤是完全有益的。

静态方法和属性对于过程式程序员来说很方便。如果你的代码是面向对象的,它们必须消失。在Spring Boot中有7,482个static关键字,这意味着每32行代码中有一个。相反,在Takes中我们有310个static,即每496行代码中有一个。

将这些数字与NULL的统计数据进行比较,你会发现去除static是一个更复杂的任务。

这是最后一步,也是最复杂的一步。它很复杂,因为我将静态分析器配置到了最大的潜力,甚至更多。我正在使用Qulice,它是CheckstylePMDFindBugs的聚合器。这些工具本身就很强大,但是Qulice使它们更加强大,添加了几十个自定义检查项。

我使用的静态分析原则是0/100。这意味着要么整个代码库是干净的,没有Qulice的投诉,要么就是有问题的。没有中间状态。这不是一个非常典型的静态分析方式。大多数程序员只是使用这些工具来收集有关他们代码的“意见”。我把它们作为重构的指南。

观看这个视频,它展示了在Spring Boot的spring-boot-project/spring-boot子模块中,Qulice给出的投诉数量(视频没有结束,因为我等得不耐烦了):

当Qulice表示一切都是干净的时候,我认为代码库已经完全准备好进行维护和修改。在这一点上,重构已经完成。

Translated by ChatGPT gpt-3.5-turbo/42 on 2024-01-09 at 18:17

sixnines availability badge   GitHub stars