SOLID与YAGNI对比

68

我经常听到的不遵循SOLID面向对象设计原则的最常见论点之一是YAGNI(尽管争辩者经常没有这么说):

"把特性X和特性Y放在同一个类中没问题。这很简单,为什么要增加一个新类呢(即增加复杂性)?"

"是的,我可以直接把所有业务逻辑放入GUI代码中,这样会更容易和更快捷。这将永远是唯一的GUI,也很难出现重大的新需求。"

"如果出现了不太可能的新需求,我的代码过于混乱时,我仍然可以重构以满足新需求。所以你的'如果以后需要...'的论点不起作用。"

对于这种做法,您最有说服力的论据是什么呢?如何真正证明这是一种昂贵的做法,尤其是对于那些在软件开发方面没有太多经验的人。


9
"新要求的不太可能情况":不错。我可以引用吗?;-) - sleske
11
我不确定如何向某人展示这些提议的错误之处,但值得指出的是,它们中的任何一个都与YAGNI的精神不一致,YAGNI关注的是功能,而不是设计原则。 - Jeff Sternal
1
@Jeff:如果你降低抽象层次,类和库具有特性,这些特性会受到你设计过程的影响。 - Roger Pate
@Roger - 当然可以,但前提是你正在交付一个库(而不是应用程序)。 - Jeff Sternal
@Jeff:我知道你的意思,但我对此持有不同意见:其他开发人员是您代码的客户。 即使完全是内部API,考虑是否添加功能时也应该考虑YAGNI的重要性。 例如:“不要写setter,因为'我们将需要它'。” 这只是术语问题;我们可以达成不同意见的共识。 :) - Roger Pate
显示剩余2条评论
10个回答

53

设计是权衡取舍的管理。 YAGNI 和 SOLID 不冲突:前者说出什么时候添加功能,后者说出如何添加,但它们都指导着设计过程。我的回应如下,针对您引用的每个具体语录,使用了 YAGNI 和 SOLID 的原则。

  1. 构建可重用组件比构建单一使用组件难三倍。
  2. 在将可重用组件纳入重用库之前,应在三个不同的应用程序中尝试该组件。

  — 罗伯特·格拉斯的 三法则, 软件工程的事实和谬误

重构成可重用组件的关键要素是首先在多个地方找到相同的目的,然后再移动它。在这种情况下,YAGNI 适用于内联所需的目的,而不必担心可能的重复,而是添加通用或可重用的功能(类和函数)。

在初始设计中展示 YAGNI 不适用的最佳方法是确定具体的需求。换句话说,在编写代码之前进行一些重构,以显示重复不仅可能存在,而且已经存在:这样可以证明额外的努力是有道理的。


是的,我可以直接将所有业务逻辑放入GUI代码中,这样做会更容易和更快。这将永远是唯一的GUI,并且极不可能有重大新要求。

真的是唯一的用户界面吗?是否计划了后台批处理模式?将来会有Web界面吗?

你的测试计划是什么?你是否会在没有GUI的情况下测试后端功能?GUI对你来说很容易测试,因为你通常不想测试外部代码(例如平台通用的GUI控件),而是专注于你的项目。

将特性X和特性Y放入同一个类中是可以的。它非常简单,为什么要添加一个新的类(即复杂性)。

你能指出需要避免的常见错误吗?有些事情非常简单,比如对一个数字进行平方运算(x * x vs squared(x))这是一个过于简单的例子,但如果你能指出某个人犯过的具体错误 - 特别是在你的项目或你的团队中 - 你就可以展示一个常见的类或函数,以避免未来发生这种错误。

如果在不太可能出现新需求的情况下,我的代码变得太混乱,我仍然可以为新需求进行重构。所以你的“如果以后需要…”的论点无效。

问题在于“不太可能”的假设。你同意这是不太可能的吗?如果是,你就同意了这个人的观点。如果不是,你的设计想法与这个人的不一致 - 解决这个差异将解决问题,或者至少向你展示下一步该如何去做。 :)


11

