违反DRY原则总是不好吗?

时间:2013-07-22 13:21:56

标签: design-patterns refactoring

我一直在讨论 DRY 不要重复自己)原则,也称为 DIE Duplication Is Evil < / em>)并且有投票,任何简单的代码重复总是邪恶的。我想听听你对以下几点的看法:

  1. 不确定的未来。让我们说,我们在两个地方有相同的代码。关键是,这两个地方只有偶然的内涵。有可能,它们将来会有所不同,因为它们的上下文和语义不同。从这些地方抽象并不便宜,如果其中一个地方发生变化,从抽象中解开将会更加昂贵。
  2. 可读性即可。存在涉及若干变量或步骤的复杂计算。在代码的其他地方还有另一个代码,它们有一些相同的部分。问题是,如果我们取出公共部分,计算的可读性会降低,创建抽象将很难给它一个描述性的名称。更糟糕的是,如果算法的某些部分将来会像第1点那样发生变化。
  3. 上述情况是否是放弃抽象过程的好理由,只留下重复的代码,有利于未来变更或只是可读性的风险?

8 个答案:

答案 0 :(得分:31)

这些都是违反DRY的正当理由。我应该添加第三个:性能。它很少是一个大问题,但它可以产生影响,抽象可能会降低速度。

实际上,我将添加第四个:浪费时间并可能通过更改可能已经正常工作的代码库的两个(或更多)部分来引入新的错误。如果不必要的话,是否值得花费如何抽象这些东西的成本,并且它可能不会在将来节省任何或很多时间?

通常情况下,重复的代码并不理想,但肯定有令人信服的理由允许它,可能还包括OP和我自己建议的其他原因。

答案 1 :(得分:16)

是的,在不使可读性显着恶化的情况下,某些代码重复很难被分解出来。在这种情况下,我在评论中留下TODO作为提醒,表示存在一些重复,但在撰写本文时,似乎最好不要这样做。

通常会发生的事情是你在第一点写的内容,重复分歧并且不再重复。还有一种情况是复制是设计问题的一个标志,但后来才变得清晰。

长话短说:尽量避免重复;如果复制是非常难以分解出来的,并且在撰写时无害,请留下评论作为提醒。


另见97 Things Every Programmer Should Know

页。 14.小心Udi Dahan的分享

  

系统的两个完全不同的部分执行某些逻辑的事实   以同样的方式意味着比我想象的少。直到我拿出那些   共享代码库,这些部分并不相互依赖。每   可以独立发展。每个人都可以改变其逻辑以适应需要   系统不断变化的商业环境。那四行相似的代码是   意外 - 时间异常,巧合。

在这种情况下,他在系统的两个部分之间创建了更好的独立性。解决方案基本上是重复的。

答案 2 :(得分:14)

让我们试着理解为什么DRY很重要,然后我们就能明白违反规则的地方是合理的:

应该使用DRY来避免两个代码在概念上做同样工作的情况,因此每当你在一个地方更改代码时,你必须在另一个地方更改代码。如果相同的逻辑位于两个不同的位置,那么您必须始终记住在两个位置更改逻辑,这可能非常容易出错。这可以适用于任何规模。它可以是正在复制的整个应用程序,也可以是单个常量值。也可能没有任何重复的代码,它可能只是一个重复的原则。你必须问“如果我要在一个地方做出改变,我是否一定需要在其他地方进行同等的改变?”。如果答案为“是”,则代码违反DRY。

想象一下,你的程序中有这样的一行:

cost = price + price*0.10 // account for sales tax

以及程序中的其他位置,您有类似的行:

x = base_price*1.1; // account for sales tax

如果销售税发生变化,您将需要更改这两行。这里几乎没有重复的代码,但事实上,如果你在一个地方进行更改,它需要在另一个地方进行更改,这就是使代码不干的原因。更重要的是,可能很难意识到你必须在两个地方进行改变。也许你的单元测试会抓住它,但也许不会,所以摆脱重复很重要。也许您会将销售税的价值计入一个可以在多个地方使用的单独常量:

cost = price + price*sales_tax;
x = base_price*(1.0+sales_tax);

或者可能创建一个函数来抽象它:

cost = costWithTax(price);
x = costWithTax(base_price);

无论哪种方式,它都很可能值得一试。

或者,您可能拥有看起来非常相似但未违反DRY的代码:

x = base_price * 1.1; // add 10% markup for premium service

如果您要更改销售税的计算方式,您可能不希望更改该行代码,因此实际上并没有重复任何逻辑。

还有一些情况需要在多个地方进行相同的更改是可以的。例如,也许你有这样的代码:

a0 = f(0);
a1 = f(1);

此代码在某些方面不是DRY。例如,如果要更改函数f的名称,则必须更改两个位置。您可以通过创建一个小循环并将a转换为数组来使代码更干。但是,这种特殊的重复并不是什么大问题。首先,这两个变化非常接近,所以不小心改变一个而不改变另一个。其次,如果您使用的是编译语言,那么编译器很可能无论如何都会遇到问题。如果您不是编译语言,那么希望您的单元测试能够捕获它。

有许多充分的理由让你的代码干掉,但还有很多很好的理由不这样做。

答案 3 :(得分:11)

工程学就是权衡利弊,因此没有明确的建议或设计模式对每个问题都有效。有些决策比其他决策更难以支持(代码重复就是其中之一),但如果重复代码的优点超过了你的情况,那就去做吧。

答案 4 :(得分:3)

没有绝对的,它总是在两个邪恶中较小的一个之间作出判断。通常情况下,DRY会赢,当你开始违反它时你必须小心滑坡,但你的推理对我来说似乎很好。

答案 5 :(得分:2)

对于这个问题的一个很好的回答,请参考Thomas,Hunt的“实用程序员”(Dave Thomas首先提出“干'这个词”)

总之,没有简单的答案,保持干燥几乎总是更好,但如果它提高了可读性,那么你应该用你最好的判断,这是你的号召!

答案 6 :(得分:2)

不,违反DRY并不总是坏事。特别是,如果你没有为复制代码的抽象提出一个好名字,即一个适合这两种情境的名称,那么它们可能毕竟是不同的东西,应该留下重复。

根据我的经验,这种巧合往往很少见,而重复的代码越大,最有可能描述一个单一的概念。

我还发现抽象到组合在这方面几乎总是比抽象到继承更好的主意,这很容易导致你错误的方程式和LSPISP违规行为。

答案 7 :(得分:1)

我相信是的。虽然作为一般规则DRY是理想的,但有时候简单地重复自己更好。在开发前的测试阶段,我发现自己经常无视DRY。你永远不知道什么时候你需要对一个功能做一些改变,你不想在另一个功能上做。我当然尝试总是在“完成”(已完成且不需要修改的应用程序)应用程序上观察DRY,但这些很少见。最终它取决于应用期货的需求。我做过我希望干的应用程序,我感谢上帝,我没有在别人身上观察过它。