重构简介
最后修改时间:2023 年 9 月 1 日IntelliJ IDEA 提供了很多自动重构功能,但作为开发人员,仅仅知道如何执行它们是不够的,我们需要了解这些重构是什么、何时应用它们以及任何可能的缺点或之前需要考虑的事项使用它们。
正如 Martin Fowler 所定义的,重构“……是一种重构现有代码体、改变其内部结构而不改变其外部行为的严格技术”。因此,在生产代码中执行任何重构之前,重要的是要有全面的测试覆盖率,以证明您没有无意中更改行为。
本教程的目标是向那些对重构(尤其是自动重构)概念不熟悉的人介绍 IntelliJ IDEA 的功能,并展示您何时可能想要应用三种基本类型的重构:重命名、提取和删除。
重命名
重命名可能看起来像是一个微不足道的重构,但通过简单的查找和替换进行重命名通常意味着具有相同名称的不相关项目被无意中更改。使用 IntelliJ IDEA 的重命名重构可以最大限度地减少这些错误。
为什么要重命名?
重命名是提高代码可读性可以做的最简单的事情之一。当类、方法或变量名称与它看起来的作用不匹配时,可能会导致很多混乱。您可能想要重命名某些内容的一些原因:
这个名字不够描述性。
类/方法/变量名称与实际名称不匹配。
引入了一些新内容,要求现有代码具有更具体的名称。
在编写代码时重命名
想象一下,您在实现某些功能或修复某些错误时遇到以下代码。
server = new Server(path, port, endpoint);
server.init();
server.run();
假设我们想要:
重命名
endpoint
一个字段,以描述它是什么类型的端点。init()
将上的方法重命名Server
为更准确地描述该方法的名称。将一个类重命名
Server
为更具体的名称。
要重命名该
endpoint
字段,请将插入符号放在单词端点处,然后按。IntelliJ IDEA 将根据类名称和其他方面弹出建议列表。在这种情况下,还会建议使用该字段的参数名称。ShiftF6选择这些选项之一或键入您自己的选项。如果该字段有 getter,IntelliJ IDEA 会询问您是否也想重命名它。
您会注意到该字段的所有使用都更改为新名称,并且如果您选择重命名 getter,项目中的其他类将更新为使用新名称。有关方法重命名的更多信息,请参阅下一步。
要重命名该方法,过程是相同的:将插入符号放在
init
并按。此处的建议会减少,因此请输入新名称:ShiftF6除了重命名该方法之外,这还会重命名该方法的所有调用以及子类中所有重写/实现的方法。IntelliJ IDEA 还可以重命名该名称的非代码使用,如果您有 XML 配置或引用类或方法的其他非 Java 文件,这非常有用。您可以配置如果您再次按下以弹出重命名对话框,则重命名的内容ShiftF6
如果重命名不仅仅适用于代码,IntelliJ IDEA 将为您预览重构,以便您可以选择要进行的更改。通常在这些情况下,您可能会选择不重命名注释中出现的事件,特别是如果原始方法名称是诸如 之类的常见单词
name
。如果您不想进行其中一些更改,请按您不想更改的用法。Backspace
重命名类类似,但也可以通过“项目”工具窗口执行。在本例中,因为我们发现要重命名使用它的类,所以我们将在代码中使用类名。ShiftF6
当然,使用此类的任何代码也将被重命名,但您也可以选择重命名变量、继承者和代码的其他部分,以便它们与新名称保持一致。同样,可以通过再次按下来设置这些选项。ShiftF6
更名的影响
重命名局部变量或私有方法可以相当安全地即时完成。例如,当您正在开发涉及此代码区域的功能时,您可以执行此重构,但知道影响范围有限。
重命名类或公共方法可能会影响许多文件。如果是这种情况,这种重构至少应该在其自己的单独提交中进行,以便将更改与您当时可能正在处理的任何更改或附加功能明确分开。
提取
当发现当前设计(无论是小规模还是大规模)不再适合目标时,IntelliJ IDEA 的提取重构使开发人员能够重塑代码。
提取变量
提取变量是一种影响较小的更改,可以使您的代码实现自我记录。它还可以用于减少代码重复。
假设您遇到以下代码
static String getUsernameFromMessage(String message) {
return message.substring(message.indexOf("\"screen_name\":\"") + 15,
message.indexOf("\"", message.indexOf("\"screen_name\":\"") + 15));
}
删除重复的
message.indexOf("\"screen_name\":\"") + 15)
引入变量来描述每个
indexOf
调用代表的内容删除神奇的数字 15
首先,让我们减少重复并引入一个变量来描述此操作正在执行的操作。将插入符号放在表达式中的任意位置
message.indexOf("\"screen_name\":\"") + 15)
并按。IntelliJ IDEA 将建议此重构的上下文,您需要选择封装此表达式的上下文:CtrlAlt0V接下来,如果 IntelliJ IDEA 检测到该表达式多次出现,您可以选择替换所有出现的表达式或仅替换您选择的表达式。
提取变量后,IntelliJ IDEA 会根据表达式所使用的参数等内容建议可能的名称。
我们将使用我们自己的名字
indexOfFieldValue
来描述它真正代表的含义。请注意,您可以决定是否希望此变量为最终变量。接下来我们将为字符串值引入一个变量。这样做有两个原因:首先,记录 String 值代表什么,其次因为它将帮助我们删除幻数。
将插入符号放在某处
screen_name
并按。CtrlAlt0V我们将给它一个更有意义的名字,
fieldName
。现在,我们将
substring()
使用相同的过程为用作 的参数的另一个表达式创建一个变量,我们将其称为indexOfEndOfFieldValue
。最后,我们可以删除幻数,因为这只是字段名称的长度。最终代码如下所示:
static String getUsernameFromMessage(String message) { final String fieldName = "\"screen_name\":\""; final int indexOfFieldValue = message.indexOf(fieldName) + fieldName.length(); final int indexOfEndOfFieldValue = message.indexOf("\"", indexOfFieldValue); return message.substring(indexOfFieldValue, indexOfEndOfFieldValue); }
它比原来的更长,但更具描述性,这在这样的代码中尤其重要,因为不清楚每个表达式代表什么。选择
final
是否申请由您决定,并且取决于您的编码标准。
提取参数
提取或添加参数允许开发人员更改方法,使其更易于使用。您可能希望更改参数,例如,通过传入对象中的一些值而不是对象本身,或者您可能希望引入方法体中的值作为参数,以允许该方法在更多地方使用。我们将看一个后者的例子。
对于此示例,我们将在重构后使用与上一个示例相同的代码,并稍微扩展它以显示同一类中的另一个方法:
static String getTextFromMessage(String message) {
final String fieldName = "\"text\":\"";
final int indexOfFieldValue = message.indexOf(fieldName) + fieldName.length();
final int indexOfEndOfFieldValue = message.indexOf("\"", indexOfFieldValue);
return message.substring(indexOfFieldValue, indexOfEndOfFieldValue);
}
static String getUsernameFromMessage(String message) {
final String fieldName = "\"screen_name\":\"";
final int indexOfFieldValue = message.indexOf(fieldName) + fieldName.length();
final int indexOfEndOfFieldValue = message.indexOf("\"", indexOfFieldValue);
return message.substring(indexOfFieldValue, indexOfEndOfFieldValue);
}
我们的目标是消除在这两种方法中看到的重复代码。为此,我们将:
改成
fieldName
一个参数,这样我们就可以让这个getUsernameFromMessage
方法适用于任何领域。重命名
getUsernameFromMessage
为代表其更一般性质的名称删除 中的重复代码
getTextFromMessage
。
将插入符置于
fieldName
并按CtrlAlt0P与其他重构一样,如果您愿意,可以为参数键入新名称。IntelliJ IDEA 还预览了更新的方法签名。按批准更改。Enter
这个特定问题告诉我们该方法被用作方法引用,并且此更改将导致方法引用转换为 lambda 表达式。此消息可能表明这不是您希望执行的重构。如果是这种情况,下一个示例展示了我们可以使用提取方法采取的方法。但是,对于此示例,我们假设我们对引入新参数的结果感到满意,因此我们只需选择“继续”。
接下来,IntelliJ IDEA 将检测现在可以通过调用新方法签名来替换的任何代码。
如果您在这种情况下选择“替换”,所有重复的代码都将被替换,并且 IntelliJ IDEA 将为新参数选择适当的值。
此时,原始方法
getUsernameFromMessage
比原来更通用,因此我们应该重命名它。我们将插入符号放在 name 和 use 处,如上一节所示。ShiftF6我们可以进一步简化代码。内联是提取的逆过程,在我们这里的代码中,内联我们的临时变量可能是合适的,因为变量名给我们带来的只是将值直接传递到方法中。或者,鉴于 getTextFromMessage 实际上是对 的简单委托
getValueForField
,我们可以使用 inline 完全删除此方法。要内联,请将插入符号放在
getTextFromMessage
变量处,然后按CtrlAlt0N现在我们的最终代码如下所示:
static String getValueForField(String message, String fieldName) { final int indexOfFieldValue = message.indexOf(fieldName) + fieldName.length(); final int indexOfEndOfFieldValue = message.indexOf("\"", indexOfFieldValue); return message.substring(indexOfFieldValue, indexOfEndOfFieldValue); }
我们调用原始 getUsernameFromMessage 方法的代码是:
Parser::getUsernameFromMessage
现在是
(message) -> Parser.getValueForField(message, "\"screen_name\":\"")
我们调用原始 getTextFromMessage 方法的代码是:
String[] wordsInMessage = Parser.getTextFromMessage(message).split("\\s");
现在是
String[] wordsInMessage = Parser.getValueForField(message, "\"text\":\"").split("\\s");
请注意,我们应用此重构的方式会强制所有调用者传入字段名称,并且 a) 在代码中广泛使用字符串值,b) 可能会引入一个或多个字符串值的重复。这可能适合您的代码,特别是在处理字符串重复或不经常使用该方法的情况下。但是,如果这不是您希望减少代码重复的权衡,请参阅下一章以了解替代方法。
提取参数非常强大,因此值得阅读更详细的帮助页面。
提取方法
提高代码可读性的一种方法是将其分成小的、易于理解的部分。Extract 方法允许开发人员做到这一点,在适当的时候将代码段移动到他们自己的、描述性命名的方法中。
一些开发人员可能会发现自己编写了很长的方法来执行他们想要的操作,当他们完成(并测试)功能时,查看代码以了解可以在哪里重构和简化并分解这些较长的方法。或者,当开发人员在实现新功能时遇到代码时,他们意识到将一些代码提取到自己的方法中可以让他们重用现有功能。
提示
当 IntelliJ IDEA 检测到重复代码时,这是创建一个可以由所有具有重复代码的位置调用的新方法的非常好的候选者。
我们将看与上一节中相同的示例,但采用与之前略有不同的方法。
static String getTextFromMessage(String message) {
final String fieldName = "\"text\":\"";
final int indexOfFieldValue = message.indexOf(fieldName) + fieldName.length();
final int indexOfEndOfFieldValue = message.indexOf("\"", indexOfFieldValue);
return message.substring(indexOfFieldValue, indexOfEndOfFieldValue);
}
static String getUsernameFromMessage(String message) {
final String fieldName = "\"screen_name\":\"";
final int indexOfFieldValue = message.indexOf(fieldName) + fieldName.length();
final int indexOfEndOfFieldValue = message.indexOf("\"", indexOfFieldValue);
return message.substring(indexOfFieldValue, indexOfEndOfFieldValue);
}
正如我们之前看到的,之前的重构需要进行一些权衡:方法引用需要转换为 lambda 表达式,并且所有调用代码都需要知道所需的字段名称。我们可以选择以不同的方式去除两种方法之间的代码重复:
将公共代码提取到自己的方法中。
内联变量以简化其余代码。
首先,突出显示两种方法之间通用的代码:
按下将弹出提取方法对话框。CtrlAlt0M
输入新方法的名称 ,
getValueForField
并检查参数名称和顺序。在本例中,我们将交换参数的顺序,因为我们希望参数fieldName
更接近方法的名称。这将取决于您的代码风格和团队偏好,您可能想大声朗读名称和参数,看看它作为自然语言的语句是否有意义。当您按下OK时,IntelliJ IDEA 将检测可以通过调用此新方法来替换的代码,并提供重构功能。我们将选择Yes。
此时,我们的
getTextFromMessage
和方法是两行简单的代码,这里内联fieldName 变量getUsernameFromMessage
是有意义的,因为方法名称具有足够的描述性,可以删除临时变量。按并选择重构。CtrlAlt0NfieldName
作为最后一步,您可能希望将所有类似的方法组合在一起。根据您的设置,IntelliJ IDEA 可能已将新方法直接放置在您选择提取方法时所在的方法下,就像我们这里的例子一样。要将辅助方法放在一起,请将脱字符号放在
getValueForField
方法名称处,然后按。这会将您的新方法 放置在现有的 getUsernameFromMessage 方法下。CtrlShift0↓getValueForField
我们的最终代码如下所示:
static String getTextFromMessage(String message) { return getValueForField("\"text\":\"", message); } static String getUsernameFromMessage(String message) { return getValueForField("\"screen_name\":\"", message); } static String getValueForField(String fieldName, String message) { final int indexOfFieldValue = message.indexOf(fieldName) + fieldName.length(); final int indexOfEndOfFieldValue = message.indexOf("\"", indexOfFieldValue); return message.substring(indexOfFieldValue, indexOfEndOfFieldValue); }
现在我们有两个非常具体的帮助器方法,用于获取消息正文和用户名,以及一个更通用的方法,可用于从消息中获取任何字段的值。当存在经常需要的其他字段时,可以添加其他辅助方法。
请注意,提取参数和提取方法示例以相同的代码开始,但以看起来非常不同的代码结束。这不仅是因为我们使用了不同的重构,还因为我们做出了不同的决定 - 在第一个示例中,我们选择完全删除重复,并将一些决策移至方法的调用者中。在第二个示例中,我们选择提供一个 API,它将字段名称的详细信息隐藏在小帮助器方法后面,但仍然提供更通用的方法。我们还可以混合和匹配这些方法,我们选择开始的重构可能会引导我们走向特定的方向,但我们可以决定我们的最终目的地。我们应该记住重构的目标(在这种情况下,减少重复)并理解当我们选择一个方向而不是另一个方向时我们所做的权衡,例如决定是否要调用代码来知道他们要求哪个字段名称。
提取的影响
好消息是您可以相当轻松地撤消提取。当然,不仅通过选择,还通过内联创建的方法,以便代码回到原来的位置。Ctrl0Z
我们在这里提到的提取重构被经验丰富的开发人员定期使用,以随着代码的发展来塑造代码,并且每次接触代码时或多或少地使用它们并不少见。一些没有涉及到的,比如extract interface和extract superclass,可能会对整体设计产生更广泛的影响,应该更加小心。
正在删除
有时,当您分几个步骤重构代码时,最终可能会得到不再使用的代码,或者理想情况下不应使用的代码。由于重构的目标是简化,因此您应该始终致力于尽可能删除未使用的代码 - 无论未使用的代码对应用程序的性能有任何影响(或不影响),未使用的代码肯定会对使用和尝试的开发人员造成影响了解应用程序。
安全删除
IntelliJ IDEA 允许您安全删除未使用的代码片段或整个文件,通知您是否可以安全删除代码,并让您可以选择在进行更改之前预览更改。识别和处理未使用代码的最快方法是确保启用相关检查,通常默认情况下:
让我们继续之前重构的例子。假设我们最终得到了这段代码:
static String getUsernameFromMessage(String message) {
return getValueForField("\"screen_name\":\"", message);
}
static String getValueForField(String fieldName, String message) {
final int indexOfFieldValue = message.indexOf(fieldName) + fieldName.length();
final int indexOfEndOfFieldValue = message.indexOf("\"", indexOfFieldValue);
return message.substring(indexOfFieldValue, indexOfEndOfFieldValue);
}
有可能一段时间后,当我们回到这段代码时,该getUsernameFromMessage
方法不再使用——也许不再需要它,或者也许人们愿意使用getValueForField
相关参数进行调用。因此,假设我们对这些原因感到满意,我们可以继续删除此方法。
如果开启未使用声明检查,则方法名称将呈灰色,表示未使用。
将插入符号放入
getUsernameFromMessage
并按。这将为您提供删除此方法的选项。AltEnter选择安全删除会弹出安全删除对话框,您可以搜索该方法的用法。
按“确定”继续进行搜索。在我们的例子中,删除是完全安全的,因此该方法被删除。
我们的“未使用”方法可能没有被标记为未使用,因为它可能被测试覆盖。但如果我们仍然知道它没有被使用,或者已经通过检查过它,我们仍然可以安全地删除它。AltF7
将插入符号放在方法名称处,然后按。这将像以前一样弹出安全删除对话框,这次当你按OK IntelliJ IDEA 会警告你这个方法有用法AltDelete
按查看用法来检查这些是什么
使用结果面板通过双击每个用法来导航到用法。在我们的例子中,我们看到有一个测试调用我们想要删除的方法。
由于此测试是为了确保我们不再需要的方法的正确性,因此我们也可以删除此测试。在编辑器窗口中,按测试方法名称,然后在“安全删除”对话框中说“确定” 。测试方法将被删除。AltDelete
现在,我们将在“安全删除冲突”窗口中看到该代码不再有效。
由于这是我们最初想要删除的方法的唯一用途,因此我们可以选择“重新运行安全删除”按钮。这次当您在“安全删除”对话框中按“确定”
getUsernameFromMessage
时,该方法将被删除。
删除的影响
提示
如果您发现某个公共方法看似未使用,但确实构成公共 API 的一部分,则应该对其进行测试。“未使用”警告可能并不意味着您应该删除该方法,而是告诉您应该测试该方法。
IntelliJ 的检查可以向您显示未使用的代码,但是如果您的代码要打包为库供其他人使用,或者以其他方式公开公共 API,则某些公共符号可能会在使用时被标记为未使用。事实上,它们由您无法控制的代码使用。如果公共符号似乎未使用,您应该检查这些符号是否被其他系统以某种方式使用。
未使用的参数、局部变量和私有字段都是删除的好选择,因为应该很容易看出删除它们不会影响任何功能。
使用安全删除来删除符号,无论它们是否未使用,都可以让您在执行重构之前检查受影响的区域是否是您期望的区域,并让您可以控制要应用的更改。但是,仍然要注意公共符号可能会被您无法控制的系统使用,因此在删除这些符号时请务必小心谨慎。
结论
IntelliJ IDEA 具有许多可用的自动重构功能,所有这些功能都旨在让您(开发人员)以尽可能低影响的方式重塑您的代码。目的是进行小的增量更改,始终保持代码处于可编译状态。重构功能的强大之处在于将较小的更改链接在一起,以将代码朝着您想要的某个目标的方向移动:减少重复、删除不必要的代码、力求简单、提高可读性或对代码进行更大的重构。设计。
在开发新功能或错误修复时,小的、简单的更改是可能的,甚至是可取的,但请记住,可能需要单独应用较大的更改,以区分不应影响现有功能的重构和功能更改。
感谢您的反馈意见!