it-swarm.cn

当DRY和KISS)原理不兼容时,我应该考虑什么?

DRY原理 有时会迫使程序员编写复杂的,难以维护的函数/类。这样的代码会随着时间的流逝而变得越来越复杂且难以维护。违反 KISS原则

例如,当多个功能需要做类似的事情时。通常的DRY解决方案是编写一个采用不同参数的函数,以允许用法上的细微变化。

好处显而易见,DRY =可以进行更改的地方,依此类推。

缺点和违反KISS=)的原因是,随着时间的流逝,随着越来越多的参数,这些函数变得越来越复杂,最终,程序员将非常害怕对此类功能进行任何更改,否则将导致该功能在其他用例中的错误。

我个人认为违反DRY原则)使其遵循KISS原则)是有意义的。

我宁愿拥有10个相似的超级简单函数,也不愿拥有一个超级复杂的函数。

我宁愿做一些乏味而又容易的事情(在10个地方进行相同的更改或相似的更改),而不是在一个地方进行非常可怕/困难的更改。

显然,理想的方法是在不违反DRY的情况下使其尽可能KISS)。但是有时似乎是不可能的。

出现的一个问题是“此代码多久更改一次?”这意味着如果它经常更改,则使其变得更重要。我不同意,因为更改此复杂DRY=)函数通常会使它的复杂性增加,并且随着时间的推移会变得更糟。

因此,基本上,我认为通常KISS> DRY。

你怎么看?在哪种情况下,您认为DRY总是应该赢得KISS,反之亦然?)您在做出决定时会考虑哪些因素?如何避免这种情况?

71
user158443

吻是主观的。 DRY容易被过度应用。它们背后都有好主意,但都易于滥用。关键是平衡。

KISS确实在您的团队眼中。您不知道KISS是什么。您的团队知道。向他们展示您的工作,看看他们是否认为这很简单。您对此判断不佳,因为您已经知道它的工作原理。找出您的代码供他人阅读的难易程度。

DRY与您的代码外观无关。您无法通过搜索相同的代码来发现真正的DRY问题。真正的DRY问题可能是您正在以完全不同的方式解决相同的问题代码在不同的地方。当您使用相同的代码在不同的地方解决另一个问题时,您不会违反DRY)。为什么?因为不同的问题可以独立更改,所以现在需要更改另一个没有。

一处做出设计决策。不要在周围散布决定。但是,不要将现在看起来相同的所有决定都放在同一位置。即使x和y都设置为1,也可以。

从这个角度来看,我从来没有把KISS或DRY)放在另一个之上。我几乎看不到它们之间的紧张关系。我谨防滥用这些都是重要的原则,但不是灵丹妙药。

144
candied_orange

我已经在 一个评论另一个答案 中通过candied_orange编写为 类似问题 并在 不同的答案 ,但需要重复:

DRY是一个可爱的三字母缩写词,它是助记符“ Do n't Repeat Yourself”的缩写,它是在 The Pragmatic Programmer 一书中创造的,它是 整个8.5页 。它还具有 Wiki上的多页说明和讨论

本书中的定义如下:

每条知识都必须在系统中具有单一,明确,权威的表示形式。

请注意,删除重复项很重要not。大约是identification哪个重复是规范的。例如,如果您有一个缓存,则缓存将包含与其他内容重复的值。 但是,必须非常清楚地指出缓存是not规范源。

原理是not三个字母DRY。这是本书和Wiki中大约20页的页面。

该原理也与OAOO密切相关,它是“ Once And Only Once”(一次又一次)的不太可爱的四字母缩写,而这又是eXtreme Programming中的一个原理,它具有 多页说明和讨论在Wiki上

OAOO Wiki页面上有来自Ron Jeffries的非常有趣的报价:

我曾经看到Beck宣布将几乎完全不同的代码的两个补丁声明为“重复”,将它们更改为WERE重复,然后删除新插入的重复以提供明显更好的东西。

他详细介绍了:

我记得曾经见过Beck看到过两个完全不同的循环:它们的结构和内容不同,除了单词“ for”外几乎没有重复,而且它们在同一循环上(不同地)采集。

他将第二个循环更改为与第一个循环相同的循环。这需要更改循环的主体,以在集合的末尾跳过项目,因为以前的版本仅在集合的前面。现在,for语句相同。他说:“好吧,必须消除重复,并将第二个身体移到第一个循环中,然后完全删除第二个循环。

现在,他在一个循环中进行了两种类似的处理。他在那里发现了某种重复,提取了一种方法,做了其他几件事,瞧!代码好多了。

第一步-创建重复-令人吃惊。

这表明:您可以在没有重复代码的情况下进行重复!

这本书展示了硬币的另一面:

