代码简洁之道:'两个就是太多'原则解析

本文深入探讨了Max Kanat-Alexander提出的"两个就是太多"编程原则,通过具体案例讲解如何在软件开发中避免重复代码,实现渐进式重构,并平衡代码通用性与简洁性,提高代码可维护性。

两个就是太多

2015年12月21日 by Max Kanat-Alexander

在进行增量开发和设计时,我个人遵循一个关键规则,我称之为"两个就是太多"。这是我实现《软件设计三大缺陷》中"只需要做到必要的通用性"规则的方式。

本质上,我通过注意到自己想要复制粘贴某些代码,然后不是复制粘贴,而是设计一个刚好满足这两个特定需求的通用解决方案,来准确知道我的代码需要多通用。一旦我想要某个东西有两个实现,我就会立即这样做。

例如,假设我正在设计一个音频解码器,最初我只支持WAV文件。然后我想在代码中添加一个MP3解析器。WAV和MP3解析代码肯定有共同的部分,我不会复制粘贴任何部分,而是立即创建一个超类或实用程序库,只做这两个实现需要的事情。

这个规则的关键方面是我立即这样做——我不允许存在两个竞争的实现;我立即创建一个通用解决方案。下一个重要方面是我不让它过于通用——该解决方案只支持WAV和MP3,不以任何方式期望其他格式。

这个规则的另一个部分是,理想情况下,开发人员永远不应该以类似或相同的方式修改代码的一个部分,就像他们刚刚修改了另一个部分一样。他们不应该在更新B类时"记住"更新A类。他们不应该知道如果常量X改变,就必须更新文件Y。换句话说,不仅两个实现不好,两个位置也不好。虽然并不总是可能以这种方式实现系统,但这是值得努力的目标。

如果你发现自己不得不为某个东西设置两个位置,请确保当它们不同步时,系统会明显且可见地失败。编译应该失败,总是运行的测试应该失败等。应该不可能让它们不同步。

当然,这个规则最简单的部分是经典的"不要重复自己"原则——不要有两个代表完全相同事物的常量,不要有两个做完全相同事情的函数等。

这个规则可能还有其他适用方式。总体思路是,当你想要一个概念的两种实现时,你应该以某种方式将其变成一个实现。

在重构时,这个规则有助于找到可以改进的地方,并提供一些关于如何进行的指导。当你看到系统中的重复逻辑时,你应该尝试将这两个位置合并为一个。然后如果有另一个位置,将其合并到新的通用系统中,并以此方式进行。也就是说,如果有许多不同的实现需要合并为一个,你可以通过一次合并两个实现来进行增量重构,只要合并它们确实使系统更简单(更容易理解和维护)。有时你必须找出合并它们的最佳顺序以使其最高效,但如果你无法弄清楚,不用担心——只需一次合并两个,通常你会得到一个解决所有问题的单一好方案。

同样重要的是,不应该合并的东西不要合并。有时将两个实现合并为一个会导致整个系统更复杂或违反单一职责原则。例如,如果你的系统中Car和Person的表示有一些稍微相似的代码,不要通过将它们合并为一个CarPerson类来解决这个"问题"。这不太可能降低复杂性,因为CarPerson实际上是两个不同的东西,应该由两个单独的类表示。

这不是宇宙中硬性的法则——更多的是我在增量开发时用于判断设计的强烈指导原则。然而,它在重构遗留系统、开发新系统以及一般提高代码简洁性方面非常有用。

-Max

22条评论

Alex Vincent 说: 2015年12月21日下午1:51 就个人而言,当有三个或更多不同的实现时,我会进行合并。当然,一个是最理想的。两个让我感觉不舒服,但有时有充分的理由。三个就完全不可接受了,是马虎代码的标志。 回复

David Cuccia 说: 2015年12月21日下午3:45 我同意Alex的观点。在我早期,即使只有一个实现,我也会过度设计某些东西。现在,我通常等到代码需要在三个不同的地方时才集中处理。很多时候,维护中央库的负担超过了避免同步问题带来的温暖感。 回复

David Cuccia 说: 2015年12月21日下午3:45 (避免同步问题) 回复

Codesimplicity – Two is too many – yaleman.org 说: 2015年12月21日晚上9:51 […] Codesimplicity – 两个就是太多 […] 回复

Jens Bannmann 说: 2015年12月22日上午3:42 嗯,我同意Max的观点:两个确实太多了。 虽然在理论上,Alex等待第三个实现的方法感觉还可以,但我的经验表明,多年来,其他开发人员通常会盲目地跟随两个不同实现建立的先例。等到有人最终想到合并它们时,可能不是三个,而是例如七个竞争的实现。在我看来,几乎总是从一开始就做更好。只有一个统一的实现也会提高可读性,帮助新开发人员理解系统。 回复

Max Kanat-Alexander 说: 2016年9月18日晚上10:35 是的,根据我的经验,“两个就是太多"方法的优点是它可靠地有效。可能有其他好的理论原因使用另一种方法,但我只是没有看到它们实际上能保持代码库的可维护性。 -Max 回复

Ian Thomas 说: 2015年12月23日上午6:21 “三个就是太多"方法的一个问题是,当你开始编写第三个实现时,你现有的实现可能已经分叉,在它们可以合并成一个通用实现之前需要做一些工作。 回复

