二即为多:提升代码简洁性的关键法则

本文探讨了"二即为多"的编程原则,强调在发现需要复制粘贴代码时立即创建通用解决方案的重要性。通过音频解码器等实例,说明了如何避免重复实现、保持代码简洁,以及在重构过程中逐步合并相似逻辑的方法。

二即为多

2015年12月21日 by Max Kanat-Alexander

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

本质上,当我发现自己想要剪切粘贴某些代码时,我就知道我的代码需要达到什么样的通用程度。这时我不会去剪切粘贴,而是设计一个刚好满足这两个特定需求的通用解决方案。一旦我想要对某个东西有两个实现时,我就会立即这样做。

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

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

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

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

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

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

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

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

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

评论

Alex Vincent 说:2015年12月21日下午1:51

就个人而言,当有三个或更多不同的实现时,我会进行合并。当然,一个是最理想的。两个让我感觉不舒服,但有时有充分的理由。三个就不可接受了,是马虎代码的标志。

David Cuccia 说:2015年12月21日下午3:45

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

Jens Bannmann 说:2015年12月22日上午3:42

嗯,我同意Max的观点:两个确实太多了。

虽然在理论上,Alex等待第三个实现的方法感觉还可以,但我的经验表明,多年来,其他开发人员通常会盲目地遵循两个不同实现所建立的先例。等到有人终于想到要合并它们时,可能不是三个,而是例如七个竞争的实现。在我看来,几乎总是从一开始就做对更好。只有一个统一的实现也会增加可读性,并帮助新开发人员理解系统。

Ian Thomas 说:2015年12月23日上午6:21

“三个太多"方法的一个问题是,当你开始编写第三个实现时,你现有的实现可能已经分叉,在它们可以合并为一个通用实现之前需要做一些工作。

Jim 说:2015年12月31日下午4:40

我同意在Max的例子中,CarPerson类是一个糟糕(实际上,我认为是可怕)的想法;然而,有时将完全相同或几乎完全相同的代码或逻辑提取到一个单独的类或模块中,并通过你所使用语言中可用的任何适当机制(例如,某种类型的继承,或者在Ruby中,“include"机制)在任何适当的类(Max例子中的Car和Person)中使用这个共享的代码/逻辑是有用的。这样的设计在Car与Person的例子中可能是适当的(尽管没有具体细节,我不能肯定)。

Anil 说:2016年1月10日上午1:29

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

abdullah 说:2016年2月19日上午8:35

作者有很多有效的观点,肯定每个开发人员都应该努力追求提到的原则。但在现实世界中,预算和时间可能是个问题,你如何证明更改已经功能正常的系统的一部分是合理的?你的老板会高兴你现在必须回去测试一些不属于项目范围的东西吗?

这方面的另一个问题是,花时间制作一个通用的实现可能很棒——但如果你从不重用那段代码呢?因此你的解决方案变得过度设计,这意味着更长的开发周期(以及修复bug的更多努力)。

作为一个通用原则,这种方法是有道理的,但像任何事情一样,它需要适度和常识来调和。

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