作为在线葡萄酒订购应用程序的一部分,您要捕获并验证用户的年龄以及订购的数量。根据网站所有者的说法,它们都应为数字,且均应大于零。因此,您可以编写验证代码:

def validate_age(value):
 validate_type(value, :integer)
 validate_min_integer(value, 0)

def validate_quantity(value):
 validate_type(value, :integer)
 validate_min_integer(value, 0)

在检查代码期间,常驻常识用户会跳回此代码,声称这是DRY违反:两个函数体都相同。

他们错了。代码相同,但是它们表示的知识不同。这两个函数验证恰好具有相同规则的两个不同的事物。那是巧合,而不是重复。

这是重复的代码,不是知识的重复。

关于复制的一个奇闻轶事可以使人们深刻了解编程语言的本质:许多程序员都知道编程语言Scheme,并且它是LISP系列中的一门程序语言,具有一流的高阶过程,词法作用域,词法闭包,以及对纯功能,参照透明代码和数据结构的关注。但是,不是很多人知道的是,它是为了研究面向对象的编程和Actor系统而创建的(Scheme的作者认为这是紧密相关的,如果不是同一件事的话)。

Scheme中的两个基本过程是lambda(创建过程)和apply(执行过程)。 Scheme的创建者又添加了两个:alpha,它创建一个actor(或对象),以及send,它向演员发送消息(或宾语)。

同时具有applysend的一个令人讨厌的结果是,过程调用的优雅语法不再起作用。在我们今天所知道的Scheme中(以及几乎所有的LISP中),一个简单的列表通常被解释为“将列表的第一个元素解释为一个过程,并将它的apply ____ ____________理解为参数,”。所以,你可以写

(+ 2 3)

那相当于

(apply '+ '(2 3))

(或者接近,我的计划非常生锈。)

但是,这不再起作用,因为您不知道是apply还是send(假设您不想区分Scheme的创建者不优先的两个变量之一,他们希望这两个范式相等)。 ……或者,你呢? Scheme的创建者意识到,实际上,他们只需要检查符号所引用的对象的类型:如果+是一个过程,则您apply,如果+是一个参与者,您send收到一条消息。您实际上并不需要单独的applysend,您可以使用apply-or-send之类的东西。

那就是他们所做的:他们接受了两个过程applysend的代码,并将它们作为条件语句的两个分支放入同一过程中。

不久之后,他们还重新编写了Scheme解释器,到目前为止,该解释器是在高级Scheme中以非常低级的寄存器传送汇编语言为寄存器机编写的。他们注意到了一些惊人的事情:条件成为相同的两个分支中的代码。他们以前没有注意到这一点:这两个过程是在不同的时间编写的(它们以“最小LISP”开始,然后添加OO),以及冗长和低级大会的措辞实际上意味着它们的写法大相径庭,但是用高级语言重写它们之后,很显然它们做了同样的事情。

这导致了对Actor和OO的深刻理解:执行一个面向对象的程序,并使用带有词法闭包和适当的尾部调用的过程语言执行程序是同一件事。唯一的区别是语言的原语是对象/角色还是过程。但是操作上,它是相同的。

不幸的是,这也导致了另一个重要的认识,即使在今天,这种认识也没有得到很好的理解:没有适当的尾调用,您就无法维护面向对象的抽象,或者说得更积极:一种声称是面向对象但没有适当的尾调用的语言。 ,不是面向对象。 (不幸的是,这适用于我所有喜欢的语言,而且也不是学术性的:我have遇到了这个问题,为了避免堆栈溢出,我不得不中断封装。)

这是一个示例,其中非常好的隐藏重复实际上模糊重要知识,而发现此重复也揭示了知识。

39
Jörg W Mittag

如有疑问,请始终选择最简单的解决方案来解决问题。

如果事实证明简单的解决方案过于简单,则可以轻松进行更改。另一方面,过于复杂的解决方案也更难以更改。

在所有设计原则中,KISS确实是最重要的,但它经常被忽略,因为我们的开发人员文化将机灵和使用精美的技术视为重中之重。但是有时候if确实比 (策略模式 )更好。

DRY=原理有时会迫使程序员编写复杂且难以维护的函数/类。

停在那儿! DRY)的用途是为了获得更多可维护的代码。到less可维护代码,则不应应用该原理。

请记住,这些原则本身都不是目标。 目标的目的是使软件能够实现其目标,并在必要时可以进行修改和扩展。 KISS,DRY,SOLID和所有其他原理都是表示以实现此目标。但是所有这些都有其局限性,可以按照与最终目标相反的方式进行应用,即编写可维护的软件。

8
JacquesB

恕我直言:如果您不再专注于KISS/DRY代码,而开始关注驱动代码的需求,您将找到想要的更好答案。

