我该如何防止意外地重复代码?


33

我在相当大的代码库上工作。数百个类,大量不同文件,许多功能,花费15分钟以上的时间才能提取新副本等。

如此庞大的代码库的一个大问题是,它具有相当多的实用程序方法,它们执行相同的操作,或者具有可能时不使用这些实用程序方法的代码。而且,实用程序方法不只是全部在一个类中(因为这将是一个巨大的混乱)。

我对代码库比较陌生,但是多年来致力于该代码工作的团队负责人似乎也遇到了同样的问题。这会导致很多代码和工作重复,因此,当发生问题时,通常会分成4个基本相同的代码副本

我们如何遏制这种模式?与大多数大型项目一样,并非所有代码都已记录在案(尽管有些文件已记录在案),并且并非所有代码都...很整洁。但是从根本上说,如果我们能够在这方面提高质量,那真是​​太好了,这样将来我们就可以减少代码重复,并且更容易发现实用程序之类的东西。

而且,实用程序函数通常在某个静态帮助器类中,在单个对象上工作的某些非静态帮助器类中,或者是主要“帮助”于其上的类的静态方法。

我进行了一个实验,将实用程序功能添加为扩展方法(我不需要该类的任何内部组件,并且肯定只在非常特定的情况下才需要)。这样可以防止主类杂乱无章,但除非您已经知道,否则实际上再也找不到了。


Answers:


30

简单的答案是,您确实无法防止代码重复。但是,您可以通过一个困难的连续重复增量过程来“修复它”,该过程分为两个步骤:

步骤1.开始在旧代码上编写测试(最好使用测试框架)

步骤2.使用从测试中学到的内容来重写/重构重复的代码

您可以使用静态分析工具来检测重复的代码,对于C#,有大量工具可以为您执行此操作:

诸如此类的工具将帮助您在代码中找到执行类似操作的点。继续编写测试以确定它们确实是正确的;使用相同的测试使重复代码更易于使用。此“重构”可以通过多种方式完成,您可以使用此列表来确定正确的列表:

此外,Michael C. Feathers也有一本关于此主题的整本书,有效地使用了Legacy Code。它深入探讨了可以采取的不同策略,以使代码变得更好。他有一个“旧版代码更改算法”,与上述两步过程相距不远:

  1. 确定变更点
  2. 查找测试点
  3. 打破依赖
  4. 编写测试
  5. 进行更改并重构

如果您要处理棕地开发,即需要更改的旧代码,那么这本书是一本不错的书。

在这种情况下

在OP的情况下,我可以想象无法测试的代码是由蜜罐引起的,这些蜜罐具有多种形式的“实用方法和技巧”:

  • 静态方法
  • 静态资源的使用
  • 单身人士班
  • 魔法价值

请注意,这些没有什么不妥,但另一方面,它们通常很难维护和更改。.NET中的扩展方法是静态方法,但也相对易于测试。

在进行重构之前,请与您的团队讨论。在进行任何操作之前,它们需要与您保持在同一页面上。这是因为如果您重构某些内容,那么机会很大,将导致合并冲突。因此,在进行某些工作之前,请对其进行调查,并告诉您的团队谨慎处理这些代码点,直到完成为止。

由于OP是代码的新功能,因此在执行任何操作之前,还需要执行其他一些操作:

  • 花一些时间从代码库中学习,即打破“一切”,测试“一切”,还原。
  • 在提交之前,请团队中的某人检查您的代码。;-)

祝好运!


实际上,我们已经进行了大量的单元和集成测试。不是100%的覆盖率,但是如果不对代码库进行重大更改,我们几乎无法对某些操作进行单元测试。我从未考虑过使用静态分析来查找重复项。接下来,我将不得不尝试。
Earlz 2012年

@Earlz:静态代码分析很棒!;-)另外,每当需要进行更改时,请考虑使更改变得更容易的解决方案(为此检查对模式目录的重构)
Spoike 2012年

+1我会理解是否有人在这个Q上悬赏,以奖励这位“额外帮助”。重构到模式目录是黄金,以GuidanceExplorer.codeplex.com的方式进行的此类编程非常有用。
杰里米·汤普森

2

我们也可以尝试从另一个角度看问题。我们可以考虑问题是否源于缺乏代码重用策略,而不是认为问题是代码重复。

最近,我读了《具有可重用组件的软件工程》一书,它的确对如何在组织级别提高代码可重用性提出了一系列非常有趣的想法。

本书的作者Johannes Sametinger描述了代码重用的一系列障碍,从概念上讲有些技术上的障碍。例如:

