48

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

  1. 不确定的未来。假设我们在两个地方有相同的代码。关键是,这两个地方只有附带的内涵。由于上下文和语义不同,它们有可能在未来发生变化。从这些地方进行抽象并不便宜,如果其中一个地方发生变化,从抽象中解包将更加昂贵。
  2. 可读性。存在涉及多个变量或步骤的复杂计算。在代码的其他地方还有另一个,它的某些部分是相同的。问题是,如果我们去掉通用部分,计算的可读性会降低,创建的抽象很难给它一个描述性的名字。更糟糕的是,如果算法的某些部分将来会像第 1 点那样发生变化。

上述情况是否是放弃抽象过程并留下重复代码以利于未来更改风险或只是可读性的充分理由?

4

8 回答 8

35

这些都是违反 DRY 的完全正当理由。我应该添加第三个:性能。这很少有什么大不了的,但它可以产生影响,并且抽象可能会降低事情的速度。

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

通常,重复的代码并不理想,但肯定有令人信服的理由允许这样做,可能包括比 OP 和我自己建议的更进一步的原因。

于 2013-07-22T13:25:00.230 回答
18

让我们试着理解为什么 DRY 很重要,然后我们就可以理解在哪里打破规则是合理的:

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

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

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

在你的程序的其他地方,你有类似的一行:

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

如果销售税发生变化,您将需要更改这两条线。这里几乎没有重复的代码,但是如果您在一个地方进行更改,则需要在另一个地方进行更改,这使得代码不是 DRY。更重要的是,您可能很难意识到您必须在两个地方进行更改。也许你的单元测试会捕捉到它,但也许不会,所以摆脱重复很重要。也许您会将销售税的价值分解为一个可以在多个地方使用的单独常量:

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。例如,如果要更改 function 的名称,f则必须更改两个位置。您也许可以通过创建一个小循环并a变成一个数组来使代码更加干燥。但是,这种特殊的重复并不是什么大问题。首先,这两个更改非常接近,因此不太可能意外更改一个而不更改另一个。其次,如果您使用的是编译语言,那么编译器很可能无论如何都会发现问题。如果您不是使用编译语言,那么希望您的单元测试能够捕捉到它。

让你的代码 DRY 有很多很好的理由,但也有很多很好的理由不这样做。

于 2013-07-22T14:38:00.817 回答
17

是的,众所周知,某些代码重复很难在不显着降低可读性的情况下排除。在这种情况下,我会TODO在评论中留下 a 以提醒您存在一些重复,但在撰写本文时,最好还是这样。

通常会发生什么是您在第一点中写的内容,重复出现分歧并且不再是重复。也发生重复是设计问题的标志,但只有在以后才变得清楚。

长话短说:尽量避免重复;如果重复是出了名的难以排除并且在撰写本文时无害,请留下评论作为提醒。


另请参阅每个程序员应该知道的 97 件事

页。14.当心Udi Dahan的分享

系统的两个完全不同的部分以相同的方式执行某些逻辑这一事实比我想象的要少。在我取出那些共享代码库之前,这些部分并不相互依赖。每个都可以独立进化。每个都可以更改其逻辑以适应系统不断变化的业务环境的需要。那四行类似的代码是偶然的——一个时间异常,一个巧合。

在这种情况下,他在系统的两个部分之间建立了依赖关系,这些部分最好保持独立。解决方案基本上是重复。

于 2013-07-22T14:23:04.617 回答
11

工程就是权衡取舍,因此没有对每个问题都有效的明确建议或设计模式。有些决定比其他决定更难支持(代码重复就是其中之一),但如果在你的情况下重复代码的利大于弊,那就去做吧。

于 2013-07-22T13:33:57.933 回答
3

没有绝对的,它总是将是两个邪恶中较小的一个之间的判断电话。通常,DRY 获胜,当你开始违反它时,你必须小心滑坡,但你的推理对我来说似乎很好。

于 2013-07-22T13:24:24.593 回答
2

有关此问题的出色回答,请参阅 Thomas, Hunt 的“The Pragmatic Programmer”(首先是 Dave Thomas 提出了“Dry”一词)

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

于 2013-07-22T13:26:35.330 回答
2

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

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

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

于 2013-07-22T15:12:37.627 回答
1

我相信是的。虽然作为一般规则 DRY 是理想的,但有时最好简单地重复自己。在预开发测试阶段,我发现自己经常忽略 DRY。你永远不知道什么时候你需要对一个函数做一些你不想在另一个函数中做的小改动。我当然会尝试始终在“已完成”(已完成且永远不需要修改的应用程序)应用程序上观察 DRY,但这些应用程序很少见。最后,这取决于应用程序期货的需求。我已经完成了我希望是 DRY 的应用程序,我感谢上帝我没有在其他人身上观察到它。

于 2013-07-22T13:47:47.850 回答