是否可以在不增加耦合的情况下应用DRY?


14

假设我们有一个实现功能F的软件模块A。另一个模块B实现了与F'相同的功能。

有多种方法可以消除重复的代码:

  1. 让A使用B的F'。
  2. 让B使用A中的F。
  3. 将F放入自己的模块C中,让A和B都使用它。

所有这些选项都会在模块之间生成其他依赖关系。他们以增加耦合为代价应用了DRY原理。

据我所知,在应用DRY时,耦合总是增加或至少增加到更高的水平。软件设计的两个最基本的原则之间似乎存在冲突。

(实际上,这样的冲突并不令人惊讶。这可能是使良好的软件设计如此困难的原因。我确实感到惊讶的是,通常在介绍性文本中未解决这些冲突。)

编辑(为澄清起见):我认为F和F'的相等不只是一个巧合。如果必须修改F,则很可能必须以相同的方式修改F'。


2
...我认为DRY可能是一种非常有用的策略,但是这个问题说明了DRY的效率低下。有些人(例如,面向对象编程的爱好者)可能会争辩说,您应该将F复制/粘贴到B中只是为了保持A和B的概念自主性,但是我没有遇到过要这样做的情况。我认为复制/粘贴代码几乎是最糟糕的选择,我无法忍受那种“短期内存丢失”的感觉,因此我无法确定已经编写了一种方法/函数来执行某项操作。修复一个功能中的错误并忘记更新另一个功能可能是另一个主要问题。
jrh

3
此OO原理有很多相互矛盾的地方。在大多数情况下,您必须找到一个合理的权衡。但是恕我直言,DRY原则是最有价值的。正如@jrh写道:在多个地方实施相同的行为是维护的噩梦,应不惜一切代价避免这种情况。如果发现您忘记更新生产中的冗余副本之一,可能会导致您的业务中断。
蒂莫西·特拉

2
@TimothyTruckle:我们不应该将它们称为OO原则,因为它们也适用于其他编程范例。是的,DRY是有价值的,但如果过度使用也会很危险。不仅因为它倾向于创建依赖关系,而且还因此带来了复杂性。它也经常应用于由巧合引起的重复项,这些重复项具有不同的更改原因。
Frank Puffer

1
...有时在遇到这种情况时,我也能够将F分解成可用于A需求和B需求的部分。
jrh

1
耦合并不是天生的坏事,通常是减少错误并提高生产率的必要条件。如果您要在函数中使用语言标准库中的parseInt函数,则会将函数与标准库耦合。我好多年没有见过一个没有做到这一点的程序。关键是不要创建不必要的耦合。大多数情况下,使用接口来避免/消除这种耦合。例如,我的函数可以接受parseInt的实现作为参数。但是,这并不总是必需的,也不总是明智的。
Joshua Jones

Answers:


14

所有这些选项都会在模块之间生成其他依赖关系。他们以增加耦合为代价应用了DRY原理。

为什么会这样呢。但是它们减少了线路之间的耦合。您将获得改变联轴器的能力。耦合有多种形式。提取代码会增加间接性和抽象性。增加它可能是好事或坏事。决定您得到哪个的第一件事就是您使用的名称。如果看名字的时候让我感到惊讶,那我就没有给任何人任何帮助。

另外,请勿在真空中遵循DRY。如果您杀死重复项,则您有责任预测该代码的这两种用法将一起改变。如果它们有可能独立变化,那么您会造成混乱和额外的工作,而收效甚微。但是,一个真正好的名字可以使它更可口。如果您能想到的只是一个坏名字,请立即停止。

除非您的系统如此孤立,以至没有人知道它是否有效,否则耦合将始终存在。因此,重构耦合是选择毒药的游戏。后续DRY可以通过在很多地方一遍又一遍地表达相同的设计决策而使产生的耦合最小化而获得回报,直到很难改变为止。但是DRY可能使您无法理解您的代码。挽救这种情况的最好方法是找到一个好名字。如果您想不出好名字,希望您能熟练地避免无意义的名字


为了确保我正确理解了您的观点,让我说点不同:如果您为提取的代码分配了好名,那么提取的代码就不再与理解软件相关,因为该名称说明了一切(理想情况下)。这种耦合仍然存在于技术层面,而不是认知层面。因此它是相对无害的,对吗?
Frank Puffer

1
没关系,我很糟糕:meta.stackexchange.com/a/263672/143358
Basilevs

@FrankPuffer更好?
candied_orange

3

有多种方法可以消除显式依赖。一种流行的方法是在运行时注入依赖项。这样,您可以获得DRY,以静态安全为代价删除了耦合。如今,它是如此流行,以至于人们甚至都不理解,这是一种折衷。例如,应用程序容器通常通过隐藏复杂性来提供依赖项管理,从而使软件极其复杂。由于缺乏类型系统,即使是普通的旧构造函数注入也无法保证某些合同。

要回答标题-是的,可以,但是要为运行时调度的后果做好准备。

  • 在A中定义接口F A,提供F的功能
  • 限定界面F1 在乙
  • 将F放入C
  • 创建模块D来管理所有依赖项(取决于A,B和C)
  • 适应F至˚F 和F 在d
  • 将包装器注入(传递)到A和B

这样,依赖关系的唯一类型就是D,这取决于彼此之间的其他模块。