概念和技术

  • 难以找到可重用的软件:除非找到它,否则软件无法重用。当存储库没有足够的组件信息或组件分类不当时,重用不太可能发生。
  • 所发现软件的不可重用性:易于访问现有软件并不一定会增加软件可重用性。无意间,很少以某种方式编写软件,以便其他人可以重用它。与从头开始编写所需功能相比,修改和改编其他人的软件可能会变得更加昂贵。
  • 不适合重用的遗留组件:除非为重用而设计和开发组件,否则很难或不可能重用它们。仅从各种旧版软件系统中收集现有组件并尝试将其重新用于新开发不足以实现系统重用。重新设计可以帮助提取可重复使用的组件,但是工作量可能很大。
  • 面向对象技术:广泛认为,面向对象技术对软件重用具有积极影响。不幸和错误的是,许多人还认为重用取决于这项技术,或者采用面向对象的技术足以满足软件重用。
  • 修改:组件并不一定总是我们想要的那样。如果需要修改,我们应该能够确定它们对组件的影响及其先前的验证结果。
  • 垃圾回收:将可重复使用的组件认证到一定质量水平有助于最大程度地减少可能的缺陷。质量控制不良是重用的主要障碍之一。我们需要一些方法来判断所需的功能是否与组件提供的功能相匹配。

其他基本技术困难包括

  • 同意可重用组件的构成。
  • 了解组件的功能以及如何使用它。
  • 了解如何将可重用组件连接到设计的其余部分。
  • 设计可重复使用的组件,使其易于以受控方式进行调整和修改。
  • 组织存储库,以便程序员可以找到并使用他们所需要的东西。

作者认为,根据组织的成熟度,会出现不同级别的可重用性。

  • 应用程序组之间的临时重用:如果没有明确的重用承诺,那么重用最多只能以非正式和随意的方式进行。大多数重用(如果有)将在项目内进行。这也会导致代码清除,并最终导致代码重复。
  • 应用程序组之间基于存储库的重用:使用组件存储库并可由各种应用程序组访问时,情况略有改善。但是,不存在将组件放入存储库的明确机制,也没有人对存储库中组件的质量负责。这可能会导致许多问题并妨碍软件重用。
  • 与组件组集中重用:在这种情况下,组件组明确负责存储库。该小组确定要存储在存储库中的组件,并确保这些组件的质量和必要文档的可用性,并帮助在特定的重用场景中检索合适的组件。应用程序组与组件组是分开的,组件组是每个应用程序组的一种分包商。组件组的目标是最大程度地减少冗余。在某些模型中,该小组的成员还可以从事特定项目。在项目启动期间,他们的知识对于促进重用非常有价值,由于他们参与了特定项目,因此他们可以确定可能的候选对象,以将其包含在存储库中。
  • 基于域的重用:组件组的专业化等于基于域的重用。每个域组负责其域中的组件,例如网络组件,用户界面组件,数据库组件。

因此,也许除了其他答案中给出的所有建议之外,您还可以设计可重用性程序,进行管理,形成负责通过进行域分析来标识可重用组件的组件组以及定义可重用组件的存储库,其他开发人员可以轻松地进行存储查询并寻找解决问题的方案。


1

有两种可能的解决方案:

预防 -尝试拥有尽可能好的文档。正确记录每个功能,并轻松搜索整个文档。另外,在编写代码时,应清楚地指出代码应该去哪里,因此应该在哪里看。“实用程序”代码的限制数量是其关键点之一。每次我听到“让实用程序成为阶级”时,我的头发就会升起,血液会冻结,因为这显然是一个问题。每当有某些功能存在时,始终可以使用快速简便的方法让人们了解代码库。

解决方案 -如果预防失败,您应该能够快速有效地解决有问题的代码段。您的开发过程应允许快速修复重复的代码。单元测试是完美的选择,因为您可以有效地修改代码而不必担心破坏代码。因此,如果您找到2个类似的代码段,那么只需进行少量重构即可将它们抽象为一个函数或类。

我个人认为预防是不可能的。您尝试的次数越多,找到已经存在的功能就越成问题。


0

我认为这种问题没有一般的解决方案。如果开发人员有足够的意愿查找现有代码,则不会创建重复的代码。如果愿意,开发人员还可以当场解决问题。

如果语言是C / C ++,则由于链接的灵活性(人可以extern在没有先验信息的情况下调用任何函数),复制合并会更容易。对于Java或.NET,您可能需要设计帮助程序类和/或实用程序组件。

我通常仅在主要错误是由重复部分引起的情况下才开始重复删除现有代码。


0

这是一个大型项目的一个典型问题,该项目已由许多程序员处理,有时在很多同伴的压力下做出了贡献。制作一个类的副本并使其适应该特定类是非常诱人的。但是,当在始发类中发现问题时,也应该在其后代中将其解决,这通常被遗忘。

有一个解决方案,它称为Java 6中引入的泛型。它等效于称为模板的C ++。通用类中尚不知道其确切类的代码。请检查Java Generics,您将找到大量的Java文档。

