修改遗留代码时如何避免破坏功能

在修改遗留代码时,提取方法是避免破坏功能风险的最佳方法。
55 位读者喜欢这个。
Coding on a computer

请允许我稍作反思。我已经从事软件工程领域 31 年了。 在这 31 年里,我修改了大量的遗留软件。

随着时间的推移,我在处理遗留代码时形成了一些习惯。 因为在大多数项目中,我的报酬是交付易于维护的可用软件,所以我无法抽出大量时间来充分理解我即将修改的遗留代码。 所以,我倾向于略读。 略读代码有助于我快速识别存储库中的相关部分。 这是一场与时间的赛跑,我没有精力去关注不太相关的细节。 我总是寻找代码中最相关的区域。 一旦找到它,我就会放慢速度并开始分析它。

我非常依赖我的强大工具——集成开发环境 (IDE)。 哪个强大工具并不重要;现在,它们几乎都能做同样的事情。 对我来说重要的是能够快速找到函数被调用的位置以及变量被使用的位置。

迟早,在我略读代码并分析我打算更改的代码段后,我会确定一个我想插入一些代码的地方。 现在我了解了执行该功能所涉及的类、组件和对象的含义,我首先编写一个测试。

之后,我编写代码以使测试通过。 我输入我打算使用的对象的名称,然后按点键 (.),IDE 会通过提供为该对象定义的所有方法的完整列表来响应。 所有这些方法都可以从我的光标所在的位置调用。

然后我选择对我来说有意义的方法。 我填写空白(即,我为预期的参数/参数提供值),保存更改,然后运行测试。 如果测试通过,我就完成了这个微小的更改。

我通常每小时重复多次此活动。 在整个工作日,重复数十次甚至数百次的情况并不少见。

我相信我修改软件的方式并非我的工作习惯所独有。 我认为它描述了许多(我什至会说是大多数)软件工程师所遵循的典型流程。

一些观察

在这种修改遗留软件的方式中,首先明显的是没有任何关于文档的工作。 经验表明,软件开发人员很少花时间去查阅文档。 花费时间准备文档并生成它以产生 HTML 样式的在线文档通常是浪费。

相反,大多数开发人员只依赖强大的工具。 这样做是正确的——IDE 永远不会说谎,因为它们始终提供他们正在修改的系统的实时画面,并且文档通常已经过时。

另一件事是开发人员不会像编写代码那样阅读源代码。 从头开始编写代码(第一遍)时,许多开发人员倾向于编写长函数。 源代码往往会聚集在一起。 聚集代码使其在第一遍和调试时更容易阅读和推理。 但是在第一遍之后,人们很少(如果有的话)以编写的方式使用代码。 如果我们发现自己从头到尾阅读整个函数,很可能是因为我们已经用尽了所有其他选择,别无选择,只能放慢速度并以笨拙的方式阅读代码。 然而,根据我的经验,缓慢而有序地阅读代码的情况很少发生。

由聚集代码引起的问题

如果将代码保持在第一遍时的状态(即,长函数,大量聚集的代码以便于初始理解和调试),这将使 IDE 失去作用。 如果将对象可以提供的所有功能塞进一个巨大的函数中,那么稍后,当您尝试使用该对象时,IDE 将无济于事。 IDE 将显示一个方法的存在(它可能包含一个巨大的参数列表,这些参数提供的值用于强制执行该方法内部的分支逻辑)。 因此,除非您打开其源代码并非常仔细地阅读其处理逻辑,否则您将不知道如何真正使用该对象。 即使那样,您的头可能也会疼。

匆忙拼凑的“聚集”代码的另一个问题是其处理逻辑无法测试。 虽然您仍然可以为该代码编写端到端测试(输入值和预期输出值),但您无法知道聚集的代码是否在进行任何其他可能存在风险的处理。 此外,您无法测试边缘情况、不寻常的场景、难以重现的场景等。 这使得您的代码无法测试,这是一个非常糟糕的事情。