或使用内置的依赖项注入功能在应用程序容器中注册C,并享受自动装配运维缓慢增长的运行时类装入循环和死锁的命运。


1
+1,但需要明确指出,这通常是一个不好的权衡。静态安全性可为您提供保护,并且在远处采取一堆怪异的动作来规避它,只是在您的项目复杂性有所提高时要求进一步追踪难以追踪的错误……
Mason Wheeler

1
真的可以说DI打破了依赖关系吗?甚至在形式上,您也需要带有F签名的接口来实现它。但是,仍然,如果模块A经常使用C中的F,则无论它在运行时注入了C还是直接链接,它都将在C上脱模。DI不会破坏依赖关系,如果未提供dependenc,则bug仅会推迟失败
max630

@ max630它将实现依赖项替换为契约性的依赖项,后者较弱。
Basilevs

@ max630您是正确的。DI不能说打破了依赖。实际上,DI是引入依赖关系的一种方法,并且实际上与有关耦合的问题正交。在F A的变化(或包封它的界面)仍然需要在A和B二者的变化
王侧滑动

1

我不确定没有更多上下文的答案是否有意义。

是否A已经依赖,B反之亦然?-在这种情况下,我们可能有明显的住所选择F

不要AB已经共享任何共同的依赖可能是一个很好的家F

有多大/复杂F?还有什么F取决于?

是模块AB在同一个项目中使用?

无论如何AB最终会分享一些共同的依赖吗?

使用哪种语言/模块系统:新模块在程序员的痛苦中在性能开销方面有多痛苦?例如,如果您使用模块系统为COM的C / C ++编写代码,这会给源代码造成麻烦,需要备用工具,对调试有影响,并且对性能有影响(对于模块间调用),我可能认真休息一下。

另一方面,如果您正在谈论在单个执行环境中无缝结合的Java或C#DLL,那是另一回事。


函数是抽象,并且支持DRY。

但是,好的抽象需要完整—不完整的抽象很可能会导致使用方的客户(程序员)使用底层实现的知识来弥补不足:与提供抽象而不是提供更完整的抽象相比,这导致了更紧密的耦合。

因此,我认为要寻求创建更好的抽象AB依赖,而不是简单地将一个函数移到新模块中C。 

我正在寻找一组函数来梳理出一个新的抽象,也就是说,我可能要等到代码库进一步发展之后,才能确定一个更完整/更完整的抽象重构,而不是基于一个抽象在单个功能代码上告诉。


2
耦合抽象和依赖图是否危险?
Basilevs

Does A already depend on B or vice versa? — in which case we might have an obvious choice of home for F.假设A将始终依赖B(反之亦然),这是一个非常危险的假设。OP认为F不是A(或B)的固有组成部分,这一事实表明F存在而不是任何一个库固有的。如果F属于一个库(例如DbContext扩展方法(F)和Entity Framework包装器库(A或B)),则OP的问题就没有意义了。
较平的

0

这里的答案集中在您可以“最小化”此问题的所有方式上,这给您带来了伤害。仅提供不同方式创建耦合的“解决方案”根本不是真正的解决方案。

事实是,您无法理解您所创建的问题。您的示例所涉及的问题与DRY无关,而与(更广泛的)应用程序设计无关。

问自己一个问题,为什么模块A和B如果都依赖于同一功能F,那么它们是分开的?当然,如果您致力于不良的设计,则在依赖管理/抽象/耦合/命名方面会遇到问题。

根据行为进行正确的应用程序建模。这样,需要将依赖于F的A和B片段提取到它们自己的独立模块中。如果不可能,则需要将A和B合并。无论哪种情况,A和B都不再对系统有用,应该不再存在。

DRY是可用于暴露不良设计而不引起不良设计的原理。如果由于应用程序的结构而无法实现DRY(它真正适用时-注意您的编辑),则很明显的迹象表明该结构已成为一种责任。这就是为什么“连续重构”也是遵循的原则的原因。

其他设计原则(SOLID,DRY等)的ABC 用于使更改(包括重构)应用程序更加轻松。重点,和所有的其他问题开始消失。


您是否建议在每个应用程序中都只有一个模块?
Basilevs

@Basilevs绝对不是(除非有保证)。我建议有尽可能多的完全解耦的模块。毕竟,这就是模块的全部目的。
国王侧滑

好了,问题意味着模块A和B包含无关的功能,并且已经被相应地提取了,为什么以及为什么不应该再存在它们?您对上述问题有什么解决方案?
Basilevs

@Basilevs该问题暗示A和B没有正确建模。正是这种固有的缺陷首先导致了问题。当然,它们确实存在的简单事实并不能证明它们应该存在。这就是我要提出的重点。显然,必须采用其他设计来避免破坏DRY。重要的是要理解所有这些“设计原则”的真正目的是使应用程序更易于更改。
国王侧滑

是否有大量其他方法具有完全不同的依赖关系,它们被建模为坏方法,并且仅由于这一非偶然方法而必须重做?还是假设模块A和B各自包含一个方法?
Basilevs

0

所有这些选项都会在模块之间生成其他依赖关系。他们以增加耦合为代价应用了DRY原理。

我至少在第三种选择上有不同的看法:

根据您的描述:

  • A需要F
  • B需要F
  • A和B都不需要。

将F放在C模块中不会增加耦合,因为A和B都已经需要C的功能。

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.