我相信:

  1. 我们需要互相鼓励,保持务实(如您所做的那样)

  2. 我们绝不能停止宣传测试的重要性

  3. 更多地关注需求将解决您的问题。

TLDR

如果您需要独立更改零件,请不要使用辅助功能来使功能独立。如果您对所有功能的要求(以及将来的任何更改)都相同,则将该逻辑移到辅助功能中。

我认为到目前为止,我们所有的答案都构成了维恩图:我们都说相同的话,但是我们在不同部分提供了详细信息。

另外,没有人提到测试,这就是我写这个答案的部分原因。我认为,如果有人提到程序员害怕更改,那么not谈论测试是非常不明智的!即使我们“认为”问题与代码有关,也可能真正的问题是缺乏测试。当人们首先投资于自动化测试时,客观地做出更明智的决策就变得更加现实。

首先,避免恐惧就是智慧-做得好!

这是您说的一句话:程序员将非常害怕对此类[helper]函数进行任何更改,否则它们将在该函数的其他使用情况下引起错误

我同意这种恐惧是敌人,如果它们仅引起对级联错误/工作/变更的恐惧,则必须从不坚持原则。如果在多个功能之间进行复制/粘贴是only消除这种恐惧的方式(我不相信这是-参见下文),那么这就是您应该做的。

与许多不关心代码改进的人相比,您感觉到自己害怕进行更改的恐惧,并且您正在尝试做一些事情,这使您成为一个更好的专业人士,他们只是照着做并进行最低限度的更改以关闭他们的门票。

另外(我可以告诉我重复您已经知道的内容):人际交往能力王牌设计能力。如果您公司中的现实生活中的人彻底坏了,那么您的“理论”是否更好也没关系。您可能必须做出客观上较差的决策,但您知道将维护该决策的人有能力理解并与之合作。此外,我们中的许多人也了解管理人员(IMO)对我们进行微管理,并找到始终拒绝所需重构的方法。

作为一个为客户编写代码的供应商,我必须时刻考虑这一点。我可能想使用currying和meta-programming,因为有一个论点认为它在客观上会更好,但在现实生活中,我看到人们对该代码感到困惑,因为它不是视觉上明显发生了什么。

其次,更好的测试可以一次解决多个问题

如果(且仅)您具有有效,稳定,可靠的,经过时间验证的自动化测试(单元和/或集成),那么我敢打赌,您会看到恐惧逐渐消失。对于自动测试的新手来说,信任自动测试可能会感到非常恐惧。新来者可能会看到所有这些绿点,而对这些绿点反映出现实生活中的生产工作却信心满满。但是,如果您个人对自动测试有信心,那么您可以在情感上/关系上开始鼓励他人也信任它。

对您而言,(如果您还没有的话)第一步是研究测试实践。老实说,我假设您已经知道这些知识,但是由于我没有在您的原始帖子中看到此内容,因此我不得不谈论它。因为自动化测试are如此重要,并且与您提出的情况有关。

在这里,我不会试图单枪匹马地简化所有测试实践,但是我会挑战您,将注意力集中在“可重构的”测试上。在对单元/集成测试进行编码之前,请问自己是否有任何有效的方法来重构CUT(被测代码),从而破坏您刚刚编写的测试。如果是这样,则(IMO)删除该测试。最好有较少的自动化测试,这些自动化测试在重构时不会不必要地破坏,而不是告诉您测试覆盖率高(质量胜于数量)。毕竟,进行重构easier是(IMO)自动化测试的主要目的。

随着时间的流逝,我采用了这种“防重构”的哲学,我得出以下结论:

  1. 自动集成测试比单元测试更好
  2. 对于集成测试,如果需要,请在“模拟器/伪造品”中写上“合同测试”
  3. 切勿测试私有API-私有类方法或文件中未导出的函数。

参考文献:

在研究测试实践时,您可能必须花更多时间自己编写这些测试。有时,唯一的最佳方法是不告诉任何人您正在这样做,因为他们会微观管理您。 很明显这并不总是可能的,因为测试的需求量可能大于保持良好工作/生活平衡的需求量。但是,有时候有些事情很小,您可以偷偷地将任务延迟一两天,以便编写所需的测试/代码,从而摆脱困境。我知道这可能是一个有争议的声明,但我认为这是现实。

另外,您显然可以在政治上尽可能地谨慎,以帮助鼓励其他人自己采取步骤来理解/编写测试。或者,也许您是可以为代码审查施加新规则的技术主管。

当您与同事谈论测试时,希望上面的第一点(务实)提醒我们所有人继续保持倾听的心态,而不是发脾气。

第三,关注需求,而不是代码

我们太多次专注于代码,而没有深刻理解我们的代码应该解决的大局!有时,您必须停止争论代码是否干净,并开始确保您对应该驱动代码的要求有充分的了解。