通过提取方法来分解聚集的代码

长函数或方法始终是思维混乱的标志。 当一个代码块包含大量语句时,通常意味着它做了太多的处理。 在一个地方塞入大量的处理通常意味着开发人员没有仔细考虑清楚。

您无需进一步了解公司的典型组织方式。 公司倾向于分解为许多较小的部门,而不是让数百名员工在单个部门工作。 这样,职责的归属就更加清晰。

软件代码没有什么不同。 应用程序的存在是为了自动化大量复杂的处理。 处理被分解为多个较小的步骤,因此每个步骤都必须映射到单独的、隔离的代码块。 您可以通过提取方法来创建这种单独的、隔离的和自主的代码块。 您采用一个长的、庞大的代码块,并通过将职责提取到单独的代码块中来分解它。

提取的方法可以实现更好的命名

开发人员编写软件代码,但它更多地是被开发人员消费(即阅读)而不是编写。

在消费软件代码时,如果代码具有表达力,则会有所帮助。 表达性归结为适当的结构和适当的命名。 考虑以下语句

if((x && !y) && !b) || (b && y) && !(z >= 65))

如果不运行代码并使用调试器单步执行,将根本不可能理解该语句的含义和意图。 这种活动称为 GAK(键盘上的极客)。 它是 100% 无效的并且非常浪费。

这就是提取方法和适当的命名实践发挥作用的地方。 获取 if 语句中包含的复杂语句,将其提取到它自己的方法中,并为该方法提供一个有意义的名称。 例如

public bool IsEligible(bool b, bool x, bool y, int z) {
  return ((x && !y) && !b) || (b && y) && !(z >= 65);
}

现在用更具可读性的语句替换丑陋的 if 语句

if(IsEligible(b, x, y, z))

当然,您还应该用更有意义的名称替换愚蠢的单字符变量名称,以提高可读性。

重用遗留代码

经验表明,任何未提取并正确命名并移动到最合理的类的功能将永远不会被重用。 提取方法促进频繁重用,这在很大程度上提高了代码质量。

测试遗留代码

为现有代码编写测试很困难,而且感觉不如进行测试驱动开发 (TDD) 那么有意义。 即使您确定应该进行多次测试以确保生产代码按预期工作,当您意识到必须更改生产代码才能进行测试时,您通常会决定跳过编写测试。 在这种情况下,实现交付可测试代码的目标会慢慢地但肯定地减少。

为遗留代码编写测试很繁琐,因为它通常需要大量的时间和代码来设置先决条件。 这与您在使用 TDD 编写测试时花费最少时间编写先决条件的方式相反。

使遗留代码可测试的最佳方法是实践提取方法。 定位嵌套在循环和条件中的代码块并提取它使您可以编写小的、精确的测试。 这种对提取的函数进行的测试不仅提高了代码的可测试性,而且提高了可理解性。 如果遗留代码由于提取方法和编写清晰的测试而变得更容易理解,那么引入缺陷的可能性会大大降低。

结论

使用 TDD 时,关于提取方法的大部分讨论是不必要的。 首先编写一个测试,然后使测试通过,然后扫描该代码以获取更多关于如何构建和改进代码的见解,进行改进,最后更改部分代码库可以保证无需担心提取方法。 由于遗留代码通常意味着未使用 TDD 方法编写的代码,因此您被迫采用不同的方法。 根据我的经验,在修改遗留代码并避免破坏功能的风险方面,提取方法可以带来最大的收益。

下一步阅读
标签
User profile image.
自 1990 年以来,Alex 一直在从事软件开发工作。 他目前的热情是如何将软性带回软件中。 他坚信我们的行业已经达到了一个高度,完全可以实现这个崇高的目标(即将软性带回软件中)。

评论已关闭。

Creative Commons License本作品采用知识共享署名-相同方式共享 4.0 国际许可协议进行许可。
© . All rights reserved.