代码必然被修改
Code will be changed
代码不是写完就结束了,它在日后必然会被修改。没有写完就扔的一次性代码。
在编写代码的时候,我们应将“代码会被修改”这一点作为进行判断和选择时的优先考虑事项。
为什么?
软件在本质上具有复杂性,这就决定了它不可能是完美无缺的。软件在发布后必然会发生故障,这时我们就需要对故障进行修复。
另外,用户可能在软件发布后产生新的需求,因为有些问题只有等到用户实际使用软件之后才能被发现。任何软件都不可能在首次发布时就满足用户所有的需求。
除用户自身之外,用户所在商务环境的变化也会导致需求发生变化。软件必须迎合这种变化。如果执着于最初编写的程序,做出没有人用的软件,那么一切都是徒劳。
怎么做?
编程中的任何一个判断都要以代码会被修改为前提。也就是说,编写的代码要经得起修改。
因此,提高代码的可读性就显得尤为重要了。代码这种东西,读远比写要费时间。如果以代码会被修改为前提,那么不管写代码需要耗费多少时间,只要读代码的时间能够缩短,我们就能把消耗在写代码上的时间赚回来。
特别是接手其他人写的代码时尤为明显,有一次我接手了一个反编译后得到的.NET工程,由于是反编译后得到的代码,所以是没有任何注释的,当时花了2,3天的时间才弄懂涉及到新需求的逻辑,之后为了完成需求而做出的修改所花费的时间比阅读代码的时间少的多。要是代码里有注释应该能节省不少时间。
KISS原则
Keep It Short And Simple(让代码保持整洁)
编写代码时,要优先保证代码的简洁性。
不管是从零开始编写代码,还是修复故障或扩展功能,都要注意保持代码简洁。
为什么?
随意修改代码会使代码变得越来越复杂,越来越没有秩序。
复杂的代码可读性较差且难以修改。强行修改不仅会降低代码的质量,还会浪费时间。这样一来,我们就无法保证能在合适的时间发布修正版或者对软件进行更新。如果我们没有重视这个问题,依旧强行修改代码,代码就会变得没有人能看懂,最终腐化为无用之物。
而一份简洁的代码,其各个组成要素也是简洁的,各要素承担的职责也都降到了最小,各要素之间的关系也比较简单。因此,简洁的代码可读性高,容易理解,便于修改。各要素职责明确,使得测试也变得简单易行。程序员之间能更加轻松地通过代码进行交流,减少了在现实世界中多余的对话,节约了交流成本。这样,我们就能保证在不降低开发速度的情况下对软件进行长期维护。
代码必然会被修改,因此易于修改的特性对代码来说不可或缺。保持代码简洁可以使代码拥有易于修改的特性。
怎么做?
下面几种情况会让代码变得复杂,应该尽量避免:
1. 试图使用新学会的技术
学会一门新技术后,人们倾向于使用新技术写出一些无谓的代码。
但是,代码并不是用来炫耀聪明才智的,它的作用是给用户提供价值。我们不能在代码上耍聪明。
我们要多多斟酌代码的写法,努力保持代码简洁。
2. 以备将来之需
有时人们觉得将来会用到某些功能,认为最好趁现在写下来,于是编写了过剩的代码。
现在用不到的东西就不应该现在写,因为在大多数情况下,这些东西将来也用不到。
我们应该只写当前需要的代码,保持代码简洁。
3. 擅自增加需求
程序员有时会擅自增加需求,添加多余的代码。他们觉得,某个需求必要与否、正确与否,与其找用户确认,不如自己直接写出来。但是,需求是由用户决定的,程序员不可以擅自增加。
一旦添加了不必要的代码,花费在维护上的时间就会像滚雪球一样增加。不写多余的代码是保证代码简洁的秘诀。
DRY
Don't Repeat Yourself(不要重复)
将整个逻辑随便复制粘贴到其他地方去用是造成代码重复的主要原因。这样一来,同一个逻辑将出现在多个地方。
直接将常量写入代码也会造成代码重复。如果意义相同的常量在多处使用,常量表达的信息就会重复出现多次。
为什么?
代码一旦出现重复,故障修复、添加功能等,代码的改善措施就会变得难以实施。具体来说,我们会遇到以下困难:
1. 代码的可读性下降
相同的代码出现多次,从量的角度来看是“代码量变大”,从质的角度来看是“复杂度变高”。显然,代码的可读性会下降。
无法准确理解代码就无法确立修改方针。
2. 代码难以修改
当相同的代码出现在多处时,只有正确修改每一处代码,才能确保整体的一致性。稍有不慎,修改就会出现遗漏。
另外,即使代码完全相同,有时某些地方也用不着修改。在这种情况下,我们就需要阅读前后代码,判断这一处是否需要修改。
若当前重复的代码之间存在细微差别,我们就需要更加深入地阅读各个位置的代码。控制语句的条件内容或条件数量只要存在一点点差别,理解的难度就会进一步增大。弄不好代码会因无法解读而得不到改善。
3. 没有测试
出现重复的代码大多是遗留代码,也就是说,这部分代码没有经过任何测试。
在没有测试的状态下,就算我们拼尽全力去修改遗留代码,发生新故障的概率还是很大。
就算克服了上述所有困难,费尽九牛二虎之力完成修改,这些代码也会因为动了多个“大手术”而变得更加混乱。长此以往,当混乱蔓延至所有代码时,修改就会变成一个不可能完成的任务。
怎么做?
我们可以通过对代码执行抽象化操作来消除重复。
对代码的逻辑执行抽象化操作,其实就是给整个处理命名,将其函数化、模块化。至于数据,则需要起个名字定义为常量。最后将重复的部分全部置换为抽象后的内容。
抽象化有以下几个优点:
- 减少了代码量,减轻了阅读负担
- 因为逻辑和数据有了名称,所以代码的可读性变高了
- 重复的代码集中到了一处,我们只对这一处进行修改即可。于是,代码的修改操作变得简单,代码的质量也得到了保证
- 抽象化的部分易于重复使用。在添加新功能的时候,重复使用代码可以更快、更好地完成编程
不过,执行抽象化操作需要我们跨越心理方面的障碍。比如将逻辑转化为函数的操作就相当费时间,我们需要有足够的耐心。另外,由于我们修改的是原本可以运行的代码,所以修改后的代码存在不能正常运行的风险。抽象化操作还有一个最明显的缺点,那就是太麻烦。
然而,避免重复这一点没有商量的余地。从长远看来,避免重复的利大于弊,这是历史总结出来的结论。所以,即便要花时间重构,即便要花时间消除代码不能正常运行的风险,即便操作起来有些麻烦,我们也要消除重复的代码。
设计模式就是具有代表性的一种设计手法,它提供了代码结构模式以达到重复使用代码的目的。从另一个方面来看,设计模式也可以说是一种防止重复思考(重复思考同一问题的解决方案)的手法。
性能调优的箴言
Proverb of performance tuning
是什么?
所谓性能调优,就是编写运行速度快的代码。性能调优也称为代码优化。
很多人认为加快代码的运行速度是一件好事。但实际上,过早优化代码会产生各种问题。
因此,对于代码优化,我们要遵守以下规则。
① 不要在编程之初就对代码进行优化
② 编程之初暂时不要对代码进行优化(适用于专家)
代码优化并不是我们在编程之初就应该考虑的事情。在编程时,我们要注意的是代码的正确性和可读性,编写高质量的代码,而不是想方设法让代码的运行速度变快。
为什么?
优化代码需要我们付出无法接受的代价。即便完成优化,代码也会失去一些重要的东西,比如以下几点。
1. 可读性变低
优化后的代码肯定比优化前的代码更难懂。
因为从性质上来说,优化所做的工作是修改代码中原本简单直接的逻辑。优化代码后,逻辑不再简单明快,变得难以表达意图。也就是说,要提高性能,必须以失去逻辑清楚的设计和降低代码的可读性为代价。
最大限度优化过的代码非常难看,我们很难掌握它的处理过程。
2. 质量变差
代码复杂化会导致代码的可读性下降,从而降低代码的质量。在没有明确描述算法过程的代码中,故障很容易被漏掉。
不论回答的速度有多快,答不出正确答案也枉然。说得讽刺一点,优化在给代码加入难以发现的新缺陷方面算是一种切实有效的方法。
3. 复杂度增大
优化会利用特殊的后门强化模块间的依赖性,提升代码的结合度,让代码能够利用一些平台固有的功能。
用如此取巧的方式编写代码会增加代码的复杂度,同时让代码失去可移植性。
慢慢地,代码将越来越不符合优质代码的条件。
4. 阻碍维护
代码复杂化导致代码的可读性下降,从而提升了维护代码的难度。
首先,问题难以被发现,因为代码优化之后,不自然的描述会增加。这样一来,我们就很难追踪处理的流程了。也就是说,优化后的代码是高风险的危险代码。
再者,优化还会对代码的可扩展性产生不好的影响。优化是在给代码设置更多前提条件的基础上实现的。因此,优化会限制代码的通用性和可扩展性。
5. 与环境相冲突
在大多数情况下,优化只能在特定的环境中发挥作用。在某个特定环境下对代码进行优化后,代码在其他环境中运行的效率可能会变低。
比如我们针对某个特定种类的处理器选用了最合适的数据类型。这种做法就可能会导致软件在其他处理器上执行的速度变慢。
6. 工作量增多
对代码进行优化就等于多加了一项工作。
程序员要做的工作非常多。代码如果能成功运行起来,我们就应该先去处理其他紧急的工作,而不是去对代码进行优化。
优化是一项非常耗时的工作。找到问题出现的原因并对代码进行优化并不是一件容易的事情。一旦弄错优化对象,就会浪费大量宝贵的劳力。
怎么做?
我们要先写高质量的代码,然后根据需要进行优化。
高质量的代码是在信息隐藏的原则下写出来的。因为各个决定只会在局部范围产生影响,所以代码的修改不会影响到其他部分。先写高质量的代码再调节性能效率更佳。
况且在大多数情况下,“高质量”与“高性能”并不矛盾。按照上述优先顺序写出来的代码只要满足高质量代码的要求,优化时就不会产生多少新的工作。而且代码质量高,我们在做添加工作时也会轻松一些。
另外,写完高质量的代码之后,如果要进行优化,一定要思考其必要性。优化在很多时候不值得我们花费那么多的时间和成本。是否进行优化,要在与解决故障、添加新功能和发布产品等重要工作相比较之后再决定。
影响软件的性能的几个因素:
从整体来看,除了代码,软件的性能还受到很多因素的影响。比如以下几个因素。
- 执行环境
- 部署的设置或者安装的设置
- 使用的中间件
- 使用的库
- 相互运用的旧系统
- 架构
这样一看,一行一行的代码对软件整体的影响十分渺小。除了各行代码之外,还有很多影响性能的因素。
性能调优的流程:
在实际工作中,很多时候我们会因为软件的特性而需要对代码进行优化。
在对代码进行优化(性能调优)时,我们需要在流程方面遵守几项规则。
1. 证明优化的必要性
首先要再三确认优化的必要性。有时候用户对某部分性能的需求并没有程序员想的那么高。
2. 测量性能,找出瓶颈
确认需要优化后,我们要先找出瓶颈所在。
性能出现问题并不代表所有代码的运行速度都很慢,大多是某个特定部分占用了较长时间。这个占去大部分处理时间的部分称为“热点”。
我们要全身心地寻找这个热点。
3. 优化瓶颈部分的代码
发现热点之后要对其进行修改。
4. 测量性能,确认优化效果
不管是代码优化前还是代码优化后,我们都必须对性能进行测量。
性能差的部分是无法推测出来的。优化的效果也只能通过测量得知。
5. 验证优化后的代码是否存在运行问题
优化可能会使代码出现一些新的问题。对代码进行优化后,必须认真检查代码是否存在运行问题。
这里再说一下寻找热点的方法。寻找热点时,应利用分析工具,尽可能仔细且准确地检查代码。
另外,由于优化过程中要多次对代码的性能进行测量,所以为了提高效率,我们最好对这部分工作执行自动化处理。
一步一步走
One by one
是什么?
编程时要一次只做一件小事。
一件一件做,一点一点来,就像上台阶一样一步一步走。不要一次性处理多项工作。
完成一个小任务后认真检查,没有问题后再开始下一个任务,如此循环。
为什么?
一次处理一项工作的工作方式更有效率,最终产品的质量也更好。
一步一步进行编程,最后一步操作撤销起来也会比较容易。
一步一步进行编程,工作检查起来也比较简单。
一步一步进行编程,新旧代码的替换也会更安全。
一步一步编程意味着程序员能够掌握和控制代码的状态。这样做能去除不确定因素,让人安心工作。
在有心理压力时,人很难像平时一样做出准确的判断。控制好自己的状态也是写出优质代码的必要条件之一。
怎么做?
不一次性处理多项工作
逻辑思考的秘诀
关于逻辑思考,有几个关键点需要我们了解。
- 想立刻获得答案的态度是不正确的。一眼看不出答案时应当继续思考
- 没有经过深思熟虑就下结论的做法是错误的。发现某个东西可以满足条件时不能想当然,要探讨其他的可能性
- 避免反复思考同一件事
- 直接用脑子思考有些困难,不如边写边思考。边写边思考能产生额外的效果。对于想不明白的地方,有时候写下来一看就明白了
- 直觉对逻辑思考来说也很重要。比如,当我们感觉“创建矩阵有助于整理信息”时,不妨先试一试。不过,直觉只能用在思考的过程中。仅凭直觉来获取答案的行为只能说是瞎猜,这可不是一个好习惯
布鲁克斯法则
是什么?
增员等于“火上浇油”
对于开发进度滞后的软件开发项目,如果为了赶进度而在开发后半程添加人手,反而会使延迟情况进一步加重。
在项目尾声,当我们发现产品无法如期交付时,常会投入更多的人手。但这种做法只会火上浇油。
为什么?
人数和月数是无法交换的
项目的工时是用人数和月数换算的,也就是几个人用几个月完成某个项目,所以用“人数×月数”来计算项目工时。
这里要注意的是,该乘法运算与数值的乘法运算不同,人数和月数不能调换。也就是说,“人数×月数= 月数×人数”的式子是不成立的。
比如一个12人月的项目,客户要求6个月内开发完成,那么我们只要投入2人即可。如果人数和月数可以调换,那么当客户说这个项目比较急,需要在2个月之内完成时,我们只要投入6个人就行了。
然而在现实中,“6×2”和“2×6”并不相同。2人工作的效率与6人工作的效率不可同日而语。
理由如下。
1. 因存在依赖关系而产生额外的负担
如果每个人的工作相互独立,那么在人数是原来3倍的情况下,生产效率也会变为原来的3倍。
然而一般来讲,工作分割之后,各项工作之间会产生依赖关系。
如此一来就会产生一些新的负担,如任务的分割、各项确认工作的出现以及通信路径的增加等。
即便追加人手,这些额外的负担也会拖慢项目的进度。
2. 培训新人会占用一定时间
在追加人手时,为了能让这些人发挥作用,必须让他们学习当前项目固有的各种知识、信息以及技术。也就是说,要花时间对新人进行培训。此外,负责培训的人是同一个项目内的成员,这就导致新团队的整体生产效率下滑。
在新人真正发挥作用之前,整个项目的进度都是滞后的。
怎么做?
重新制订时间表
无条件地投入更多人手来赶上进度是一种不明智的做法。
强行给当前成员增加负担也只会对项目造成损害。
进度滞后最好的解决方法是重新制订时间表。在此过程中,要与用户做好协调,同时决定各个功能的优先程度,进行阶段式发布。
拓展:
人与人也不可交换
前面说过,人数与月数不可交换。从某种意义上讲,人与人也是不可交换的。
一个程序员离开了,并不是再补一个程序员就行。之所以这么说,是因为程序员的水平参差不齐。
在物理空间内的生产效率方面,有能力的人与没能力的人之间的差距最多也就几倍。但像程序员这种以信息空间为主战场的人,由于不受物理方面的制约,各个程序员之间的生产效率有很大的差别。据说能差30倍。
不过,“同样的时间内能写出多少代码”这种生产效率上的差距并不是造成上述现象最根本的因素。某些方面的差距更根本且更巨大。
比如以下几个方面。
- 有能力 / 没能力
有些人写出的代码能用,有些人写出的代码不能用。这是一个有与无的比较,计算差距已经没有意义了。
- bug多 / bug少
有些人写出的代码没有bug,有些人写出的代码到处都是bug。
二者的维护成本会出现巨大的差别。
- 执行速度快 / 执行速度慢
有些人写出的代码执行速度快,有些人写出的代码执行速度慢。代码的执行速度慢意味着会浪费用户的时间。软件的目的是实现业务的高效化,为用户节省更多的时间。代码执行速度慢的话就违背了这一目的。
况且,代码执行速度慢还会引来用户的投诉。这时,我们不仅要花时间应对用户投诉,还会失去用户的信任。
- 代码可读性高 / 代码可读性低
有些人写出的代码可读性高,有些人写出的代码可读性低。
另外,有些人写出的代码便于修改,有些人写出的代码一经修改就会出问题。
二者由此产生的优化成本大不相同。代码质量差到一定程度时甚至无法优化。
综合上面几点来看,有能力的程序员和没能力的程序员确实差出好几个档次。
有能力的程序员在项目中起到的作用非常大。对于这些有能力的程序员,我们不可以将他们视为可交换的“1人月”,要把他们留在项目中承担固定的职责。
防御性编程
是什么?
防患于未然的程序设计
我们在编程的时候不要想当然。
防御性编程与开车时的防御性驾驶是同一种思路。
在采取防御性驾驶这一驾驶方式的情况下,我们总抱有一种不知道其他驾驶员会做出什么事情的心态。也就是说,自己不认为驾驶的过程是百分之百安全的,觉得中途可能会发生什么事。这样一来,当其他驾驶员做出一些危险的行为时,自己就能做好充分的准备不受伤害。即便过失在其他驾驶员身上,自己的命也要由自己来保护。防御性驾驶体现的就是这样一种心理。
与此类似,当函数接收到非法数据时,即便问题出在其他函数身上,我们也应准备好“防御性”的代码以避免函数受到损害。为此,编程时要注意以下几点内容。
1. 确认外部代码传来的数据输入值(检测“预想之内的错误”)
在从文件、用户接口、网络以及其他外部接口获取数据时,要确认数据是否在合法范围内。比如检查数值是否在有效范围内、字符串的长度是否符合规定等。
尽量在较早的阶段检测出无效输入。检测出无效输入后要迅速对其进行适当的错误处理。
2. 确认参数的值(检测“预想之外的错误”)
确认其他函数传来的参数的值。与检测外部代码传来的数据不同,这里如果检测出无效输入,就意味着代码存在bug。
我们可以使用断言确认参数,在发现非法值时立刻停止程序。
为什么?
开发与运维中的“安全驾驶”
开发中的“安全驾驶”
提早发现非法数据能提升调试的效率,因为提早检测出非法数据,并以明确的形式进行通知,可以帮助我们立刻找到出现问题的地方。这样一来,代码的调查与修改都变得非常容易。
反过来,如果没能提早检测出非法数据,那么故障就会蔓延到其他地方,这时我们就需要花费更多的时间来寻找根本原因。
运维中的“安全驾驶”
尽早处理非法数据能防止运维中出现的问题进一步扩大。在较早的阶段处理掉问题,能防止问题的蔓延。
错误如果处理得不彻底就会蔓延到其他处理中,问题会变得越来越大。特别是当错误的数据进入软件深处时,软件的运行可能会发生错误,或者错误的数据会进入数据库中,这将造成无法挽回的后果。
其中最棘手的当属安全问题。黑客在入侵系统时,喜欢利用没有彻底处理错误的地方。可见,不完备的错误处理也可能会成为安全漏洞。
怎么做?
路障战术
我们需要采用“路障战术”。建立路障,将损害控制在一定的区域内。
船体由多个相互隔离的区域组成,这与路障战术是同一种战略思想。即便船撞上冰山,船体破损,只要隔离破损的区域,整个船体就不会有沉没的危险。
另外,建筑物中的防火墙与路障战术也有异曲同工之妙。防火墙的作用在于防止火势蔓延。
为了在代码中建立路障,我们需要将特定的接口用作安全地带与非安全地带的分界线。检验通过这条分界线的数据,一旦发现非法数据,立即采取适当的措施。
这就好比手术室,所有东西都必须经过消毒才能拿进去。因此,通过大门进入手术室的东西都是安全的。
以门(= 路障)为界,分界线的左侧是“脏房间”,右侧是“干净的房间”。
在代码设计中,我们要明确“哪些东西可以进入手术室”“哪些东西不能进入手术室”,以及“门的位置”,也就是对安全地带里面的模块、安全地带外面的模块和在中间负责消毒的模块进行分工。
拓展1:
错误处理的变种
对于预想之内的错误,不同的情况有不同的处理方式。具体来说有以下几种处理方式。
- 返回无害的值
在确认某值无害的情况下,返回该值。
比如在数值计算的情况下返回0,在字符串计算的情况下返回空字符串,在指针计算的情况下返回NULL。
- 使用下一个数据
在处理一连串数据的情况下,返回下一个有效数据。
以从数据库读取记录为例,如果记录无效,则一直读取,直到发现有效记录。
- 返回和前面一样的值
如果不会对结果造成重大影响,则返回和前面一样的值。
以1秒内读取100次温度计的代码为例,如果其中有一次读取失败,在这种情况下,即使返回失败前最后一次读取的值,也不会有什么问题。
- 使用近似值
在满足一定的严密性的前提下,返回近似值。
比如在能显示0℃~100℃的温度显示画面中,温度低于0℃时显示0℃,高于100℃时显示100℃。
- 在日志中记录警告信息
在日志文件中记录警告信息后继续执行处理。
当发生微小的错误时,忽略错误继续执行处理有时是一个很好的选择。不过,发生过的错误一定要记录下来。
- 返回错误
为了调用上游函数来处理错误,我们要将检测出来的错误记录在报告中。
在这种情况下,决定让代码的哪个部分负责处理错误,哪个部分负责报告错误就变得至关重要。
我们可以使用模块的状态变量、函数的返回值,或者通过抛出异常来报告错误。
- 调用错误处理函数
错误处理要交给共同的错误处理函数来完成。
将错误处理的责任一元化能降低调试的难度。不过,这个一元化的功能会使代码整体产生较高的耦合度。因此,如果想把一部分代码用到其他系统中,就需要连同错误处理算法一起“搬家”。
- 显示错误信息
在发生错误的地方显示错误信息。
将错误处理的开销抑制到最小。不过,由于信息会分散在软件各处,所以创建具有统一性的用户接口、区分用户接口与其他部分、将软件转换为其他语言等工作变得难以实施。
- 终止处理
检测到错误后终止处理。
这个方法对重视安全性的软件来说非常有效。在关键任务系统中,比起带着错误继续处理,很多时候重新启动程序会比较好。
- 各部分选择最合适的方式处理错误
选择何种方式处理错误,由负责设计与实现错误发生部分的程序员来决定。
这给了程序员很大自由,但从软件整体来看,错误处理将失去统一性。
拓展2:
错误处理中的“正当性”和“坚固性”
错误处理中有“正当性”和“坚固性”两种思路。
正当性指一定不返回不正确的结果。与其返回不正确的结果,不如什么都不返回。
而坚固性指为了让软件继续运行而不择手段。即使会产生不正确的结果,也要让软件继续运行下去。
以哪种思路为先,就要看软件的目的是什么了。
重视安全性的软件要以正当性为先。与其返回错误结果,不如直接停止软件。以医疗相关的管理软件为例,相较于返回错误结果继续处理,通知错误并停止软件更能防止重大事故的发生。
而对于提供给用户的软件,坚固性就要优先于正当性了。以文字处理软件为例,比起软件突然关闭导致大量宝贵的输入数据丢失,带着错误继续运行所造成的损失更小。
拓展3:
不忽视错误代码
不忽视错误代码是防御性编程的铁则。
即使函数返回错误代码,接收方也有可能会忽视掉它。但是,我们一定要养成评价函数返回值的习惯。即便某个函数在理论上不会发生错误,保险起见我们也要对其进行检查。因为防御性编程的目的就是防止预料之外的情况出现。
自己编写的函数不能忽视错误,系统函数同样不能。每次进行系统调用都要检查错误代码。
发现错误之后,要在日志中输出错误编号以及错误的详细内容。
破窗效应
是什么?
不好的代码是“蚁穴”
如果大楼这类建筑物上有一扇长期未被修理的窗户,这栋大楼就会给人一种“被遗弃”的感觉。人们便不会再留心这栋大楼的状态。
这样的话,还会有窗户继续碎掉。接着是垃圾乱倒,满墙涂鸦。别看只是破了一扇窗户,如果放置不管,整栋建筑也会遭到严重的破坏。
软件也会发生这样的事情。如果对软件的“破窗”,也就是那些不好的设计、错误的决定或不好的代码放置不管,那么不论它多么微不足道,也能在很短的时间内让整个软件腐烂。
为什么?
不好的代码会带来邪念
软件中一旦存在“破窗”,程序员的脑中就会不自觉地产生“剩下的代码肯定也是一团糟,随便改一改算了”的想法。
关于这种现象,有一个叫作“信箱实验”的著名心理学实验。如果自家信箱附近的墙壁上有涂鸦,或者信箱附近有垃圾,那么该信箱中信件被盗的概率就会达到25%。仅仅是一些垃圾和涂鸦,就能将许多正直人士变成小偷。
除了从众心理之外,我们也可以用“莫名的不安”这种心理来解释为什么会出现这种现象。一扇被弃之不管的破窗户,会让人产生“在这附近遇到危险的话肯定没人来救”的想法,随之让人产生不安的情绪。即便是一些细枝末节的东西,如果总是以一种没有得到处理的状态摆在人们眼前,也会让人渐渐变得神经质,使人的交感神经处于紧张状态, 甚至促使人付诸暴力。
也就是说,出现这种现象的关键原因,与其说是“破窗户”本身,不如说是小小的问题被弃之不管而带来的“不安”。相较于时间短强度大的精神压力,人们对时间长强度小的精神压力更加敏感。当某些有违社会道德的现象一直出现在我们的眼前时,人就会暴露出脆弱性。
怎么做?
保持代码整洁
我们不能对代码的“破窗”,也就是代码不好的部分放置不管,要在发现“破窗”的时候立即进行修补。没有了“破窗”,代码就能保持整洁的状态,这样一来,程序员便会小心翼翼地对待这些代码,避免弄脏它们。就算交付日期近在眼前,也没人愿意当第一个弄脏代码的人。
另外,如果没有足够的时间修复代码,至少要简单明了地指出“这段代码不好”。
比如对于自己认为不好的地方,可以添加带标签的注释以显示在IDE(Integrated Development Environment,集成开发环境)的任务列表里。这么做的目的是强调这些不好的地方已经得到了管理,防止损害进一步扩大。
扩展:
人会模仿人
破窗效应既与“莫名不安”的心理因素有关,也与“反射性模仿他人行为”的人类自身特性有关。
心理学中已经证实,人类在婴儿时期就已经具备“反射性模仿他人行为”的特性了。不过,这个特性需要有足够长的时间才会显现出来。如果人们长期处于一种低素质的“习惯性懈怠”的状态,就会去模仿他人不好的行为,最终陷入恶性循环,这也可能是破窗效应出现的原因。
不过,不管是因为“莫名不安”还是“反射性模仿他人行为”,及时解决不好的代码都是不变的应对策略。
熵增原理
是什么?
代码会自然而然地开始腐坏
熵是物理学术语,表示体系的混乱程度。根据热力学法则,人们证明了全宇宙的熵处于增加状态。
软件开发可以超越大部分的物理法则,却逃不出熵增原理的束缚。如果不对代码进行管理,其混乱程度就会不断加深,直到突破极限。也就是说,代码会逐渐转向腐坏。
为什么?
代码会向着混乱的方向转变
代码变得越来越混乱是软件开发中自然而然的事情。
不管开头多么有序,只要过上一阵子,代码就会开始腐坏。就像生肉放久了会变质一样,随着时间的推移,代码的腐坏程度会越来越深。臃肿的代码越积越多,使得维护难度不断增大。用不了多久,即便是很小的修改都需要耗费大量劳力,迫使我们不得不重新设计软件。
在这种情况下,重新设计软件很难一帆风顺。如今的软件日新月异,新的设计必须能跟得上时代的变迁才行。
也就是说,我们就算有非常明确的目标,也难免会跟不上步调,因为我们在实际工作时打的是“移动的靶子”。
怎么做?
抓住代码腐坏的征兆
代码开始腐坏时有几个征兆。不要放过这些征兆,发现它们后立刻处理。
- 刻板
刻板指不容易修改代码。
仅仅因为一处修改,就需要对所有与其存在依赖关系的模块进行修改,我们称这种代码设计为刻板的设计。
刻板的设计会给我们带来很多困扰。比如我们接到委托,要对代码做一个很简单的修改,于是简单调查了需要修改的地方,预估了工作量。然而在实际工作时,随着工作的推进,我们还是要对其他预想之外的地方进行修改。结果,工作量远远超出预估,我们只能在规模庞大的代码中追查需要修改的地方。
- 脆弱
脆弱指一处修改会对其他部分的代码造成很大损害。脆弱的代码甚至会损坏与其完全不相关的代码。因此,程序员在处理新问题时就可能会引发其他问题,这就使程序员陷入追着自己尾巴跑的状态。
毫不夸张地说,脆弱的模块并不罕见。这类模块很容易辨认。那些需要经常修复的模块、常年出现在故障列表中的模块、程序员认为需要重新设计的模块,以及越修复质量越差的模块等就属于脆弱的模块。
- 可移植性差
可移植性差指软件难以移植到其他环境中。
如果软件在任何环境下分离可运行部分和依赖环境的部分都会出现困难并伴随风险,我们就可以说该软件不具备可移植性。
- 难以掌控
难以掌控指代码难以掌控和开发环境难以掌控。
代码难以掌控是指设计结构不具备灵活性。我们无法在保持设计结构的前提下轻松修改难以掌控的代码。相较于能保持设计结构的方法,使用投机取巧的方法更能轻松地完成修改。在代码难以掌控的状态下,做错事容易,做对事反而难。
而开发环境难以掌控常发生在开发环境效率低下的时候。比如,当编译需要花费大量时间时,即使我们知道已经无法保持设计结构了,还是会倾向于采用能避免大规模编译的修改方式。如果提交确认两三个文件需要花费好几个小时,我们就不会再思考保持设计结构的方法了,而是会寻找更节约时间的修改方式。
- 复杂
复杂指不必要的元素过多。
当程序员预判规格说明书会发生变更,在代码中事先埋下应对机制时,就容易使代码变得复杂。这类做法总给人一种好的印象。很多人认为预见未来并提早做出准备就能保持代码的灵活性,防止今后苦于修改。
然而很遗憾,这样做只会带来相反的效果。为应对更多不测,我们会在代码中留下大量一次都用不上的结构。这会让代码变得复杂,变得难以理解。
- 重复
重复指同样的代码出现多次。
在写文档时,复制粘贴是一个很好用的方法,但在编辑代码时,使用复制粘贴则会招来很严重的后果。在代码出现重复的情况下,修改软件将成为一项劳神费力的工作。如果在重复的部分发现故障,就需要修改代码中所有相同的部分。
况且,代码有时候看上去相同,但实际上有着细微的差别,这时修改方式就可能不同了。
如果这种看上去相同但存在细微差别的代码在软件中大量出现,就表示程序员没有做抽象化工作。如果能找出所有重复的部分,将其适当抽象化,消除重复,系统将更容易理解且更容易维护。
- 不透明
不透明指代码难以理解。
代码有时候很难让人理解。而频繁修改的代码会随着时间的流逝越来越难以让人理解。
在刚写完代码时,代码对编码者本人来说是非常明了的,因为编码者沉浸于开发,熟悉该项目的每个地方。然而过一段时间再回过头来看,编码者就会觉得自己怎么能写出如此不堪的代码。
为了防止此类情况发生,编码者需要站在代码阅读者的立场思考,写出别人能够理解的代码。让别人来看自己写的代码是一个行之有效的方法。
80-10-10原则
是什么?
编程没有万能药
我们在用高水平的工具或语言开发软件时,可以在非常短的时间内实现用户80% 的需求。而在剩下20%的需求中,有10的需求需要通 过一定努力才能实现,另10%则完全不可能实现。
因此,如果要100%满足用户的需求,开发就会陷入进退两难的境地。
如果此时已经开发一部分内容了,那么抛弃原有工具重新开发就显得不切实际。这时,我们就得放弃使用工具,用最笨拙的方式来满足某部分需求。
为什么?
编程的问题领域太广
软件行业从20世纪90年代中期起,举整个行业之力花费十几年做了一场实验。实验内容是创造一款能够让能力平庸的技术人员的生产效率飞跃性提升的“万能工具”,比如模型驱动开发、4GL(第四代语言)等。
实验的结果显示,使用单一工具很难在所有领域都获得完美的成果。
人们创建这种工具是为了开发出更人性化、质量更好的软件。因此,为了防止能力平庸的技术人员引发问题,人们对语言施加了相当强的功能限制。结果,工具产生了自己的“防守范围”。
但软件要处理的问题范围是无限大的。用一个工具解决所有问题的“万能药”路线显然走不通。
第二系统综合征
是什么?
第二次发布总会出现功能过多的情况
由发布第一版软件的程序员设计的第二版软件会成为最危险的一个版本。
第二版软件有功能过多、质量差以及功能的使用体验较差等倾向。
为什么?
人在适应开发后会倾向于“多功能主义”
在开发第一版软件时,由于未知的情况很多,风险较高,所以我们在进行判断时会比较慎重。即便想到了好的功能,也会留到下一次再实现。
然而,在开发第二版软件时,我们掌握了更多的信息,也有了自信,所以倾向于把之前保留的功能以及新想到的功能一股脑儿加进去。
添加过多功能之后,代码变得复杂,不易维护。功能本身也变得复杂,使用体验变差,结果添加的功能也没能得到人们的青睐。不管是代码还是实现的功能,质量都较以前有所下降。
另外,那些暂时保留的功能在第一版软件中也许是比较实用的,但在第二版软件中,这些功能可能已经失去了必要性,或者落后于时代了。也就是说,把这部分功能放到第二版软件中实现是一种浪费时间的做法。
怎么做?
考虑用户
程序员要有自制力,避免陷入多功能主义的怪圈。
要做到这一点,一个有效的做法就是重新对用户进行定义并将用户具象化。此时不论是有意识的还是无意识的,程序员对用户的印象都会对程序员的判断产生影响。这就给程序员添加新功能的欲望带上了“枷锁”。
具体做法就是在编程时多想想以下问题。
- 用户是谁
- 用户需要什么
- 用户认为什么是必要的
- 用户想要什么
拓展:
第二系统后综合征
前面说程序员容易在第二版软件中产生多功能主义的倾向,但实际上,第二版以后的版本也会出现同样的情况。
特别是数据包软件等需要持续发布的软件,随着一次次版本升级,没用的功能会越来越多。
出现这种现象的原因可能是用户群体不固定,程序员很难对用户进行具象化。而且功能一旦发布就很难有机会删除,所以只能越积越多。
不可否认,添加功能可以提升软件的魅力。但是,相较于新功能,用户往往希望基本功能是稳定的,或者基本功能的使用体验能得到改善。
功能蔓延:
功能的过分扩张不能全部归罪于程序员的一己私欲,毫无原则地满足用户的需求也是重要原因之一。
无条件满足用户的愿望,就会在软件中增加大多数用户用不到的功能,还要准备用于控制该功能的复杂的设置画面以及相关设置文件。如此一来,软件就会变得难以维护,故障频出。
这种功能肆意增多的现象称为功能蔓延(feature creep),该现象意味着软件开始迈向破灭(或者已经破灭了)。
软件设计的终极之美是“简单”。越是简单优质且拥有众多用户的软件,越容易出现更多的需求。如果忠实地满足这些需求,将所有功能都开发出来,软件将失去简单性,变成一款没人用的软件。这时我们就会陷入进退两难的窘境。
避免出现这种悲剧的关键是要有勇气对需求说“NO”。对于那些与软件核心无关、需要与其他软件组合才能实现的功能,我们要明确地说“NO”。只有这样,才能产生优秀的设计,才能让软件保持简单性。
不过,有时候我们很难拒绝用户强烈的诉求。这时,我们不要直接在软件主体中实现该功能,而是要围绕软件主体进行扩展,或者以插件的形式在不改变软件核心代码的前提下修改软件的运行模式,以此来保持软件主体的简单性。
重新发明车轮
是什么?
制作已有的东西
有时候对于某种功能,明明有现成的代码或库可以使用,人们却还要自己重新开发相同的功能。这就像专门花时间又重新发明一遍世上早就有的车轮一样,是一种无用功。
有现成的东西,却要去重新发明一个,这是在浪费时间。当开发规模足够大时,其危害也是非常大的。想要一个“能运行各种服务的服务器”,于是专门把Web 服务器这种规模极大的软件重新开发了一遍。这种做法会浪费非常多的时间。
而且在大部分情况下,相较于重新发明出来的东西,既有产品的质量更好。比如相较于我们现写出来的库,既有的标准库更好,因为它不仅能反映出提供标准库的专家的知识,还能反映出人们在使用过程中积累的经验。标准库还有一个好处,那就是就算我们什么都不做,随着时间的推移,其中的故障、功能和性能也会自行改善。
另外,如果忽视标准规格,根据自己的协议编写代码,将来就只能走自己的路线了。仅靠本地的几个程序员是不可能跟得上世间的主流的。另外,由于所有的插口都是独创的,所以将来也无法实现替换。
为什么?
不知道车轮和想制作车轮
重新发明车轮的原因有以下两种。
- 不知道车轮
程序员不知道车轮的存在。也就是说,这种发明不是程序员有意而为的。
这归咎于程序员的知识不足和学习不足。编写与语言标准库功能相同的代码,或者在有标准协议的情况下用独创的格式编写通信功能的代码等都属于这种情况。
- 想制作车轮
程序员有制作车轮的欲望。也就是说,这种发明是程序员有意而为的。
这是一种叫作“非我发明”(Not Invented Here,NIH)综合征的问题。具体表现为某个东西原本没有重新制作的必要,程序员却出于对技术的兴趣或排斥他人制作的东西而想重新制作一遍。
怎么做?
关注车轮之外的东西
我们要避免重新发明车轮,将重点放在本来应该做的工作上。
为此,在编写代码之前,一定要先确认是否存在相同功能的标准库、开源库,是否存在标准协议等。
另外,要借助团队会议等机会从其他程序员处获取信息。这样就能避免团队内出现重复劳动的情况。
同时,在团队中彻底清除利己主义的思想。
因为想做而做,这是程序员自私的一面。然而,软件的目的不是满足程序员的欲望,而是满足用户的需求。为了用户,为了在质量、开发时长和费用等方面做到最好,我们应该时常调查哪些东西可供使用,掌握高质量的开源工具或商用工具。
拓展:
许重新发明车轮的情况
有时我们也需要大胆地重新发明车轮。
- 商业目的
商业上的核心部分必须由自己制作。
在使用已有的东西时,必然会对该部分产生依赖。依赖则意味着对该部分失去了控制权。
即便知道其中潜藏着致命的问题,我们也无法主动去修改。就算可以委托他人修复,何时能够发布,是否真的能得到改善,都是未知数。质量和交付期方面的问题很可能在商业上造成无可挽回的损害。
况且,使用已有的东西就意味着放弃该部分的“差别化”。因此,商业上的核心部分,从原则上来讲都应该由自己制作。只有自己制作出这部分内容,并且花心思做出个性,从中积累经验,才能开发出独特的、能贡献于世界的软件。
- 学习目的
要成为优秀的程序员,就得不断积累高质量的经验。
软件开发的模式、设计和编程等方面的好书有很多,然而读书和实践之间有很大的差别。
同样,借用已有的代码与自己从零设计、测试软件,解决故障,提高软件质量得来的经验有天壤之别。
不过,有机会编写软件核心部分代码的程序员少之又少。大部分程序员只能借用已有代码。在这种情况下,我们不知道代码内部是如何运作的,因此和使用“黑箱”没什么区别。
只看水面的话,我们是无法得知水下隐藏着何种危险的。如果不知道水底究竟发生了什么,就不能灵活运用水流。自己亲手制作是一种必要的经历。为此而“重新发明车轮”是程序员学习、提高技术非常有效的一个方法。
当然,我们免不了失败,但这种经历也比直接拿现成的使用要宝贵。
亲手从零开始写代码,进行各种尝试,从一次次失败中学习,能带来不同于阅读技术类图书的好处。不过,读书与实践同等重要,它们对程序员来说都是不可或缺的。
给牦牛剃毛
是什么?
抓不住问题的本质
有种家畜叫牦牛。它是牛的一种,特征是身上长着厚厚的毛。每当临近夏天,牦牛就需要剃毛。我们需要给牦牛剃去相当多的毛才能让它的皮肤露出来。
我们处理某些问题时就像给牦牛剃毛一样,在解决问题的过程中总会有新的问题冒出来,让我们难以抓住问题的本质。
这种状态如果持续太久,人们就可能会忘记原本要解决的问题是什么。
为什么?
问题会接二连三地出现
问题总是接二连三地出现。
假设我们想导入在Web 服务器上运行的任务自动化工具,以提高工作效率。
“先下载Web服务器程序。”
“文件太大了,没有办法下载。”
“那就导入下载工具。”
“下载工具怎么不运行呀?”
“原来需要前置模块啊。”
“那就下载前置模块。”
“需要注册用户。”
“那就注册一个吧。”
“诶?用户注册页面不动了。”
“原来是浏览器版本太老了。”
“升级了浏览器,注册了用户,模块也下载好了。”
“怎么下载工具还是不运行?”
“哎呀,需要操作系统的补丁包。”
(后面依然没完没了。)
这种像给牦牛剃毛一样的情况会造成时间上的浪费。有时候,就算我们预估了工作所需时间,也没有办法在预估的时间内完成工作,这种情况发生的原因就是我们把时间耗费在了给牦牛剃毛上。
另外,在给牦牛剃毛的状态下,人非常容易积攒压力。我们很难推测出需要花多长时间才能把牦牛身上的长毛剃光。如果这种无法达成目标的状态一直持续下去,人就会产生挫败感。
怎么做?
尽早收手
当我们发觉自己已经陷入给牦牛剃毛的状态时,应停下脚步,回想自己原本要实现的目标是什么。如果发现自己已经偏离了目标,或者从时间、成本的角度来看不适合再继续操作下去了,应立刻停止工作。因为在这种情况下,寻找其他出路往往会带来更好的结果。
另外,为防止其他人也陷入同样的状态,我们应将整个过程分享给团队成员。在一个全员共享的空间留下一份笔记,能够防止他人浪费时间。
拓展:
勇于面对“给牦牛剃毛”
一般来说,见到要给牦牛剃毛的情况应该绕着走。但是,出于一些有价值的目的,或者因为紧急故障等,有时我们必须跨越“给牦牛剃毛”的障碍,解决问题。
这个时候最麻烦的是我们大脑解决问题的速度跟不上问题出现的速度。由于前一个问题尚未解决就冒出了下一个问题,所以我们的大脑在解决问题时往往像使用栈一样,先让问题入栈,再一个一个出栈解决(同时让继发的新问题入栈)。这就是给牦牛剃毛的状态。在这种情况下,问题通常会接二连三地发生,出栈速度赶不上入栈速度,导致脑内栈溢出。
为了防止这类情况的发生,我们要记住不能只在脑中解决问题。应当把问题写下来,一个一个地解决。
编程中的“给牦牛剃毛”
给牦牛剃毛的情况常出现在搭建环境的过程中。不过,编程中也会遇到类似的情况。
比如,在写代码时,由一个问题联想到其他问题,离最初要解决的问题越来越远。在最坏的情况下,我们甚至会忘记最初或中间想到的问题是什么。
又比如,在读代码时,由于代码未整理,所以我们很难找到当初想知道的东西。在梳理错综复杂的调用关系时,一不小心就会忘记代码读到了哪里,或者读代码的目的是什么。
为防止这类情况发生,我们在读写复杂的代码时,要一边做记录一边操作。特别是在写代码时,我们需要思考的部分比实际操作的部分要多,不做记录的话就可能会有陷入循环思考的状态。
更多推荐
【笔记】编程的原则:改善代码质量的101个方法
发布评论