进行right操作比根据诸如KISS/DRY之类的代码认为代码“漂亮”更为重要。这就是为什么我犹豫要注意那些流行短语,因为(在实践中)它们不小心使您专注于代码,而没有考虑requirements是对好的代码提供良好判断的事实质量。


如果两个功能的需求是相互依存/相同的,则将该需求的实现逻辑放入帮助函数中。该辅助功能的输入将是该需求的业务逻辑的输入。

如果功能要求不同,请在它们之间复制/粘贴。如果此时两者都碰巧具有相同的代码,但是could正确地独立更改,则辅助函数为bad,因为它会影响另一个函数requirement是独立更改。

示例1:您有一个名为“ getReportForCustomerX”和“ getReportForCustomerY”的函数,它们都以相同的方式查询数据库。我们还假装有一个业务需求,每个客户都可以按他们想要的任何方式从字面上自定义其报告。在这种情况下,根据设计,客户在报表中需要不同的数字。因此,如果您有一个需要报告的新客户Z,则最好复制/粘贴另一位客户的查询,然后提交代码并移动它。即使查询是完全相同,这些功能的定义点仍是分离来自一个影响另一个客户的更改。如果您提供了所有客户都希望在其报表中使用的新功能,则可以:是:您可能会在所有功能之间键入相同的更改。

但是,假设我们决定继续制作一个名为queryData的辅助函数。不好的原因是因为通过引入辅助函数会出现more级联更改。如果您的查询中有一个“ where”子句对所有客户都是相同的,那么一旦一位客户希望他们的字段不同,那么就不必1)更改函数X中的查询,而必须是1 )更改查询以执行客户X想要的操作2)将conditionals添加到查询中,而不为其他用户执行此操作。在逻辑上向查询中添加更多条件是不同的。我可能知道如何在查询中添加子条款,但这并不意味着我知道如何在不影响未使用子条款的情况下使该子条款成为条件。

因此,您注意到使用助手功能需要两项更改而不是一项更改。我知道这是一个人为的示例,但是根据我的经验,布尔的维护复杂性比线性增长的更多。因此,添加条件的行为被视为人们必须关心的“另一件事”,而每次都需要更新。

在我看来,这个示例可能就像您遇到的情况一样。有些人在情感上对这些功能之间的复制/粘贴想法不满意,这样的情感反应是可以的。但是,“最小化级联更改”的原则将客观地确定复制/粘贴正常时的例外情况。

示例2:您有三个不同的客户,但是唯一允许他们的报告不同的是列的标题。请注意,这种情况非常不同。我们的业务需求不再是“通过在报告中提供竞争灵活性为客户提供价值”。相反,业务要求是“通过不允许客户大量自定义报告来避免过多的工作”。在这种情况下,唯一一次更改查询逻辑的时间就是必须确保其他所有客户都得到相同的更改。在这种情况下,您肯定要使用一个数组作为输入来创建一个辅助函数-列的“标题”是什么。

将来,如果产品所有者决定他们希望允许客户自定义有关查询的内容,那么您将在帮助器功能中添加更多标志。

结论

您越关注代码需求而不是代码,代码对文字需求的同构就越多。您自然写出更好的代码。

4
Alexander Bird

尝试找到合理的中间立场。与其将一个参数分散且复杂的条件散布在整个函数中,不如将其拆分为几个简单的函数。调用者中会有一些重复,但是不会像您一开始没有将通用代码移到函数中那样重复。

最近,我遇到了一些我正在与Google和iTunes应用程序商店交互的代码。大部分的一般流程是相同的,但是有足够的区别,我无法轻易编写一个函数来封装所有内容。

因此,代码的结构如下:

Google::validate_receipt(...)
    f1(...)
    f2(...)
    some google-specific code
    f3(...)

iTunes::validate_receipt(...)
    some iTunes-specific code
    f1(...)
    f2(...)
    more iTunes-specific code
    f3(...)

我不太担心在两个验证函数中调用f1()和f2()是否违反DRY任务。

3
Barmar

肯特·贝克(Kent Beck)拥护与这个问题相关的4条简单设计规则。正如Martin Fowler所说,它们是:

  • 通过测试
  • 展现意图
  • 没有重复
  • 最小的元素

关于中间两个的顺序有很多讨论,因此可能值得将它们视为同等重要。

DRY是列表中的第三个元素,并且KISS可以视为第二个和第四个的组合,甚至可以看作整个列表的组合。

此列表提供了DRY/KISS二分法的替代视图。您的DRY代码显示意图吗?)您的KISS代码?您可以让以太坊版本更显露还是减少重复?

目标不是DRY=或KISS,这是很好的代码。DRY,KISS和这些规则仅仅是达到目标的工具。

3
Blaise Pascal