我目前正在审查一个非常古老的 C++ 项目,并在那里看到很多代码重复。
例如,有一个类有 5 个 MFC 消息处理程序,每个处理程序包含 10 行相同的代码。或者到处都有一个非常具体的字符串转换的 5 行代码段。在这些情况下,减少代码重复根本不是问题。
但是我有一种奇怪的感觉,我可能误解了某些东西,并且这种重复本来是有原因的。
复制代码的正当理由是什么?
我目前正在审查一个非常古老的 C++ 项目,并在那里看到很多代码重复。
例如,有一个类有 5 个 MFC 消息处理程序,每个处理程序包含 10 行相同的代码。或者到处都有一个非常具体的字符串转换的 5 行代码段。在这些情况下,减少代码重复根本不是问题。
但是我有一种奇怪的感觉,我可能误解了某些东西,并且这种重复本来是有原因的。
复制代码的正当理由是什么?
约翰·拉科斯 (John Lakos ) 的大型 C++ 软件设计是一本很好的读物。
他对代码重复有很多好处,这可能有助于或阻碍项目。
最重要的一点是在决定删除重复或重复代码时询问:
如果将来此方法发生更改,我是否要更改重复方法中的行为,还是需要它保持原样?
毕竟,方法包含(业务)逻辑,有时您会想要更改每个调用者的逻辑,有时则不会。视情况而定。
最后,这都是关于维护,而不是关于漂亮的来源。
懒惰,这是我能想到的唯一原因。
更严肃的一点。我能想到的唯一正当理由是在产品周期的最后阶段发生变化。这些往往会经历更多的审查,最小的变化往往会获得最高的成功率。在这种有限的情况下,与重构较小的更改相比,更容易通过代码重复更改。
仍然在我嘴里留下不好的味道。
除了缺乏经验之外,还有可能出现重复代码的原因:
没有时间正确重构
我们大多数人都在现实世界中工作,真正的约束迫使我们快速解决实际问题,而不是考虑代码的好坏。所以我们复制粘贴然后继续。对我来说,如果我后来看到代码被重复了几次,这表明我必须花更多的时间在上面,并将所有实例收敛到一个。
由于语言限制,无法对代码进行概括/不“漂亮”
可以说,在函数的深处,您有几个语句在相同重复代码的实例之间有很大不同。例如:我有一个为视频绘制二维缩略图数组的函数,它嵌入了每个缩略图位置的计算。为了计算命中测试(从点击位置计算缩略图索引),我使用相同的代码但没有绘画。
您根本不确定是否会有泛化
首先复制代码,然后观察它将如何发展。由于我们正在编写软件,因此我们可以允许对软件进行“尽可能晚”的修改,因为一切都是“软的”和可变的。
如果我记得别的东西,我会添加更多。
后来添加...
循环展开
在编译器像爱因斯坦和霍金相结合之前变得聪明之前,你必须展开循环或内联代码才能更快。循环展开将使您的代码被复制,并且可能会快几个百分点,它编译器并没有为您做这件事。
当我第一次开始编程时,我写了一个应用程序,其中我有一堆类似的功能,我用一个简洁的 20-30 行函数包裹起来......我为自己编写了如此优雅的一段代码感到非常自豪。
不久之后,客户在非常具体的情况下更改了流程,然后再一次,再一次,再一次,再一次,再一次....(很多次)我优雅的代码变成了一个非常困难,hackish,buggy, & 高维护混乱。
一年后,当我被要求做一些非常相似的事情时,我故意决定忽略 DRY。我整理了基本流程,并生成了所有重复的代码。记录了重复的代码,我保存了用于生成代码的模板。当客户要求进行特定的条件更改时(例如,如果 x == y^z + b 则 1+2 == 3.42),这简直是小菜一碟。维护和更改非常容易。
回想起来,我可能已经用函数指针和谓词解决了许多这些问题,但是使用我当时所拥有的知识,我仍然相信在这个具体案例中,这是最好的决定。
您可能希望这样做以确保将来对某一部分的更改不会无意中更改另一部分。例如考虑
Do_A_Policy()
{
printf("%d",1);
printf("%d",2);
}
Do_B_Policy()
{
printf("%d",1);
printf("%d",2);
}
现在您可以使用以下功能防止“代码重复”:
first_policy()
{
printf("%d",1);
printf("%d",2);
}
Do_A_Policy()
{
first_policy()
}
Do_B_Policy()
{
first_policy()
}
然而,有一些其他程序员想要更改 Do_A_Policy() 并且会通过更改 first_policy() 来实现的风险,并且会导致更改 Do_B_Policy() 的副作用,程序员可能不知道的副作用。所以这种“代码重复”可以作为一种安全机制来防止程序中这种未来的变化。
有时领域方面的方法和类没有共同点,但实现方面看起来很相似。在这些情况下,通常最好进行代码复制,因为未来的更改更频繁,不会将这些实现分支到不同的东西中。
我能想到的正当理由:如果代码变得更复杂以避免重复。基本上,这就是您在几种方法中几乎相同的地方 - 但并不完全相同。当然 - 然后您可以重构并添加特殊参数,包括指向必须修改的不同成员的指针。但是新的重构方法可能会变得过于复杂。
示例(伪代码):
procedure setPropertyStart(adress, mode, value)
begin
d:=getObject(adress)
case mode do
begin
single:
d.setStart(SingleMode, value);
delta:
//do some calculations
d.setStart(DeltaSingle, calculatedValue);
...
end;
procedure setPropertyStop(adress, mode, value)
begin
d:=getObject(adress)
case mode do
begin
single:
d.setStop(SingleMode, value);
delta:
//do some calculations
d.setStop(DeltaSingle, calculatedValue);
...
end;
您可以以某种方式重构方法调用(setXXX) - 但取决于语言,它可能很困难(尤其是继承)。这是代码重复,因为每个属性的大部分主体都是相同的,但很难重构出公共部分。
简而言之 - 如果重构方法的因素更复杂,我会选择重复代码,尽管它是“邪恶的”(并且会保持邪恶)。
我能看到的唯一“有效”的事情是当这些代码行不同时,然后通过后续编辑收敛到相同的事情。我以前也遇到过这种情况,但没有太频繁。
当然,现在是时候将这个通用代码段分解为新功能了。
也就是说,我想不出任何合理的方法来证明重复代码的合理性。看看为什么不好。
这很糟糕,因为一个地方的改变需要多个地方的改变。这是增加的时间,有可能出现错误。通过分解它,您可以将代码维护在一个单独的工作位置。毕竟,当你编写一个程序时,你不会写两次,为什么一个函数会有什么不同呢?
对于那种代码重复(很多行重复很多次),我会说:
不过,从我通常看到的情况来看,可能是第一个解决方案:-(
我见过的最佳解决方案:让您的开发人员在被雇用时从维护一些旧应用程序开始 - 这会告诉他们这种事情不好......他们会明白为什么,这是最重要的部分。
将代码拆分为多个功能,以正确的方式重用代码,以及所有这些通常都伴随着经验——或者你没有雇用合适的人;-)
很久以前,当我做图形编程时,在某些特殊情况下,您会以这种方式使用重复代码,以避免代码中生成低级 JMP 语句(通过避免跳转到标签/函数来提高性能) . 这是一种优化和执行伪“内联”的方法。
但是,在这种情况下,我认为这不是他们这样做的原因,呵呵。
如果不同的任务偶然相似,在两个地方重复相同的动作并不一定是重复。如果一个地方的行为发生了变化,是否可能它们也应该在其他地方发生变化?那么这是你应该避免或重构的重复。
此外,有时——即使是重复逻辑——减少重复的成本也太高了。这可能会发生,尤其是当它不仅仅是代码重复时:例如,如果您有一个数据记录,其中某些字段在不同的地方重复(数据库表定义、C++ 类、基于文本的输入),减少这种情况的常用方法重复与代码生成有关。这会增加您的解决方案的复杂性。几乎总是,这种复杂性得到了回报,但有时却没有——这是你要做的权衡。
我不知道代码重复的许多充分理由,但与其先跳脚进行重构,不如只重构您实际更改的那些代码位,而不是更改您不更改的大型代码库却完全明白。
听起来原作者要么缺乏经验和/或时间紧迫。大多数有经验的程序员将重用的东西放在一起,因为以后会减少维护——一种懒惰的形式。
您唯一应该检查的是是否有任何副作用,如果复制的代码访问一些全局数据,则可能需要进行一些重构。
编辑:回到编译器蹩脚而优化器甚至更蹩脚的日子,由于编译器中的一些错误,可能不得不做这样的伎俩才能绕过错误。也许它是那样的?几岁?
在大型项目(代码库大至 GB 的项目)中,很可能会丢失现有的 API。这通常是由于文档不足,或者程序员无法找到原始代码;因此重复代码。
归结为懒惰或糟糕的复习习惯。
编辑:
另一种可能性是,这些方法中可能有其他代码在此过程中被删除。
你看过文件上的修订历史吗?
所有的答案看起来都是对的,但我认为还有另一种可能性。也许有性能方面的考虑,因为你所说的让我想起“内联代码”。内联调用它们的函数总是更快。也许你看到的代码已经被预处理了?
当源代码生成器生成重复代码时,我对重复代码没有任何问题。
我们发现迫使我们重复代码的是我们的像素操作代码。我们处理非常大的图像,函数调用开销消耗了我们每像素时间的 30%。
复制像素操作代码使我们的图像遍历速度提高了 20%,但代价是代码复杂性。
这显然是一个非常罕见的情况,最后它大大膨胀了我们的源代码(一个 300 行的函数现在是 1200 行)。
既然有“策略模式”,就没有重复代码的正当理由。没有一行代码必须重复,其他一切都是史诗般的失败。