Max Kanat-Alexander 说: 2016年9月18日晚上10:34 是的,它有很多问题。另一个问题是,注意到有两个实现比注意到有三个实现要容易得多。也就是说,可能有三个实现,但开发人员只找到另一个实现,所以认为只有两个,并等待有三个。是的,正如你指出的,合并三个实现可能比合并两个然后在你需要它做稍微不同的事情时增强合并的解决方案要困难得多。在非常大的代码库中,这一点变得非常明显,在某些情况下甚至合并两个现有实现都很困难。 回复

Alex Vincent 说: 2015年12月23日上午7:08 就我而言,我并不是说这是一个硬性规定。我发现,很大一部分与你有多少能力和影响力首先做出这样的改变有关……但这更多是关于项目/时间管理,而不是一个普通高级工程师想做正确的事情。 我说两个实现让我感觉不舒服。在可行的情况下,我会尝试合并它们,而不是等待第三个。 回复

Mark 说: 2015年12月23日上午8:40 谢谢提醒,Max。好东西。 回复

Jim 说: 2015年12月31日下午4:40 我同意在Max的例子中,CarPerson类是一个糟糕的(实际上,我认为是可怕的)想法;然而,有时将完全相同或几乎完全相同的代码或逻辑提取到一个单独的类或模块中,并通过你所使用语言中可用的任何适当机制(例如,某种类型的继承,或在Ruby中,“include"机制)在任何适当的类(Max例子中的Car和Person)中使用这个共享代码/逻辑是有用的。这样的设计可能在Car与Person的例子中是合适的(尽管没有具体细节,我不能肯定)。我相信,在Meyer的继承分类中(http://se.ethz.ch/~meyer/publications/computer/taxonomy.pdf),这种情况通常会被归类为"实现继承”。 回复

Anil 说: 2016年1月10日凌晨1:29 再同意不过了。我们需要特别注意在维护别人写的旧代码时处理这个问题。在那里,我反复遇到,不仅是2个,而是至少5-6个地方需要做相同的更改。我从事软件开发已经10年了(从C开始,然后转到C++,现在是Java),我正在将我的方法从OOP转向函数式,并看到生产力的显著提高。 回复

abdullah 说: 2016年2月19日上午8:35 作者有很多有效的观点,绝对每个开发人员都应该努力实现提到的原则。但回到现实世界,预算和时间可能是个问题,你如何证明改变一个已经功能正常的系统的一部分是合理的?你的老板会高兴你现在必须回去测试一些不在项目范围内的东西吗? 这个问题的另一个方面是,花时间做一个通用的实现可能很棒——但如果你从不重用那段代码呢?因此你的解决方案变得过度工程化,这意味着更长的开发周期(以及修复bug的更多努力)。 作为一个通用原则,这种方法是有道理的,但像任何事情一样,它需要适度和常识来调和。 回复

Shalaka Virkar 说: 2016年5月11日下午12:16 这是一个非常有效的担忧。但我认为这是单元测试发挥非常重要作用的地方。如果你有好的单元测试作为安全网,那应该能帮助你发现是否破坏了任何东西。如果需要,你应该添加更多。话虽如此,端到端测试是没有替代品的。一个好的测试自动化框架应该能处理这个问题。如果时间允许,你也可以进行手动测试。 回复

Max Kanat-Alexander 说: 2016年9月18日晚上10:25 我在《测试哲学》中详细讨论了这一点。 回复

Max Kanat-Alexander 说: 2016年9月18日晚上10:29 Abdullah,总的来说,我建议你阅读这本书,它回答了所有这些问题以及更多。 -Max 回复

Kamran 说: 2016年2月20日下午3:37 我编程多年,从高中就开始弹钢琴和学习音乐(我47岁)。我看到编程和写严肃音乐之间有很多相似之处。音乐中有句谚语,大意是:“如果能让你的音乐听起来更好,你可以忽略音乐中的任何规则”。编程或音乐(以及许多其他领域)的规则或模型只是一些指导,一旦你掌握了它们,你就会看到它们的优缺点,然后可以在你想的时候不使用它们,即使别人说,“嘿,你做的这件事不对!",如果你知道自己在做什么,就让他们说去吧。 回复

Shalaka Virkar 说: 2016年5月11日下午12:35 这是一个非常有效的担忧。但我认为这是单元测试发挥非常重要作用的地方。如果你有好的单元测试作为安全网,那应该能帮助你发现是否破坏了任何东西。如果需要,你应该添加更多。话虽如此,端到端测试是没有替代品的。一个好的测试自动化框架应该能处理这个问题。如果时间允许,你也可以进行手动测试。 回复

Dave Beck 说: 2016年7月3日下午1:21 复制和粘贴就像往嘈杂的变速箱里倒锯末——看起来你取得了进展,但你以后会付出代价。 回复

Code Simplicity » Kindness and Code 说: 2017年8月12日上午8:07 […] 这行代码很难理解,这看起来像代码重复。你能重构它,使得它[…] 回复

Kindness and Code - Best Application Development Platforms, Software, App Developers 说: 2017年8月16日凌晨4:18 […] 这行代码很难理解,这看起来像代码重复。你能重构它,使得它[…] 回复

Ruslan Spivak: Let’s Build A Simple Interpreter. Part 15. – Cebu Scripts 说: 2019年6月22日凌晨4:10 […] 我们可以移除上面的手动步骤(b),并只在一个地方保留令牌类型。这是"两个就是太多"规则在起作用——向前看,添加新关键字唯一需要做的更改是[…] 回复

留下回复取消回复

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计