大家发现没,设计原则通常都有一个很普遍的特点:语言很简练,听上去很有道理,但是拿来指导实践根本无从下手。比如说,在学习简单原则(也就是我们平时说的 KISS 原则) 时,你可能遇到过下列问题:

  • 为什么身边的程序员都告诉你保持“简单”代码很重要?

  • 什么才是好的“简单”代码?

  • 如何能写出“简单”的代码?

  • YAGNI 原则和 KISS 原则是相同的吗?

这些问题看上去简单,回答起来却不简单。所以,今天我们就带着这些问题,重新开始学习 KISS 原则。

一、为什么要让代码保持“简单”?

KISS 原则(Keep It Simple and Stupid),翻译过来就是:保持简单,保持愚蠢

KISS 原则自 1970 年起就开始得到广泛使用。现在在软件开发中,KISS 原则通常是指代码应该更容易理解、更容易编写、更容易改变,并应该一直保持如此。

可能你会疑问:为什么非得要让代码保持“简单”呢?写出来能运行不就好了吗?其实主要有以下三个重要的原因。

第一,防止代码腐坏。编程里有一个很大的矛盾:刚开始时一切都好,代码简单可读,逻辑清晰,可随着时间的推移,特别是当越来越多的人参与到项目中时,代码数量呈指数级增长,功能越来越丰富,逻辑却越来越复杂,代码变得难以理解和难以维护,这时代码就开始变得腐坏。保持代码简单性一方面可以降低模块的理解难度,另一方面也让代码修改的范围边界更清晰,这样就能有效地防止代码腐坏。

第二,减少时间成本的投入。我们知道软件复杂度有三个来源:代码规模、技术复杂度和实现复杂度。通常来说,代码越复杂,在这三方面投入的时间成本就会越多。比如,维护 10 万行代码和维护 1000 行代码所需要投入的理解与逻辑梳理时间是不同的。再比如,使用 Redis 作为缓存和使用 LRU 本地缓存所需要的学习时间也是不同的。

对于开发人员来说,维护代码必然需要花时间修改、调试、理解内在逻辑并让代码正常运行,一旦代码非常复杂,势必会增加时间成本,进而让开发团队的研发成本增加。如果代码能够保持简单清晰,那么最直接的效果就是降低学习与维护代码的时间,这是很多开发团队都愿意看到的结果。

第三,快速迭代,拥抱变化。虽然现在越来越多的团队开始采用敏捷开发,但是在现实中,一直陷在“业务需求永远也做不完的死循环”里才是开发团队的现状,而这意味着开发人员只有更少的时间写代码,即便有想要写简单代码的心,迫于进度压力也只能尽快写能工作的代码。久而久之,团队代码变得越来越复杂,开发效率越来越低,本来初衷是想拥抱变化,却被迫变成无力承受变化。要想改变这种现状,有效的办法之一就是保持简单性,比如,更好的测试性、更好的扩展性、更好的理解性、更灵活的设计等。

现在我们明白了保持简单的重要性,但是 KISS 原则并没有告诉我们该以什么样的标准来判断简单代码,也没有提出用什么具体的方法来指导我们写出有简单性的代码。所以,接下来我们就分析下简单代码应该长什么样。

二、如何理解代码中的“简单”?

大家是不是常常会听到很多类似这样的关于 KISS 原则的讨论?

  • 产品需求文档(PRD)都出来了,你就选择一种简单的设计吧,因为项目要快速迭代上线。

  • 这个功能不是有现成的类库吗?选择一种简单的类库封装一下就行了吧,客户明天就想要这个功能。

  • 现在开源框架这么多,随便选一个简单的来实现,我不关心技术,只关心能不能解决问题。

  • 你看 XX 大厂的设计多简单、体验多好,你们为什么不也搞成那种简单的呢?

发现没,很多时候我们都有一种“简单”的错觉:简单分析+简单设计+简单编程 = 简单产品。但实际上,这种简单组合就能构造出最终的“简单”吗?

