DRY, KISS, but you may gotta need it
这次来讨论一下YAGNI。在这个blog里面我也很多次表达了对YAGNI的不认可,不过,这也是有原因的。
首先是看到dev和商业人士大谈「为什么我们需要YAGNI」、「清洁的设计」、「满足客户的需要」。看起来像是软件工程里面常见的大道理。不过这里关于YAGNI的讨论就很有意思了。
首先看看YAGNI是怎么定义的:
YAGNI: Then I would say that the first thing to assure is to remove unnecessary code. Otherwise you can spend a lot of time and energy on unnecessary or soon-to-be-removed product. The job isn’t to write code. The job is to do the job — nothing less, but also nothing more. YAGNI along with making it work guarantee that we have just enough code in our editor.
翻译一下就是:
删掉代码中所有「不需要」的代码。写代码不是工作,工作就是工作,既不能多,也不能少。
这里我就先不给出说人话的翻译了。
有人会觉得这样的定义非常武断,想要反驳:
我的理解是,YAGNI原则强调只关注当前具体需求中直接有用的部分。例如,在《清洁代码》中,我只采用对我当前情境中立即有用的10%内容。根据这个原则,如果一个问题可以用一个类解决,我就只写一个类;如果需要2-3个类,简单复制粘贴优于复杂的抽象;但当面对更复杂的需求时,我会考虑构建合适的抽象。简而言之,YAGNI就是在所有合理方案中选择最简单的一种。
这时候,就会有人无情地指出漏洞:
所以对你而言,开发一些“将来会用到”的东西(例如,现在只有一种用途或实现,却让一个类变得通用)难道不是对YAGNI原则的违反吗?
那么我想说:既然定义如此,那么我不认同YAGNI。关于设计原则的讨论看起来就像是一种奇怪的宗教信仰,我既不对「把某些良好的架构设计和选择人为地标签化」感兴趣,也不想认同一个否定了抽象和封装的「设计原则」——因为我坚信计算机程序的核心是抽象,包括对现实世界和业务建模的抽象。计算机程序的核心不是「不多不少地把客户的要求写出来」。
也许有人会说「YAGNI不是这样,这只是有些人念歪经了」,但是从不同社区和不同的程序员的理解观察下来,我觉得,抱有前面引用的观点的程序员或者非程序员绝对不在少数。
不过话说回来,哪怕这是念歪经了又怎样呢,也许有人觉得他真正理解了YAGNI,认为前面讨论YAGNI的人讨论不到点上,那么,能够提出一个比前面的「在所有合理方案中选择最简单的一种」更有说服力,而不会被「现在只有一种用途或实现」辩倒的例子吗。
接下来就来聊一下为什么我不认同YAGNI。
首先还是拿之前讨论表驱动的那个打印月份的函数的算法例子。如果还记得的话,那个例子里面,用了一个字符串数组来存入每个月份,然后读取用户输入的月份序数,打印出对应的月份名。
不难看出,哪怕是这么基础的一个例子,也能展现出封装和抽象的一个场景:一个常用的实现是手写12个if或者switch,而这个控制流就被一个查表的过程给抽象出来了。
这样的好处是什么呢?方便维护和易于扩展。
更进一步说,这个代码例子也可以被进一步抽象,例如把核心逻辑提取到一个函数里,使用依赖注入让表格可以是不同语言的月份,把利用副作用的打印改造成无副作用的返回值,之类的。
就像我之前在别的文章里提到的,这一切并不需要你写100个工厂、200个构造器和300个单例。
所以,其实我并不反对DRY和KISS。反过来说,KISS还能让人想起经典的Less is more的哲学。
但是,「你不需要」是一个很强烈的断言。而把软件设计和「不多不少地满足客户」打包在一起,更是一件令人困惑的事情。更不用说,这种「不多不少」直接否定了设计的意义,拒绝了抽象,要求程序员「永远只根据给定的需求做」,可是,哪怕是LeetCode的算法题,也不会因为程序员写了个泛型类型而扣分吧。
此外,软件项目的很多重要设计都是强烈受着项目早期代码的影响的。换句话说, 早期的设计基本上为项目的未来定型了,也注定了项目后期的演化。
一个很典型的例子是关于并行和并发的。一般来说,讨论到并发,从线程到线程池,到结构化并发,可组合的任务,最后到协程,是一个层层递进,一层一层向上抽象的过程。
而对于YAGNI来说,线程往后的所有概念,you ain't gotta need it,所以都不存在了。所以新建一个Web项目后该做什么呢,单线程,锁,最后死锁套死锁,Loom来了都救不了。
另一个例子就是单体项目之于微服务。在微服务和容器化日渐成为主流的时代,单体项目却多受制于其早期的技术债务而无法改造为可伸缩的弹性架构。
还有一个例子是Java项目的升级问题。跟C#/.NET不同,Java的JDK是向后兼容的,过时的API也会提前通知警告,而Java从旧版本升级到LTS得到的性能和安全性改善更是有目共睹的。但即使如此,到Java21的时代,还是有很多停留在旧版的代码,理由是「升级麻烦bug一堆」。
例如这个从1.6升级到1.7的问题
但是仔细一看,HashMap本就不保证依赖关系,JDK的某个特定版本恰好保证了这个依赖关系也只是一种巧合,依赖这种假设来实现的代码,感觉就是在面向bug编程了。
这也是一种YAGNI吗。
说到底,任何软件项目的演化中,都应该有自我反思和自我批评的过程,哪怕是我不喜欢的敏捷设计,里面也有这样的过程。而YAGNI,感觉就是以一种傲慢的态度把这给粗暴的删去了。
也许短期来看,这一切都不重要, you ain't gotta need it,甚至项目组和程序员可以短视地少付出时间或者金钱上的成本。但是长远来看,代码会腐坏不是没有道理的。
无论如何,对未来的需求保持开放的态度,不设前提地考虑各种可能,理性讨论并留有恰当的余地,总是更合适的,也总比一句粗暴的YAGNI更得体。
说到最后,也许某个年代,YAGNI本身具有其价值,也许它也指导过一些合理的设计。但是在否定程序设计和否定计算机理论日益成为主流的现在,我觉得YAGNI的弊端已经很明显了。
要为未来的优化预留空间与否,或者是否去做优化、重构,都是值得讨论并且有讨论价值的事情。也许在有些场合,这样的需求不太能被justify,那么hard code一些东西的同时,也应该意识到这样暗含的风险。我想,作为软件工程师,更适合的,不是武断地说YAGNI,而是去构思一下可能出现的情况,并且做好预案。
如果真的想要追求什么原则上的哲学的话,去追求组合吧。只要lambda、macro和continuation就能构建出所有绝大部分程序员需要或者不需要的feature。
当然,YAGNI的鼓吹者们也许会说,you ain't gotta need it。
So, you choose it.