一个好的方法是通过重写由于某种错误而需要修复的第一个代码来重写似乎在许多地方被复制/粘贴的代码。重写它以使用泛型,并编写非常严格的测试代码。

确保泛型类的每个方法都被调用。您还可以介绍代码覆盖率工具:通用代码应完全覆盖代码,因为它将在多个地方使用。

还要编写测试代码,即对要与通用代码段结合使用的第一个指定类使用JUnit或类似代码。

当所有前面的代码都可以使用并且经过充分测试时,请开始在第二个(大多数情况下)复制版本中使用通用代码。您将看到有一些特定于该指定类的代码行。您可以在抽象的受保护方法中调用这些代码行,该方法必须由使用Generic基类的派生类实现。

是的,这是一项繁琐的工作,但是随着您的前进,将淘汰类似的类并将其替换为非常干净,编写良好且易于维护的东西会越来越好。

我遇到过类似的情况,在通用类上最终替换了大约6或7个几乎完全相同但又在一段时间内被各种程序员复制和粘贴的其他几乎完全相同的类。

是的,我非常赞成对代码进行自动化测试。一开始它会花费更多,但肯定会为您节省大量时间。并尝试使通用代码的代码覆盖率至少达到80%和100%。

希望这会有所帮助,并祝你好运。


0

实际上,我将在这里和其他地方回应最不受欢迎的观点,Gangnus并建议代码重复并不总是有害的,有时可能是危害较小的。

例如,如果您给我选择使用:

A)一个经过测试的稳定(不变)的微型图像库,它复制了几十行用于矢量数学的琐碎数学代码,例如点积,and和钳位,但与其他任何东西完全脱钩,并且仅占很小一部分一秒。

B)不稳定的(快速变化的)图像库,它依赖于史诗般的数学库来避免上述几行代码,数学库是不稳定的,并不断接收新的更新和更改,因此图像库也必须如果没有彻底改变,也可以重建。清理整个过程需要15分钟。

...那么显然,对于大多数人来说,A是可取的,实际上正是由于其较小的代码重复,A是更可取的。我需要重点强调的是经过测试的部分。显然,没有什么比复制代码更糟糕的了,因为复制代码最初甚至都不起作用,这时它就是在复制错误。

但是,还需要考虑耦合和稳定性,在这里和那里进行一些适度的复制可以用作解耦机制,这也可以提高包装的稳定性(不变的性质)。

因此,我的建议实际上是将更多的精力放在测试上,并尝试提出一些真正稳定的东西(如不变,找到将来改变的很少理由),并且可靠地依赖外部资源(如果有)非常稳定,而不是尝试在代码库中消除所有形式的重复项。在大型团队环境中,后者往往是一个不切实际的目标,更不用说它可以增加代码库中的耦合和不稳定代码的数量。


-2

不要忘记代码重复并不总是有害的。想象一下:现在您有一些任务需要在项目的完全不同的模块中解决。刚才是相同的任务。

可能有三个原因:

  1. 这两个模块围绕此任务的主题是相同的。在这种情况下,代码重复不好,应该清理。创建一个类或模块来支持该主题并在两个模块中使用其方法将是明智的。

  2. 就您的项目而言,这项任务是理论上的。例如,它来自物理学或数学等。任务独立存在于您的项目中。在这种情况下,代码重复很不好,也应该清理。我将为此类函数创建一个特殊的类。并在需要的任何模块中使用此类功能。

  3. 但是在其他情况下,任务的巧合只是暂时的巧合,仅此而已。危险的是,由于重构甚至调试,相信在项目更改期间这些任务将保持不变。在这种情况下,最好在不同的地方创建两个相同的功能/代码段。而且其中之一的未来变化不会触及另一个。

第三种情况经常发生。如果您“不知不觉地”重复,则主要是因为这个原因-这不是真正的重复!

因此,在确实需要时尝试保持其清洁,如果不是必须的话,不要害怕重复。


2
code duplication is not always harmful是一个不好的建议。
图兰斯·科尔多瓦

1
我应该向你的权威鞠躬吗?我把我的理由放在这里了。如果我误会了,请指出哪里出了问题。现在看来,这是您进行讨论的能力差。
Gangnus

3
代码重复是软件开发中的核心问题之一,许多计算科学家和理论家已经开发出范式和方法论,只是为了避免代码重复成为软件开发中可维护性问题的主要来源。这就像在说“编写糟糕的代码并不总是不好”那样,任何事情都可以用言辞来证明。也许你是对的,但避免重复代码是这样的生活,鼓励相反的一个太好的原则..
Tulains科尔多瓦

这里提出了论点。你还没有 从16世纪开始,对权威的引用就不起作用了。您不能保证您已经正确理解它们,也不能保证它们也是我的权威。
Gangnus 2012年

没错,代码重复并不是软件开发中的核心问题之一,并且尚未开发出避免这种情况的范例和方法。
图兰斯·科尔多瓦
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.