在软件开发中,“简单”其实是最终的一个状态。管理层或终端用户的确不需要关心背后复杂的代码逻辑,然而对于开发人员来说,就不得不去考虑背后各种各样的复杂性。换句话说,编程的本质就是控制复杂度

那到底该如何正确理解代码中的“简单”呢?

我们先来看“简单”不是什么。

首先,简单≠简单设计或简单编程。 简单一旦和动词放在一起,太容易被误解,尤其是在软件开发中。比如,现在我们在很多项目开发中,为了完成进度而做简单设计甚至不设计,认为只要后期有需要时再重构就行,编码时也就采用简单编程,并美其名曰迭代敏捷开发。但实际上,项目到后期几乎没有时间重构,并频繁出现问题(定位时间变长、逻辑嵌套过深、不断打补丁等),最后项目往往以失败告终。

所以说,保持简单并不是只能做简单设计或简单编程,而是做设计或编程时要努力以最终产出简单为目标,过程可能非常复杂也没关系。

其次,简单≠数量少。 我们通常把简单好坏和数量多少挂钩,比如,代码行数少、使用的类库组件少、架构设计少似乎就是简单。但实际上,数量少可能只是表现出来的简单,也会引入更加复杂的问题的。

比如,曾经的一个项目中,为了解决限流问题,引入了一个不太常用的开源限流组件,看上去只要一个组件就能轻松解决问题,但是实践中发现,这个框架存在一个很不稳定的 Bug,当流量过大时,容易造成线程池挂死而导致应用崩溃,当时团队花费了一周的时间才找出这个问题。所以,一定不能以数量少作为衡量简单的标准,因为很可能看上去简单的背后隐藏了未知的复杂。

最后,简单≠过度简洁。如果你见过被加密的代码,那你一定知道什么叫过度简洁的代码。这样的代码机器可读,但是对于人来说几乎不可读。在现实中,我们可能都曾写过这样的代码:没有任何注释说明,不使用任何设计模式,用最直接的数据结构和算法来实现,使用字母缩写来命名变量。然后,过一段时间后,连自己都看不懂这样的简洁代码,除非再从头到尾看一遍才能回想起含义。如果换成别人,那么可想而知其阅读难度有多大。所以说,简单代码可能会很简洁,但一定不是过度简洁。

下面我们再来看“简单”是什么。

第一,简单应该是坚持实践。 我们常说代码应该简单,但却忽略了更重要的是保持简单的动作。换句话说,道理我们都懂,但实际上写代码时并没有坚持做。保持简单之所以很困难,是因为大多数人都只盯着最后实现简单后的好处,却忘记了在做到简单之前需要付出的努力。

真正的简单代码通常背后都隐藏了大量不简单的工作,比如,仔细分析需求,选择合适的技术框架,设计更合适的数据结构和算法,实现时保持代码可读性,等等,每一件事都不简单,并且长期坚持才有可得到最后的简单。

第二,简单应该是尽量简单,但又不能太简单。换句话说,就是要管理合适的代码上下文环境,并且在边界范围内以“最少知识”的方式构建程序,满足要求即可,保持一定的克制。很多时候,我们之所以容易过度开发功能,是因为没有考虑上下文的边界,进而导致需求扩散而不断扩充知识。比如说,需求中只要求你开发一个编辑器,你却在开发过程中发现了你想要试验的新功能,最后你开发了一个 IDE。从用户角度来看,他只需要一个简单的编辑器,虽然你做的事情也满足了要求,但你把简单的事情搞复杂了。

第三,简单应该是让别人理解代码逻辑时更简单。 代码写出来后,80% 的时间都在被阅读,简单代码的好处在于能让别人一眼就知道代码表达的意图,要想做到这样,就对写代码的人提出了一个更高的要求:不仅需要使用清晰的算法和数据结构实现代码逻辑,还需要使用面向对象编程技巧提升代码复用性,甚至需要写更多的单元测试和注释来提升可维护性。总之,好的代码就是将简单带给别人,复杂留给自己。

三、如何写出“简单”的代码

实际上,写出“简单”的代码是一个主观的判断,但其中有一些技巧和方法。以下分享“四要”和“四不要”经验。