我喜欢用“半个,不是半吊子”的方式来思考YAGNI,借用了37signals的这个短语 (https://gettingreal.37signals.com/ch05_Half_Not_Half_Assed.php)。它的含义是限制你的范围,这样你就可以专注于做好最重要的事情。它不是懈怠的借口。

对我来说,在GUI中处理业务逻辑感觉有些草率。除非你的系统非常简单,否则我会惊讶于你的业务逻辑和GUI已经独立变更了多次。所以你应该遵循SRP(SOLID中的"S")并进行重构-因为你已经需要了,所以YAGNI不适用。

如果你正在做额外的工作来适应假设的未来需求,那么关于YAGNI和不必要的复杂性的争论绝对适用。当那些“如果后来我们需要…”的场景未能发生时,你就会被高昂的维护成本困扰,因为现在那些抽象化的东西已经妨碍了你实际所做的更改。在这种情况下,我们谈论的是通过限制范围来简化设计-做一半,而不是草率地完成。


8
似乎你正在和一堵墙争论。我很喜欢YAGNI,但同时,我也期望我的代码至少会被用在两个地方:应用程序和测试中。这就是为什么在UI代码中使用业务逻辑是行不通的原因;在这种情况下,你无法单独测试业务逻辑而不涉及UI代码。
然而,从你描述的回复中,似乎这个人只是对做得更好没有兴趣。在这种情况下,没有任何原则可以帮助他们;他们只想做最少的工作。我敢说,驱动他们行动的不是YAGNI,而是懒惰,而你一个人无法战胜懒惰(除了威胁性的经理或失业)。

我有一种感觉,那些人想要快速实现功能/需求,测试主要用例,然后就忘记了。等到代码部署后再修复问题。客户有点像验收测试员。 - bitbonk
@bitbonk:是的,这是一个非常常见的情况。只要您告知客户他们将成为测试人员(并且他们同意),这可以是一种合法的工作方式。然而,并不是每个人都会这样做 :-)。 - sleske

3
没有一个确定的答案,或者说,有一个答案,但你和你的对话者都可能不喜欢:YAGNI 和 SOLID 都可能是错误的方法。
试图在没有经验的团队或者具有紧迫交付目标的团队中采用 SOLID 方法,几乎可以保证你最终会得到一堆昂贵且工程化过度的代码…… 这些代码不会是 SOLID 的,只会是过度工程化(又称欢迎来到现实世界)。
长期项目如果一开始尝试采用 YAGNI,然后希望以后能够重构,这种方法只有在一定程度上有效(又称欢迎来到现实世界)。YAGNI 适用于概念验证和演示,获取市场/合同,然后能够投资于更加 SOLID 的东西。
在不同的时间点上,你需要两者都要。

2

优质的单元测试,我指的是单元测试而不是集成测试,需要遵循SOLID原则的代码。并非必须完全遵守,实际上很少有这样的情况,但在你的例子中,将两个功能塞进一个类中会使单元测试更加困难,违反了单一职责原则,并且使新手团队维护代码变得更加困难(因为更难理解)。

通过单元测试(假设覆盖率良好),您将能够安全地重构功能1,并确保不会破坏功能2,但如果没有单元测试并且将功能放在同一个类中(仅仅是为了懒惰),那么重构最多是有风险的,最坏的情况是灾难性的。

底线:遵循KIS原则(保持简单),或者对于知识分子来说,遵循KISS原则(保持愚蠢)。根据具体情况进行判断,没有通用答案,但始终要考虑其他程序员是否需要阅读/维护未来的代码以及在每种情况下单元测试的好处。


实际上,当特性相互关联时,将多个特性塞入一个类中并不一定会使事情变得更简单。为了满足SOLID原则,保持相互关联的特性分开可能会非常复杂,不仅在技术上,而且在智力上也是如此(也就是说,保持简单可能很难)。 - Eric Grange

2
这些原则的正确应用通常并不是很明显,非常依赖于经验。如果您没有亲身经历,那么就很难获得这种经验。每个程序员都应该有过做错事情后的经历,但当然总是会认为这不是“我的”项目。
向他们解释问题所在,如果他们不听并且您没有权力让他们听,就让他们犯错误吧。如果您经常需要修复问题,那么您应该修改您的简历。

1
“Polish your resume”?你的意思是,找到一份更好的工作吗? - bitbonk
2
是的,当麻烦太多时,最好放手。毕竟,有些公司重视这种思维方式。 - knitti

