Yagni
2015年5月26日
Yagni最初是“You Aren't Gonna Need It”(你不需要它)的首字母缩略词。它来自极限编程的口头禅,通常在敏捷软件团队中广泛使用。它表明,我们假设软件将来会需要的某些功能现在不应该构建,因为“你不需要它”。
Yagni是用来指代XP实践中的简单设计(来自白皮书的第一版,第二版提到了相关的“增量设计”概念)。[1] 就像XP中的许多元素一样,它与90年代后期广泛流行的软件工程原则形成了鲜明的对比。当时,人们大力推崇对软件开发进行仔细的前期规划。
让我们想象一下,我在米那斯提力斯与一家创业公司合作,为航运业提供保险。他们的软件系统分为两个主要部分:一个用于定价,另一个用于销售。它们之间的依赖关系使得在完成相关定价软件之前,无法有效地构建销售软件。
目前,团队正在更新定价组件,以添加对风暴风险的支持。他们知道,六个月后,他们还需要支持对海盗风险的定价。由于他们目前正在处理定价引擎,他们考虑现在构建对海盗定价的假定功能[2],这样定价服务将在他们开始处理销售软件之前完成。
Yagni反对这种做法,它认为,既然六个月内不需要海盗定价,就不应该在需要之前构建它。因此,如果你认为构建这个软件需要两个月,那么你应该再过四个月才开始(不考虑任何用于计划风险和更新销售组件的缓冲时间)。
Yagni的第一个论据是,虽然我们现在可能认为需要这个假定功能,但我们很可能错了。毕竟,敏捷方法的背景是接受不断变化的需求。一个计划驱动的需求专家可能会反驳说,这是因为我们没有做好需求分析工作,我们应该投入更多的时间和精力。我反驳说,提前弄清楚你的需求是多么困难和昂贵,但即使你能做到,当冈多海军消灭海盗时,你仍然会被蒙在鼓里,从而破坏整个商业模式。
在这种情况下,假定功能有一个明显的成本——**构建成本**:分析、编程和测试这个现在无用的功能所花费的所有努力。
但是,让我们假设我们对需求的理解完全正确,冈多海军并没有消灭海盗。即使在这种理想的情况下,构建假定功能也会产生两种严重的成本。第一个成本是**延迟价值成本**。通过将我们的精力投入到海盗定价软件上,我们没有构建其他功能。如果我们把精力投入到构建风暴风险的销售软件上,我们就可以将一个完整的风暴风险功能投入生产,并在两个月前开始产生收入。由于假定功能造成的这种**延迟成本**是两个月的风暴保险收入。
人们构建假定功能的常见原因是,他们认为现在构建比以后构建更便宜。但是,这种成本比较至少要与延迟成本进行比较,最好还要考虑你构建不必要功能的可能性,这种可能性至少是⅔。[3]
通常,人们不会考虑现在构建与以后构建的比较成本。我在指导开发人员处理这种情况时使用的一种方法是让他们**想象一下**以后需要引入该功能时需要进行的重构。这种思想实验通常足以说服他们,以后添加它不会贵很多。这种想象的另一个结果是添加一些现在很容易做的事情,增加最小的复杂性,但显著降低以后的成本。使用查找表来显示错误消息而不是内联文字是一个例子,它很简单,但使以后的翻译更容易支持。
提醒一下,任何从未使用过的扩展点不仅是浪费的努力,而且很可能也会妨碍你。
延迟成本是成功假定功能带来的一个成本,但另一个是**维护成本**。假定功能的代码会增加软件的复杂性,这种复杂性会使软件更难修改和调试,从而增加其他功能的成本。在软件中添加海盗定价功能带来的额外复杂性可能会使构建风暴保险销售组件所需的时间增加几周。这四周的影响有两个方面:构建功能的额外成本,以及由于构建时间更长而造成的额外延迟成本。在海盗保险软件开始发挥作用之前,我们将在构建的每个功能上产生维护成本。如果我们永远不需要海盗定价软件,我们将为构建的每个功能产生维护成本,直到我们删除海盗定价功能(假设我们确实删除了它),以及删除它的成本。
到目前为止,我已经将假定功能分为两类:成功和不成功。当然,这里实际上有一个频谱,并且在这个频谱上有一个值得强调的点:正确的功能构建错误。开发团队总是在学习,包括关于他们的用户和他们的代码库。他们了解他们使用的工具,这些工具会定期升级。他们还了解他们的代码是如何协同工作的。所有这些意味着,你经常会意识到,六个月前编写的功能并不是你现在意识到应该编写的方式。在这种情况下,你已经积累了技术债务,并且必须考虑修复该功能的**修复成本**,或者围绕其困难进行工作的持续成本。
因此,我们最终得到了三类假定功能,以及忽略Yagni时产生的四种成本。
我的保险示例谈论的是相对用户可见的功能,但同样的论点也适用于支持未来灵活性的抽象。在构建风暴风险计算器时,你可能会考虑现在添加抽象和参数化,以支持以后的海盗和其他风险。Yagni说不要这样做,因为你可能不需要其他定价功能,或者如果你需要,你目前对需要哪些抽象的想法将与你在实际需要时学到的东西不符。这并不意味着要放弃所有抽象,但这确实意味着任何使当前需求代码更难理解的抽象都被认为是有罪的。
Yagni在较大的功能中最为明显,但在较小的功能中更常见。最近,我编写了一些代码,允许我突出显示代码行的一部分。为此,我允许使用正则表达式来指定突出显示的代码。我发现这个问题的一个问题是,由于整个正则表达式都被突出显示,我无法处理需要正则表达式匹配比我想突出显示的更大部分的情况。我希望我可以通过在正则表达式中使用一个组,并在存在组的情况下让我的代码只突出显示该组来解决这个问题。但是,我还没有需要使用匹配比我突出显示的更多内容的正则表达式,因此我还没有扩展我的突出显示代码来处理这种情况——并且只有在我真正需要它的时候才会这样做。出于类似的原因,我不会在真正准备好使用它们之前添加字段或方法。
像这样的小的Yagni决策会飞过项目规划的雷达。作为一名开发人员,很容易花一个小时添加一个我们确信很快就会需要的抽象。然而,上面所有的论点仍然适用,许多小的Yagni决策加起来会显著降低代码库的复杂性,同时加快对更紧急需要的功能的交付。
现在我们了解了为什么Yagni很重要,我们可以深入研究关于Yagni的一个常见误解。**Yagni只适用于构建到软件中的支持假定功能的功能,它不适用于使软件更容易修改的努力。** Yagni只有在代码易于更改的情况下才是一种可行的策略,因此在重构上花费精力并不违反Yagni,因为重构使代码更具可塑性。类似的推理适用于自测试代码和持续交付等实践。这些是演化设计的支持性实践,没有它们,Yagni就会从一种有益的实践变成一种诅咒。但是,如果你确实拥有一个可塑的代码库,那么Yagni就会加强这种灵活性。Yagni具有一个奇怪的特性,即它既被演化设计所支持,又支持演化设计。
Yagni不是忽视代码库健康状况的理由。Yagni需要(并支持)可塑的代码。
我还认为,Yagni只适用于你现在引入的额外复杂性,直到以后才会利用。如果你为未来的需求做了一些实际上不会增加软件复杂性的事情,那么就没有理由调用Yagni。
话虽如此,有时应用Yagni确实会导致问题,你会面临一个昂贵的更改,而早期的更改会便宜得多。这里棘手的是,这些情况很难提前发现,而且比Yagni节省了多少努力更容易记住[4]。我的感觉是,Yagni失败的情况相对较少,它们的成本很容易被Yagni成功时的成本所抵消。
注释
1: 这个短语的起源是Kent Beck和Chet Hendrickson在C3项目上的早期对话。Chet找到Kent,列出了一系列系统很快就会需要的功能,Kent对每一个功能都回答“你不需要它”。Chet是一个快速学习的人,很快就以他发现应用Yagni的机会的能力而闻名。虽然“Yagni”最初是一个首字母缩略词,但我认为它现在已经成为我们词汇中一个普通的词,因此不再使用大写字母。
2: 在这篇文章中,我使用“假定功能”来指代任何支持尚未提供使用的功能的代码。
3: ⅔这个数字是Kohavi等人提出的,他们分析了微软产品中构建和部署的功能的价值,发现即使经过仔细的前期分析,只有⅓的功能改善了它们旨在改善的指标。
4: 这是可用性偏差的结果