1、四不要

  • 1、不要长期进行打补丁式的编码。 因为这会给团队树立一个不好的榜样,当你在打补丁式的编码中快速获得收益时(修改快,见效快),就会不知不觉地给其他人一种心理暗示——这段代码只需要不断打补丁就能解决问题,那么维护代码的人一定会优先选用这个方法,而不是重构它。

  • 2、不要炫耀编程技巧。 如果你的团队对 Java 8 的 Lamda 表达式语法还不够熟悉,那么你不要一开始就写这样的代码,但可以通过不断分享 Lamda 表达式的优势和知识来帮助团队提升编程实力(间接提升你的技术影响力)。切记不要为了彰显自己厉害而使用一些技巧,尤其是在一些维护项目上使用高级语法,这会很容易导致维护代码的人需要花费大量的精力和时间去学习或研究你的代码。

  • 3、不要简单编程。 硬编码、一次性编码、复制粘贴编码、面向搜索编程都是简单编程,如果一直习惯性地简单编程,那么带来的可能就是更复杂、更高成本的重构和重写。这不仅不能提升代码扩展性,还会使得代码在后期无法被维护和重构。局部的简单导致整体的更加复杂,这是现在公认的一种得不偿失的做法。

  • 4、不要过早优化。 为了让代码变得简单,优化是必不可少的手段,但是过早的优化会造成很多核心代码逻辑被隐藏,而维护代码的人为了不破坏原有的设计(误认为越早的设计就会越好),只能不断修改现有的设计来适应这种不变,最后反而容易导致架构可能被破坏。

2、四要

  • 1、要定期做 Code Review。 如果说简单性的判断标准很难统一,但是一眼让人快速理解的代码始终都是好代码,当他人在阅读你的代码时会自然对你的代码做评价,无论好坏,这都是你思考代码是否足够简单的契机。

  • 2、要选择合适的编码规范。 编码规范是优秀编码实践的经验总结,能帮助你写出“简单”代码,并发展出一种简单性的编程风格。不过,现在很多大厂的编码规范过于通用,虽然很好,但是不一定适合你,这时你应该进行适度的裁剪优化,逐渐找出真正适合你的经验,发展出适合你的“简单”编码风格。

  • 3、要适时重构。 并不是说非得要等到代码完全无法修改时再重构,你完全可以轻松地在每一个小的迭代版本里进行重构,比如,分离一个过多职责的类,抽象一个上次没来得及做的通用服务,减少业务里 if-else 的嵌套层数,对不同业务数据对象分包管理等。及时这样做以后,代码整体就会变得简单。

  • 4、要有目标地逐渐优化。 优化一定要制定一个目标,不然很容易就成了盲目优化,甚至把优化当成了一种单纯的 KPI。如果程序性能都没有达到真实的性能瓶颈,就没有任何优化的必要。而且优化应该是分阶段和步骤的,不要搞一次性的大优化、大提升,这样会导致频繁的代码修改,反而容易引入更多的 Bug。

四、YAGNI 原则

YAGNI 原则(You Ain’t Gonna Need It),翻译过来就是:你不会需要它。换句话说,在软件开发中,它希望你不要写“将来可能需要,但现在却用不上”的代码

但实际上,我们作为开发者都会有一种冲动:“既然发现某段代码有重复,抽象成通用功能也许以后能用上呢?”以我多年的经验来看,这样“额外”做之后,多数情况下代码就再也没有被使用过了。更为严重的是,它不仅让代码变得更加复杂,而且还可能让系统出现 Bug 的概率更高。

YAGNI 原则和 KISS 原则联系紧密。YAGNI 原则能够帮助我们更好地实现 KISS 原则,因为保持简单更像是一个要达成的目标,而在实际开发中我们稍不留神就会写出一些多余的功能,使用 YAGNI 原则可以及时提醒我们“在确定真的有必要的时候再写代码,那时再重构仍然来得及”。

文章(专栏)将持续更新,欢迎关注公众号:服务端技术精选。欢迎点赞、关注、转发