2
根据我的经验,这通常是个判断问题。是的,你不应该担心实现的每一个小细节,有时将方法插入到现有类中是可以接受的,尽管这是一种丑陋的解决方案。
确实,你可以以后重构代码。重要的是实际上进行重构。因此,我认为真正的问题不是偶尔的设计妥协,而是一旦出现问题就推迟重构。实际上,完成它才是难点(就像生活中的许多事情一样...)。
至于你的个人观点:
“把X特性和Y特性放在同一个类中没关系,这很简单,为什么要添加新类(即复杂性)。”
我想指出,把所有东西都放在一个类中是更加复杂的(因为方法之间的关系更紧密,更难理解)。拥有许多小类并不复杂。如果你感觉列表变得太长了,只需将它们组织成包,你就会没问题:-)。就个人而言,我发现只是将一个类分成两个或三个类可以大大提高可读性,而不需要任何进一步的改变。
不要害怕小类,它们不会咬人;-)。
“是的,我可以直接将所有业务逻辑放入GUI代码中,这样更容易快捷。这将永远是唯一的GUI,而且极不可能会出现重大新需求。”
如果有人可以毫不犹豫地说“极不可能会出现重大新需求”,我认为这个人真的需要一个现实检查。直言不讳,但温和...
“如果在不太可能出现新需求的情况下,我的代码变得太混乱,我仍然可以为新需求进行重构。所以你‘如果以后需要...’的论点不成立。”
这有一些优点,但只有他们实际上确实进行了重构才行。所以接受它,并要求他们遵守自己的承诺:-)。

4
确保有适当数量和适当大小的类。如果把它们做得太小,你就会把复杂性放到类之间的连接中,这并没有帮助。 - Donal Fellows
2
@Donal Fellows:是的,那是真的。我的经验法则是在每个类中至少放置一个非微不足道的功能-这样做相当有效。但说实话,我从未见过一个拥有太多小类的真实系统,相反,很多系统都有很多大类,所以大多数人似乎会倾向于“大类”那一边。 - sleske
1
我见过两种情况(一些知名框架在这方面确实很糟糕)。我只是试着记住一个问题有一定的“自然”复杂度,并且我尽量确保解决方案具有该复杂度水平,同时尽可能少地增加额外的复杂度。 - Donal Fellows

2

SOLID原则允许软件适应变化-无论是需求变化还是技术变化(如新组件等),您的两个论点都是针对不变的需求:

  • “极不可能出现重大新需求。”
  • “如果出现了不太可能的新要求”

这真的可能吗?

在开发的各种费用方面,经验是无可替代的。我认为,对于许多从业人员来说,以糟糕、难以维护的方式进行操作从未给他们带来问题(嘿!工作安全)。从长远来看,我认为这些开销变得清晰明了,但提前做出改变是别人的工作。

这里还有其他一些很好的答案。


工作安全性非常重要,我已经看到这种情况很多次了,感到非常不安。 - d-_-b

2
易于理解,灵活且可以修复和改进的能力始终是您需要的。事实上,YAGNI假定您可以轻松地回来添加新功能,因为当它们被证明是必要的时,没有人会像在一个类中添加不相关的功能(在该类中不需要YAGNI!)或将业务逻辑推到UI逻辑。
有时候现在看起来很疯狂的事情过去是合理的 - 有时候UI与业务之间的边界线或应该在不同类中的不同责任集之间的边界线并不那么清晰,甚至会移动。有时候3小时的工作在2小时后绝对是必要的。有时候人们只是没有做出正确的选择。由于这些原因,偶尔会发生这方面的突破,但它们将妨碍使用YAGNI原则,而不是导致它。

1

tldr;

SOLID和YAGNI是关于代码设计的两种不同思想。SOLID假设你能够预测未来的变化并据此设计类,而YAGNI则认为你无法预测未来的变化,因此应该将所有相关代码放在一个类中。SOLID要求为代码设计单一职责的类,而YAGNI则认为这样做可能会增加不必要的复杂性。

详细解释:

阅读书籍“敏捷软件开发:原则、模式与实践”可以更好地理解SOLID/SRP。

SOLID要求设计类时应考虑到代码变化的单一原因,例如小的GUI变化或ServiceCall变化。如果应用程序不会以导致这两个职责在不同时间发生变化的方式发生变化,则没有必要将它们分开。相反,将它们分开会增加不必要的复杂性。

这里有一个相关性。只有发生变化,改变的轴才是改变的轴。如果没有症状,应用SRP或其他原则都不明智。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接