写"简单"而不是"容易"的代码
简单 vs 容易
简单 == 容易?
在大多数人的第一印象中,这两个词好像是等同的,我们在很多场景下会相互替换这两个词而表达出相同的意思,比如下面这些:
“高数题很简单/容易” “开车很容易/简单”
“C语言写起来很简单/容易”
当然 “抗原测试阳性也很容易/简单”...
对于以上这些句子我想大家都是认同的......吧?好吧,或许前三个问题可能有不同的意见,但在第四个问题上我们应该还是能达成一致的(如果还是有疑问,也许去医院溜达一圈可以改变你的想法,即使全程佩戴口罩;))。
正如上面的句子所示,随意更换结尾的 简单 和 *容易 *两个词,好像依然可以得出一样的含义,这并不会带来理解上的偏差,所以我们可以就此推出结论 简单 == 容易 吗?或许我们可以从另一个角度再来看下这个问题
从其他语言体系中收获一些启发:
Simple:
在英文单词中 Simple** **的词根是sim 和 plex, 可以理解是一层,一圈,至于plex 中所代表的折叠和扭曲的含义,当只有一层或者一圈时,实际上也就是没有折叠,不扭曲了,这个词的反义词是complex,意思是编织在一起或者折叠在一起
因此对应到软件开发过程中,当我们追求简单的事物时,其中最关键的一点就是,我们希望它是专注于某一方面的,我们不想看到它和其他事物交织在一起,当然,这并不意味着我们需要太过追求单一一个,相反 简单* *的关键无关乎数量,更多的是追求是更少的交织,或者是没有交织,这一点才是重中之重,正如我们上面描述的一样,事物是否是交织重叠的,只要进去看下就知道了,它是客观的,是可以深入去研究的,而这种客观也是后面我们区分于 **容易 **的核心所在
Easy:
来源于拉丁语动词adjacere(附近放置),其现在分词为adjacens(附近的,手边的,方便的,英语adjacent的词源,adjective的间接词源),进入古法语后有名词aise(英语ease的词源),派生了动词aisier(轻松、随意放置),其过去分词aisie进入盎格鲁-诺曼底语中为aise,进入英语为easy
而 **Easy *就有趣了,其最初是来源于拉丁词 ***adjacens(附近的,手边的,方便的)一词。***靠近是一个很有意思的概念,*我们可以从下面几个方面来理解下它
- 物理意义上的靠近
这个东西就在你附近,触手可得,不需要骑车,或是开车去
- 靠近我们已知的东西,或者换个词:熟悉
对于我们来说俄语很难吗?当然是的,不过对于俄国人来说他们可能并不会这么觉得,无非是他们相对来说更熟悉罢了
- 靠近我们的能力
手里有一把锤子,看什么都像钉子,当我们说这个东西很容易的时候,很大程度上是因为我们已经想到了一些功能相似的东西
所以,simple** == easy** 吗?不,easy 是相对的, 弹钢琴和俄语对我来说真的很难,但是对其他人来说却很容易。不像 *simple,它是客观的,*我们总是可以进去看看,寻找是否存在重叠和交叉,而 easy 总是要问一句,对谁来说容易,对谁来说很难?
为什么需要简单,而不是容易
许多时候我们说的简单,都是从自身出发的,其实更应该用词为 容易;而真正的 **简单 **,是不和别的东西耦合的,独立的东西。多数时候我们在产品开发过程中的冲突在于,产品经理会说自己的设计是简单的(Easy),但是开发同学认为复杂(不符合自己开发的 Easy),但是却忽略了,我们真正需要的是 Simple
这样说你会发现,我们做的许多事情,往往都是从 easy 开始,而不是 simple 。 easy 开始的速度很快,但是随着项目的扩展,复杂度越来越高,速度慢慢就掉下来了 —— 想想每次重构代码的痛苦吧。而 simple 则刚开始并没有太快的速度,因为需要定义许多的东西,抽象归纳许多对象,但后续推进则是越来越快 —— 因为结构清晰构件完备,只需要理解有限的上下文就可以完成模块的修改或扩展。
因为限制
任何事情都是有限制的:
我们只能让我们理解的东西变可靠
我们只能在同时思考很少的一些事情
互相纠缠的事情我们只能把它们放在一起来思考
复杂性会降低我们的理解
我们怎么可能制造出我们不了解的可靠产品,当我们在某些系统上,想在未来使事情变得更加灵活、可扩展和动态时,我们将在理解它们的行为并确保它们正确的能力上做出权衡。但是对于我们想要确保正确的事情,我们都将受到限制,受限于对它的理解。
而且我们的理解力是很有限的,举个例子,你一次能在空中保持多少个球,或者你一次能记住多少件事?数量有限,而且数量很少,对吧?所以我们只能考虑一些事情,当事情交织在一起时,我们就失去了独立对待它们的能力。
因此,每次我们需要理解软件的一个新部分,并且它与另一件事相关联时,我就不得不将另一件事拉入脑海,因为我们无法在没有另一件事的情况下考虑这件事。这就是他们交织在一起的本质。因此,每一次交织都会增加这种负担,而这种负担是我们可以考虑的事物数量的组合。因此,从根本上说,这种复杂性,这种将事物编织在一起,将极大的限制我们理解系统的能力
简单带来的收益
《针织城堡》 《积木城堡》
- 容易理解
理解代码想表达什么,而不是写的是什么
- 容易改变
你能想象在一个针织的城堡上做改动吗?
容易debug
灵活性
容易代码的产生
随着时间的拉长和各种各样因素的干扰,代码慢慢就脱离了我们的掌控,当回过神来,看着日积月累的代码库,我们会发现它已大到经难以撼动了,只能寄希望于它不会哪一天突然炸开,或赶在它炸开前把它扔出去。
坏味道
读侦探小说时,透过一些神秘的文字猜测故事情节是一种很棒的体验;但如果是在阅读代码,这样的体验就不怎么好了。我们也许会幻想自己是《名侦探柯南》中的柯南,但我们写下的代码应该直观明了,代码中最重要的一环就是好的名字。
然而,很遗憾,命名是编程中最难的事情之一,所以我们需要深思熟虑如何给函数、模块、变量和类命名,使它们能清晰地表明自己的功能和用法,很多情况下我们不愿意给程序元素改名,觉得不值得费这个劲,但好的名字能节省未来用在猜谜上的大把时间。
重复代码
如果你在一个以上的地点看到相同的代码结构,那么可以肯定,设法将它们合而为一,程序会变得更好。一旦有重复代码存在,阅读这些重复的代码时你就必须加倍仔细,留意其间细微的差异。如果要修改重复代码,你必须找出所有的副本来修改。
过长函数
函数越长,就越难理解,从已有的经验中可以看出,活得最长、最好的程序,其中的函数往往都比较短。初次接触到这种代码库的程序员常常会觉得“计算都没有发生”——程序里满是无穷无尽的委托调用。但和这样的程序共处几年之后,你就会明白这些小函数的价值所在。间接性带来的好处,更好的阐释力、更易于复用、更多的选择,都是由小函数来支持的。
过长参数列表
把函数所需的所有东西都以参数的形式传递进去。这可以理解,因为除此之外就只能选择全局数据,而全局数据很快就会变成邪恶的东西。但过长的参数列表本身也经常令人迷惑,使用它的人必须小心再小心。尤其在参数末尾出现了布尔类型的参数时,更容易引起人的误解,传true或false会有什么不同吗?
发散式变化
我们希望软件能够更容易被修改——毕竟软件本来就该是“软”的。一旦需要修改,我们希望能够跳到系统的某一点,只在该处做修改。如果不能做到这一点,这可不是一个好的信号。
重复的switch
在不同的地方反复使用同样的switch 逻辑(可能是以switch/case语句的形式,也可能是以连续的if/else语句的形 式)。重复的switch的问题在于:每当你想增加一个选择分支时,必须找到所有 的switch,并逐一更新。
“所有条件逻辑都应该用多态取代,绝大多数if语句都应该被扫进历史的垃圾桶”,这不免有些矫枉过正了
过大的类
一个类过大往往是因为想利用单个类做太多事情,其内往往就会出现太多字段。一旦如此,重复代码也就接踵而至了
注释
当然,并不是说你不该写注释,之所以要在这里提到注释是因为注释往往被当做除臭剂来使用了,大多数情况下注释的大量出现是是因为代码本身已经很糟糕了
有意义的注释应该更多关注无法通过代码本身表达的内容,除了用来记述将来的打算之外,注释还可以用来标记你并无十足把握的区域。你可以在注释里写下自己“为什么做某某事”,这类信息可以帮助将来的修改者理解代码所没有表达出来的细节
保持简单的代码
什么是重构
如何保证代码不随着时间和迭代而慢慢腐坏呢? 恐怕没有比重构更有效的方法了吧,但是重构一词近些年被用的太广泛了,很多人用“重构”这个词来指代任何形式的代码清理,但实际上用“结构调整”来泛指对代码库进行的各种形式的重新组织或清理更准确一点,而重构则是特定的一类“结构调整”,整体而言,经过重构之后的代码所做的事应该与重构之前大致一样:
重构(名词):对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本
重构(动词):使用一系列重构手法,在不改变软件可观察行为的前提下,调整其结构
也就是说,如果你在重构过程中发现了一个bug,那么重构之后它应该依然存在(当然,你可以在重构后顺手就修改掉它),同样,我们也需要把它跟性能优化也区分下开,他们两个也很像,都是在不改变程序的可观测行为下做的改动,两者的差别在于,重构的目的是为了让代码更容易理解和更容易修改,最终可能使程序更快了,也可能更慢了,而性能优化时,我们只关注让程序运行的更快,最终可能使得代码变得更难以理解和维护了,当然这点也是在我们预期之内的。
为何要重构
重构并不是万能药,它只是一种工具,帮助我们达到以下目的方式中的一种
▪ 保持软件的设计
如果没有重构,程序的内部设计会逐渐腐败,当我们经常只为了短期目的而修改代码时,往往会忽略掉或者说没有理解整体的设计,于是代码会逐渐失去其结构,我们会越来越难以通过阅读代码来理解原来的设计,而随着代码结构的流失,我们也将越来越难以维护其设计意图,导致更快的代码腐败,所以,经常性的重构有助于我们维护代码应有的形态
▪ 使软件更容易理解
机器并不关心程序的样子,它只是按照我们的指示精确执行罢了,但别忘记了,除了我们自己和机器外,代码还有其他的读者,几个月后可能会有另一个程序员尝试读懂你的代码并做一些修改,他才是重要的那一个,相比于机器是否多消耗一个时钟周期,如果一个程序员需要花费一周时间才能读懂你的代码并修改才更要命呢。
重构可以帮我们让代码更易读。开始进行重构前,代码可以正常运行,但结构不够理想。在重构上花一点点时间,就可以让代码更好地表达自己的意图——更清晰地说出我想要做的。
▪ 提高编程速度
归结到最后其实可以总结为一点:重构帮我们更快速地开发程序
我们很容易得出这些好处:改善设计、提升可读性、减少bug,这些都是在提高质量。但花在重构上的时 间,难道不是在降低开发速度吗?
我们在软件开发中经常能碰到这种场景,一个团队,一开始的迭代和进展都很快,但是如今想要添加一个新的功能需要的时间就长的多了,bug排查和修复也越来越慢,代码库一个补丁摞一个补丁,需要细致的“考古”工作才能搞懂系统是如何工作的,这些负担会不断的拖累我们的开发速度,直到最后恨不得重写系统。
有些团队则不同,他们添加新功能的速度越来越快,因为他们可以利用已有的功能快速构建新功能,两者
的主要区别就在于软件的内部质量,良好的内部质量可以让我们轻易找到在哪里以及如何修改,良好的模块划分可以使我们只需要理解代码的一小块就可以做出修改,引入bug的可能性也会变小,即使有了bug,我们也可以很快的找出来并修复掉,最终我们的代码会演变成一个平台,在其上,我们可以很容易的构造其领域相关的新功能
何时重构
▪ 添加新功能
重构的最佳时机就在添加新功能之前,当要添加一个功能时,我们一般都会先看下代码库中已有的内容,此时经常可以发现,有些函数或者代码只要稍微调整下结构,就能使我们添加新功能变得更容易,可能只是一个函数的参数不太一样或是代码中一些字面量不太一样,如果不先重构,就只能把这段代码复制过来,修改几个值,这样就产生了重复的代码,更麻烦的是,一旦后续需要调整这块逻辑,我就需要同时修改这两处(希望我还能想起来有两处需要修改)
就好像要去东边的上海,你不一定会直接向东开,而是先向北开去上高速,后者会使你更快到达目的地。
▪ 使代码更易懂
在做改动前你需要先先理解代码在做什么,然后才能修改,这段代码可能是自己写的,也可能是别人写的,一旦你需要花费很大精力思考代码在做什么时,这就是一个好的时机了,“这个变量名称代表这个意思”,“这段逻辑是这样工作的”......我们通常无法把太多的细节长时间留存在脑海里,为什么不把他转移到代码本身,使其一目了然呢?如果把对代码的理解植入代码中,这份知识会保存得更久,其他人也能获得同等的收益。
不仅如此,长远来看的话,当代码变得更易理解时,我们通常能够看到之前设计中没有看见的问题,如果没有做前面的重构,也许我们永远也看不到这些设计问题,因为我们不够聪明,无法在脑海中推演所有的变化。
▪ 有计划的重构
上面的重构时机都是夹杂在我们日常的功能开发中的,重构本身就是我们开发新功能或者修复bug的一环,但有时候,因为快速的功能迭代而忽视了代码的设计,问题就会在某些区域逐渐累积长大,最终需要专门花些时间来解决,但这种有计划的重构应该很少,大部分重构应该是不起眼的、见机行事的。
▪ 何时不应该重构
虽然我们提倡重构,但确实有一些不值得重构的情况。
如果看见一块凌乱的代码,但并不需要修改它,那么就不需要重构它。 如果丑陋的代码能被隐藏在一个函数或者API之下,我们可以暂时容忍它继续保持丑陋。只有当我们需要理解其工作原理时,对其进行重构才有价值。
另一种情况是,如果重写比重构还容易,就别重构了,当然在这之前我们总是需要花些时间尝试的。
重构的挑战
▪ 延缓新功能开发
从上面的讨论中其实我们已经得到这个问题的答案了,重构的意义不在于炫技或是把代码库打磨的闪闪发光,而是纯粹从经济角度出发的考量。我们之所以重构,因为它能让我们更快,添加功能更快,修复bug更快。如果有人告诉我们“重构会拖慢进度”,我们应该坚信,他说的一定是别的东西,而不是重构,重构的唯一目的就是让我们开发更快,用更少的工作量创造更大的价值
▪ 代码归属
很多重构可能不仅涉及一个模块的改动,同时也会影响其他一些模块或者系统,代码的所有权边界会妨碍我们的一些重构,例如修改某个模块暴露出去的函数命名,不仅实现方需要修改,调用方也需要修改,尤其是涉及到暴露给其他团队的api,情况可能会更复杂,很有可能根本不知道调用法有哪些
当然这并不会完全阻止我们,只是受到很多限制罢了,比如我们可以同时保留老的函数签名和api,使其内部调用重构后的实现,等到确认所有调用方都修改后,再删除老的声明,当然也可能选择永久保留老的声明。
▪ 分支合并
大多数情况下,我们都是多个人同时维护一个代码仓库的,现代便利的仓库分支管理工具很好的支持了我们的团队协作,使我们可以更快的完成产品的开发,我们通常会从主干上拉取一个功能分支进行开发,直到功能上线时才会合并会主干,以保证主干不被功能分支所影响,这存在一个问题,我们的功能分支存在的时间越久,和主干的差异就会越大,尤其是当多个人同时在进行不同的功能分支开发时情况会更加复杂,当你因为重构修改了一个函数的命名,而其他人在新加代码中又使用了它,当代码集成时,问题就来了,而且随着功能分支存在时间的增加,这种痛苦也会不断的增加。
▪ 测试
重构的一个重要特征就是--不会改变程序可观察的行为,我们不能寄希望于“只要我足够小心,就不会破坏任何东西”,但万一我们犯了个错误怎么办?或许应该把万一两个字去掉,人总是会犯错误的,关键是在于如何快速的发现错误,要做到这一点,我们就需要一套能够快速测试的代码组件,所以大多数情况下如果我们想要重构,我们就需要先有可以自测试的代码,一旦能够自测试,我们就可以使用很小的步子进行前进,一旦测试失败,我们只需要执行退回到上一次可以成功运行的状态即可。
▪ 遗留代码
大多数情况下,有一大笔遗产是件好事,但从程序员的角度来看就不同了。遗留代码往往很复杂,测试又不足,而且最关键的是,是别人写的。重构能很好的帮助我们理解系统,理顺代码的逻辑,但是关键点在于遗留系统多半没测试。如果你面对一个庞大而又缺乏测试的遗留系统,很难安全地重构清理它。
对于这个问题,显而易见的答案就是“没测试就加测试”,说起来轻松,做起来可就不轻松了,一个系统只有在一开始设计时就考虑到了测试,添加测试才会容易,可要是如此的话系统早就该有测试了,还需要现在才开始加吗?
但是,无论如何,就像《整洁代码之道》中所说的那样:“让营地比你来时更干净些”。
如何安全的重构
▪ TDD
▪ 自动化重构
链接:https://juejin.cn/post/7182893492002095141
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。