本文阅读时间大约10分钟。
我们工作中的大部分时间都是在跟别人(包括半年前的你)写的老的代码打交道,开发者的最重要的产出就是代码,如果代码的可读性、可扩展性、可维护性不好,那么对于后面的维护者来说就是一个噩梦,会花费大量时间在还技术债务。本书没有谈论宏观的技术架构,而是专注于代码的细节,整理了很多提供代码可读性的实用技巧,值得阅读。本书的纸质版已经无法买到,我是在微信读书阅读的电子版,体验很好。
◆ 译者序
程序员之间的互相尊重体现在他所写的代码中。他们对工作的尊重也体现在那里。
◆ 前言
程序员的日常工作的大部分时间都花在一些“基本”的事情上,像是给变量命名、写循环以及在函数级别解决问题。并且这其中很大的一部分是阅读和编辑已有的代码
◆ 第1章
代码应当易于理解
◆ 是什么让代码变得“更好”
我会选择第二种,因为第二种可以将复杂性一分为二,至少我可以分别看两个分支分别的逻辑是在干啥。都放在一行,出错的时候不好debug,阅读理解的时候也会互相干扰。
第一个版本更紧凑,但第二个版本更直白。哪个标准更重要呢?一般情况下,在写代码时你如何来选择
◆ 可读性基本定理
这里的理解就是理解了背后的设计思路,敢于和能够改动对应的代码,而不是表面的看懂
当我们说“理解”时,我们对这个词有个很高的标准。如果有人真的完全理解了你的代码,他就应该能改动它、找出缺陷并且明白它是如何与你代码的其他部分交互的。
当我们说“理解”时,我们对这个词有个很高的标准。如果有人真的完全理解了你的代码,他就应该能改动它、找出缺陷并且明白它是如何与你代码的其他部分交互的。
◆ 总是越小越好吗
有些团队到目前还在用代码行数的多少来作为考核指标,想想真是笑话;代码的长度和多少永远不应该作为考核指标,可维护性、可扩展性、稳定性才是
因此尽管减少代码行数是一个好目标,但把理解代码所需的时间最小化是一个更好的目标。
因此尽管减少代码行数是一个好目标,但把理解代码所需的时间最小化是一个更好的目标。
◆ 理解代码所需的时间是否与其他目标有冲突
当你犹豫不决时,可读性基本定理总是先于本书中任何其他条例或原则。
◆ 避免像tmp和retval这样泛泛的名字
好的名字应当描述变量的目的或者它所承载的值
◆ 为名字附带更多信息
基本上,如果这是一个需要理解的关键信息,那就把它放在名字里。
◆ 名字应该有多长
因此如果一个标识符有较大的作用域,那么它的名字就要包含足够的信息以便含义更清楚。
◆ 利用名字的格式来传递含义
规范不是死的,各有特色,但是在一个项目里应该尽量保持一致
是否要采用这些规范是由你和你的团队决定的。但不论你用哪个系统,在你的项目中要保持一致。
是否要采用这些规范是由你和你的团队决定的。但不论你用哪个系统,在你的项目中要保持一致。
◆ 例子:Filter()
我平常使用filter的时候,一般表示要留下的数据,不过这里确实有二义性
最好避免使用“filter”这个名字,因为它太容易误解
◆ 给布尔值命名
通常来讲,加上像is、has、can或should这样的词,可以把布尔值变得更明确。
◆ 用方法来整理不规则的东西
使代码“看上去漂亮”通常会带来不限于表面层次的改进,它可能会帮你把代码的结构做得更好。
◆ 把声明按块组织起来
我们的大脑很自然地会按照分组和层次结构来思考
这个版本容易理解多了。它还更易读,尽管代码行数更多了。原因是你可以快速地找出4个高层次段落,然后在需要时再阅读每个段落的具体内容。
◆ 把代码分成“段落”
请注意, 我们还给每个段落加了一条总结性的注释,这也会帮助读者浏览代码
◆ 个人风格与一致性
一致的风格比“正确”的风格更重要。
◆ 什么不需要注释
不要为那些从代码本身就能快速推断的事实写注释。
这个函数现在更加“自我说明”了。一个好的名字比一个好的注释更重要,因为在任何用到这个函数的地方都能看得到它
通常来讲,你不需要“拐杖式注释”——试图粉饰可读性差的代码的注释。写代码的人常常把这条规则表述成:好代码>坏代码+好注释。
◆ 记录你的思想
这段注释承认代码很乱,但同时也鼓励下一个人改正它(还给出了具体的建议)。如果没有这段注释,很多读者可能会被这段乱代码吓到而不敢碰它。
这不过是匆匆记下你在决定这个常量值时的想法而已。
◆ 站在读者的角度
我们的建议是你可以做任何能帮助读者更容易理解代码的事。这可能也会包含对于“做什么”、“怎么做”或者“为什么”的注释(或者同时注释这三个方面)。
◆ 最后的思考——克服“作者心理阻滞”
请注意我们把写注释这件事拆成了几个简单的步骤:
1.不管你心里想什么,先把它写下来。
2.读一下这段注释,看看有没有什么地方可以改进。
3.不断改进。
当你经常写注释,你就会发现步骤1所产生的注释变得越来越好,最后可能不再需要做任何修改了。并
◆ 用输入/输出例子来说明特别的情况
对于注释来讲,一个精心挑选的输入/输出例子比千言万语还有效。
◆ 声明代码的意图
给出了比代码本身更多的信息
注释从更高的层次解释了这段程序在做什么。
注释从更高的层次解释了这段程序在做什么。
◆ if/else语句块的顺序
一般会先将特殊逻辑和异常逻辑处理了,然后剩下的就是正常流程,需要再想想
同样,根据具体情况的不同,这也是需要你自己来判断的。
◆ 从函数中提前返回
有些程序员认为函数中永远不应该出现多条return语句。这是胡说八道。从函数中提前返回没有问题,而且常常很受欢迎。
◆ 最小化嵌套
技术债务,有借有还 再借不难
当你对代码做改动时,从全新的角度审视它,把它作为一个整体来看待。
当你对代码做改动时,从全新的角度审视它,把它作为一个整体来看待。
与if(...)return;在函数中所扮演的保护语句一样,这些if(...) continue;语句是循环中的保护语句。
◆ 总结
这也是为啥看js的多层递归嵌套很难理解的原因
嵌套的代码块需要更加集中精力去理解。每层新的嵌套都需要读者把更多的上下文“压入栈”。应该把它们改写成更加“线性”的代码来避免深嵌套。
通常来讲提早返回可以减少嵌套并让代码整洁。“保护语句”(在函数顶部处理简单的情况时)尤其有用。
嵌套的代码块需要更加集中精力去理解。每层新的嵌套都需要读者把更多的上下文“压入栈”。应该把它们改写成更加“线性”的代码来避免深嵌套。
通常来讲提早返回可以减少嵌套并让代码整洁。“保护语句”(在函数顶部处理简单的情况时)尤其有用。
◆ 例子:与复杂的逻辑战斗
一般正向思考一个事情非常复杂的时候,反向思考通常会简单一些,因为总的分支和变量是固定的
但是找到更优雅的方式需要创造力。那么怎么做呢?一种技术是看看能否从“反方向”解决问题。根据你所处的不同情形,这可能意味着反向遍历数组,或者往回填充数据结构而非向前。
在这里,OverlapsWith()的反方向是“不重叠”。判断两个范围是否不重叠原来更简单,因为只有两种可能:
1.另一个范围在这个范围开始前结束。
2.另一个范围在这个范围结束后开始。
但是找到更优雅的方式需要创造力。那么怎么做呢?一种技术是看看能否从“反方向”解决问题。根据你所处的不同情形,这可能意味着反向遍历数组,或者往回填充数据结构而非向前。
在这里,OverlapsWith()的反方向是“不重叠”。判断两个范围是否不重叠原来更简单,因为只有两种可能:
1.另一个范围在这个范围开始前结束。
2.另一个范围在这个范围结束后开始。
◆ 总结
判断条件如果特别复杂,就把他们抽出来作为一个方法,然后利用本章的内容进行优化
例如if (!(a && !b))变成if (!a || b))。
◆ 减少变量
写出好代码真的是艺术,一点不多,一点不少,一切都刚刚好,这真是是理想的状态;现实世界在项目进度和需求变更的压力下,能保留多少这种状态,确实是我们做技术的同学需要坚持和评估的
通过让代码提前返回,我们不再需要index_to_remove,并且大幅简化了代码。通常来讲,“速战速决”是一个好的策略。
通过让代码提前返回,我们不再需要index_to_remove,并且大幅简化了代码。通常来讲,“速战速决”是一个好的策略。
◆ 缩小变量的作用域
最小权限和最小可见性原则,放在类、模块的设计上也是这样
实际上,让所有的变量都“缩小作用域”是一个好主意,并非只是针对全局变量。
实际上,让所有的变量都“缩小作用域”是一个好主意,并非只是针对全局变量。
变量在使用之前再定义
原来的C语言要求把所有的变量定义放在函数或语句块的顶端。这个要求很令人遗憾,因为对于有很多变量的函数,它强迫读者马上思考所有这些变量,
原来的C语言要求把所有的变量定义放在函数或语句块的顶端。这个要求很令人遗憾,因为对于有很多变量的函数,它强迫读者马上思考所有这些变量,
◆ 只写一次的变量更好
不可变的对象才不会带来麻烦
如James Gosling(Java的创造者)所说:“(常量)往往不会引来麻烦。”
如James Gosling(Java的创造者)所说:“(常量)往往不会引来麻烦。”
◆ 第10章
所谓工程学就是关于把大问题拆分成小问题再把这些问题的解决方案放回一起
你将会看到,这是个简单的技巧却可以从根本上改进你的代码。然而由于某些原因,很多程序员没有充分使用这一技巧。这里的诀窍就是主动地寻找那些不相关的子问题。
◆ 介绍性的例子:findClosestLocation()
写代码、做设计都是需要在同一个抽象层次思考🤔
这段代码的可读性好得多,因为读者可以关注于高层次目标,而不必因为复杂的几何公式分心。
这段代码的可读性好得多,因为读者可以关注于高层次目标,而不必因为复杂的几何公式分心。
◆ 纯工具代码
工作中也应该积累自己的工具代码库,不管用什么语言😀😀
通常来讲,如果你在想:“我希望我们的库里有XYZ()函数”,那么就写一个!(如果它还不存在的话)经过一段时间,你会建立起一组不错的工具代码,后者可以应用于多个项目。
通常来讲,如果你在想:“我希望我们的库里有XYZ()函数”,那么就写一个!(如果它还不存在的话)经过一段时间,你会建立起一组不错的工具代码,后者可以应用于多个项目。
◆ 其他多用途代码
分而治之
当format_pretty()中的代码自成一体后改进它变得更容易。当你在使用一个独立的小函数时,感觉添加功能、改进可读性、处理边界情况等都更容易。
当format_pretty()中的代码自成一体后改进它变得更容易。当你在使用一个独立的小函数时,感觉添加功能、改进可读性、处理边界情况等都更容易。
◆ 创建大量通用代码
重要的是最终的结果:移除并单独解决子问题。
◆ 简化已有接口
最近在写代码的时候感触颇深,代码认真写,BUG真的会少很多;设计仔细做,代码会好写很多
这里我们学到的是“你永远都不要安于使用不理想的接口”。你总是可以创建你自己的包装函数来隐藏接口的粗陋细节,让它不再成为你的阻碍。
这里我们学到的是“你永远都不要安于使用不理想的接口”。你总是可以创建你自己的包装函数来隐藏接口的粗陋细节,让它不再成为你的阻碍。
◆ 按需重塑接口
最近有用这个技巧,真正的业务动作才是业务逻辑代码需要关注的,至于为了支撑这些业务动作的代码,都可以封装起来,隔离关注点
然后得到的程序中执行“真正”逻辑的代码很简单:
然后得到的程序中执行“真正”逻辑的代码很简单:
◆ 过犹不及
函数拆得太细也是有问题的,只有两种情况需要拆分:(1)不同层次的逻辑混杂在一起;(2)低层次的逻辑比较复杂和独立,又很可能会被其他接口复用
为代码增加一个函数存在一个小的(却有形的)可读性代价。在前面的情况里,付出这种代价却什么也没有得到
为代码增加一个函数存在一个小的(却有形的)可读性代价。在前面的情况里,付出这种代价却什么也没有得到
◆ 总结
原则“把一个方法中的所有操作保持在一个抽象层次上”
◆ 更大型的例子
分而治之
在这种方法里,我们简单地把问题从不同的角度“切开”。两种方法都很有可读性,因为它们让读者一次只需要思考一件事情。
在这种方法里,我们简单地把问题从不同的角度“切开”。两种方法都很有可读性,因为它们让读者一次只需要思考一件事情。
◆ 了解函数库是有帮助的
这就是为啥要对自己使用的平台和语言足够熟悉的原因
编写精练代码的一部分工作是了解你的库提供了什么。
编写精练代码的一部分工作是了解你的库提供了什么。
◆ 把这个方法应用于更大的问题
这个有意思,先用自然语言将要做的事情的步骤描述清楚,然后转换成对应的代码
这个描述清晰得多,并且比以前的代码更优雅。
这个描述清晰得多,并且比以前的代码更优雅。
◆ 总结
本章讨论了一个简单的技巧,用自然语言描述程序然后用这个描述来帮助你写出更自然的代码。这个技巧出人意料地简单,但很强大。
另一个看待这个问题的角度是:如果你不能把问题说明白或者用词语来做设计,估计是缺少了什么东西或者什么东西缺少定义。把一个问题(或想法)变成语言真的可以让它更具体。
◆ 熟悉你周边的库
尽量不要重复造轮子,对于常用的库:jdk、guava、apache.commons系列等要做到烂熟于心
在一个成熟的库中,每一行代码都代表相当大量的设计、调试、重写、文档、优化和测试。任何经受了这样达尔文进化过程一样的代码行就是很有价值的。这就是为什么重用库有这么大的好处,不仅节省时间,还少写了代码。
在一个成熟的库中,每一行代码都代表相当大量的设计、调试、重写、文档、优化和测试。任何经受了这样达尔文进化过程一样的代码行就是很有价值的。这就是为什么重用库有这么大的好处,不仅节省时间,还少写了代码。
◆ 第14章
测试对不同的人意味着不同的事。
◆ 让错误消息具有可读性
Java生态里是JUnit和TestNG
无论你用什么语言,都可能会有一个库/框架(例如XUnit)来帮助你。了解那些库对你有好处!
无论你用什么语言,都可能会有一个库/框架(例如XUnit)来帮助你。了解那些库对你有好处!
◆ 选择好的测试输入
八月份在团队中,PD进行验收测试时设计的测试用例,就符合这个原则:简单,但是覆盖面广
又简单又能完成工作的测试值更好。
又简单又能完成工作的测试值更好。
小的、简单的测试更好维护,相比于前面说的简单而全面的测试,这个更好实现一点;对于不熟悉的产品,我倾向于先创建和梳理小而简单的测试,有能力的情况下实现小而覆盖面广的测试
与其建立单个“完美”输入来完整地执行你的代码,不如写多个小测试,后者往往会更容易、更有效并且更有可读性。
与其建立单个“完美”输入来完整地执行你的代码,不如写多个小测试,后者往往会更容易、更有效并且更有可读性。
◆ 为测试函数命名
我在写测试的时候,测试方法的命名习惯:方法名Test测试场景
依照测试的精细程度不同,你可能会考虑为测试的每种情形写一个单独的测试函数。可以使用Test<;FunctionName><;Situation>()这样的格
依照测试的精细程度不同,你可能会考虑为测试的每种情形写一个单独的测试函数。可以使用Test<;FunctionName><;Situation>()这样的格
这里不要怕名字太长或者太繁琐。在你的整个代码库中不会调用这个函数,因此那此要避免使用长函数名的理由在这里并不适用。测试函数的名字的作用就像是注释。并且,如果测试失败了,大部分测试框架会输出其中断言失败的那个函数的名字,因此一个具有描述性的名字尤其有帮助。
◆ 对测试较好的开发方式
在所有的把一个程序拆分成类和方法的途径中,解耦合最好的那一个往往就是最容易测试的那个
◆ 尝试1:一个幼稚的方案
其次,我们把迭代器从i改名为rit。i这个名字更常用在整型索引上。我们考虑过用it这个名字,这是迭代器的一个典型名字。但这一次我们用的是一个反向迭代器,并且这一点对于代码的正确性至关重要。
◆ 比较三种方案
这是一个正面的改进:有100行易读的代码比有50行不易读的要好。
如果你有我的微信,会发现我最近在参加极客时间的一个打卡活动——《8个月,攻克设计模式打卡召集令》,如果能够在2020年5月之前完成100天打卡,就可以领取100极币,除了这个奖品比较吸引我。更重要的是设计模式之美这个专栏的质量非常高,我在学习的过程中,再配合上工作中的具体实践,感觉收获很大。
目前我已经坚持了28天,下面这是我本周的打卡记录:
下面是我的推广海报
本号专注于后端技术、JVM问题排查和优化、Java面试题、个人成长和自我管理等主题,为读者提供一线开发者的工作和成长经验,期待你能在这里有所收获。