@Dmaxiya
2021-03-02T08:21:34.000000Z
字数 5824
阅读 731
博文
《Clean Code》是一本非常出名的介绍关于“如何写出整洁代码”的书,网上已经有许多相关的读书笔记,多是以一句话概括书中一段话的内容,这是一个“把书读薄”的过程,未来想再回顾一遍这本书时,只要看着自己之前的读书笔记,就可以回忆书上相关的内容——对已有知识的总结是巩固所学内容最好的方式。
之前的博客,我都更倾向于“让毫无背景知识的读者也能看懂”。“读书笔记”并不属于这一类,它最多属于“只有看过了原书的人才能看得懂的博客”,而这本书的内容似乎已经足够精炼:2~13 章中作者提出的每一点“准则”式的建议,都附带了一段这么做的理由(有时还会附上一些代码示例),篇幅从 1 到 8 段不等,它本身就是一本“笔记”,目录就是它的摘要——这本书精炼到我认为任何形式的总结都无法还原作者要表达的意思,所以你可能至少要读过一遍原书,才能来看看其他关于这本书的博客。
因此,如果是完全没有读过这本书的读者,不要寄希望于读别人的“读书笔记”来了解这本书的内容,更建议去把书读一读。如果未来想要“复习”一遍,翻开书浏览小标题,也能帮助回忆内容。其他博客通过梳理总结书中的内容来巩固知识,我更想通过从书中挑出一些有想法的点来“展开讲讲”。
这篇博客每个小标题的内容可大可小,只是我在读这本书的过程中,认为应该记一记、写一写、聊一聊的内容,所以取名为“随记”。本文记录的是自己的想法,不一定完全正确,也可能受限于我的知识背景,问题考虑得不够全面,欢迎大家一起讨论、交流。
You know you are working on clean code when each routine you read turns out to be pretty much what you expected.
如果每个例程都让你感到深合己意,那就是整洁代码。
个人感觉“深合己意”比原文表述更妙,听起来也更让人感到舒服。
我们只要用心设计过一段代码,当下都会认为这是一段“深合己意”、不能再写得更好的代码了——堪称完美,但再过几个月回头看看自己之前精心设计过的代码,又会发出“当初的代码为何会写得如此之烂”的感叹。
可见要写出当下能深合己意的代码并不困难,让几个月后的自己仍然能感到深合己意,要让给你做 code review 的同事感到深合己意,是一件非常困难的事情,这需要不断打磨原来的代码,争取做到当别人看到自己的代码时,能由衷地发出一句赞叹:“这代码写得真好!”
作者认为花时间精力来给每个变量、函数取一个好的名字,是十分有必要的,一个好的命名,能让阅读这段代码的人极大地减少理解上下文的成本;枚举值或者常量定义必须根据其具体的含义被安排在它应该存在的位置;基类中不应该含有派生类中的任何逻辑,不应该了解派生类的实现……诸如此类的做法,让代码出现在它应该出现的地方,使代码的阅读者像在读一段自然语言一样顺畅,对待代码也像在对待一篇文章一般,字斟句酌,使其语义通顺自然。
这一点在书中第 14~16 章的三个重构案例中体现得淋漓尽致,作者写出了他对重构过程中每一个改动的思考,因为代码的每一次改动,都需要有充分的理由,不然这一行代码就不需要修改。
重构过程中每一次小小的改动,目的都是要让代码更深合己意。
Leave the campground cleaner than you found it.
让营地比你来时更干净。
被广泛引用的一句话,可以称得上是“名人名言”了。
作者用一章的篇幅来强调命名的重要性,希望我们尽量符合书中提出的建议、避免书中不推荐的做法。每个变量、常量名,都值得反复推敲,因为在命名上花费的时间,比以后阅读这段代码的同事去理解代码含义的时间,要少得多。
在第 2 章中关于命名的建议,在 14~16 章的重构中有多处体现,再小的一个命名问题,作者都会用一段话来说明:
第一点作者在说明:要对代码进行修改(即使只是一个命名),必须要有充分的理由,否则就不应该修改它。第二点作者在说明:即使是一个变量名,都值得反复推敲。
最后在 2.16 节,作者给我们展示了一段精彩的有关命名的重构,如果给我同样的题目,结果可能与代码清单 2-1 相差无几,我甚至有好几个不去重构这段代码的理由:
util
包中逻辑非常简单的一段代码,一看就懂了在写完代码的当下,我们总是认为自己的代码逻辑非常清晰,一目了然,但当下对于我们自己的一目了然,从另一个人的角度来看,就如同我们第一次想要完全看懂代码清单 2-1 一样,我们花了多长时间?1~2 分钟?可能更长。直到我们看到代码清单 2-2,才会发现,自己那些不重构的理由都变得不够充分了。
看这样的代码,更令我们赏心悦目,一目十行。
一个人的视野、认知是有限的,而人与人之间的文化背景、知识面也是不同的。
我认为,尽管我们遵守了书中提到的每一点建议,将其像一套规章制度一样严格地执行,对每一个变量的命名都字斟句酌,想出自认为能完美表达每一个变量含义的命名,当其他人阅读同一段代码时,仍可能产生歧义,或者对给出的命名感到不满。
囿于本人极其有限的英语水平,在最近的一场 code review 中,关于命名问题的讨论大概占据了整个会议时长的 ,大致可总结为以下几个问题:
web_app
与 h5
,在此前很长一段时间,小程序在代码中的命名使用的还是 miniprogram
或者 mp
,最近一段时间内将“小程序”在代码中的命名改为 gadget
,而我当时还未了解到“网页应用”的命名已经统一为了 web_app
;apply
与 apply for
,apply
作为及物动词使用,是“应用”的意思,作为不及物动词,apply for
表示的才是“申请”的意思;common
库中并引用它,这可能会是比出现魔术数更好的一个解决方案;如果这些代码只是写给自己看,当然是毫无问题的,因为“我理解我自己”,这些命名已经达到了“深合己意”的标准。而我们是在一个团队中工作,我们理应考虑到其他同学对同一个命名的标准与理解。
当有同学对代码的命名有疑惑时,我们可以一起讨论引起疑惑的原因,共同得出消除这种疑惑的更好的命名;有同学给出更好的命名建议时,我们应该考虑建议的合理性,对合理的建议予以采纳;当团队共同制定出一套命名规范时,我们应尽可能严格地遵守,因为这是团队达成共识的结果,不因个人喜好而随意改变,这套规范同时反应了团队对代码质量的要求变高了,我们也应该为写出更高质量的代码做出努力。
所以我认为,命名(写出好代码)不是一个人的事,良好的自我感觉会阻碍我们写出更高质量的代码,团队成员提出的建议能促使我们进步,这大概也是 code review 的意义与价值所在,就看我们是否足够重视它,利用好它。
不看文章内容,只对这个小标题望文生义的话,实在是太容易引起误解了。
一个函数只做一件事,并不是要一个函数中只写一个 if
语句、一个 for
循环或者一次函数调用,而是要保证同一个函数中的每一个语句都要在同一抽象层级上,当一个函数中出现了不止一个抽象层级时,说明是时候再提取出一个函数了。
If one function calls another, they should be vertically close, and the caller should be above the callee, if at all possible.
若某个函数调用了另外一个,就应该把它们放到一起,而且调用者应该尽可能放在被调用者上面。
由于上学时写得最多的是面向过程的 C++
代码,而 C++
对被调用的函数必须要在调用之前声明,为减少“声明”与“定义”时对函数名的重复书写,我养成了每当需要添加一个函数时,都将该函数直接定义在当前函数的上方,相信养成这种习惯的同学不止我一个,打开 kuangbin 的 ACM 模板,几乎都是这种结构的代码。
即使现在写的是 Go
语言,函数声明 / 定义的顺序已经不影响编译结果,我仍然保留着这一习惯,直到看到《clean code》中的这段话。我们写代码时的逻辑是自顶向下的,我们阅读他人代码的思维习惯也是如此,书中作者始终保持这样的编码顺序,我即使不在 IDE 中阅读这些代码,也仍然感觉代码和我的思路是同步的。因此,我看到这里就立即改变了函数定义的顺序,并一直遵守着这一准则。
作者将代码清单 5-6 来作为编码范例,我对其中 if
、while
后因只有一行代码就不加大括号的写法感到十分难受,不论这段代码在其他地方的格式处理(如行中空格、段间空行)有多么优美,都无法让我赞同这段代码可以作为编码范例来参考,感觉鲍勃大叔还是有点自恋的……
我的一位同学曾写出这样的代码,导致我们花了半个多小时的时间来 debug:
if (condition)
code line one;
code line two;
原因是他在 if
后只有一个语句时,不习惯加大括号,后来想在当前 if
条件下再增加一个语句,而此时他又忘了加上大括号,导致程序没有往预期的逻辑执行,这段代码通过了所有本地 case,而每次提交结果都是失败。经历过这次的惨痛教训后,我的那位同学开始每个 if
/ while
/ for
之后都加上一个大括号了。
log4j
本节作者向我们演示了如何通过测试 搜索 阅读 测试……的方式来快速学习一种新工具的使用,我们或许不需要完全掌握新工具如何使用的每一个细节,只需要了解能覆盖我们当前需求场景下最小范围的正确使用方法即可。
在学习使用 log4j
过程中产生的代码不是一次性的,当后续有新同事想要学习 log4j
的使用时,执行一遍单测他就能有一个大致的了解并快速上手;当 log4j
有新版本发布时,不需要了解它所有的特性,只需要通过单测来判断升级所带来的改动是否会影响我们的需求即可。
Classes should have one responsibility—one reason to change.
类只应有一个权责——只有一条修改的理由。
设计模式六大原则之一,这句话实在是太有名了,而我对这句话的思考与理解可能还不够充分,谨摘抄至此,没有更多的讨论。
第 14 章作者向我们展示了第一个重构实例,第一眼看到代码清单 14-2 时,我感觉这段代码似乎还有重构的空间,例如函数 parseSchemaElement
,这个函数中的 if-else
分支多达 6 个,相比于其他二三十个函数,这一个函数就将整段代码的圈复杂度从 2 提升到了 6,而且随着需要解析的语法越来越多,理论上这一个函数的圈复杂度可以无限增加。
在这里个人觉得可以用一个语法检查器列表,所有新增的类型都可以向该列表注册语法检查器,根据注册的先后顺序决定解析的优先级,这样每种语法解析及数据操作,都可以分别放在每一种类型自身的代码中,也不会造成不断增加的圈复杂度。
代码清单 15-1 列出的所有测试用例代码覆盖率达到了 ,已经是一个非常完善的单测了,但这些单测中似乎没有关于类似 aaa
与 aaaaa
之间对比的结果,实际上我也没有仔细分析代码清单 15-2,因此没有办法对这种情况可能得到什么样的输出做出猜测。这里提出这个例子只是想说:尽管单测覆盖率达到了 ,但仍可能没有完全覆盖所有实际场景。
codeforce 给出了一种比赛规则:通过某道题目 X 的选手,可以锁住自己的提交(后面无法再对第 X 题进行提交),查看其他通过这一题的选手的代码,若发现代码存在逻辑漏洞,可出 corner case 数据来 hack 掉别人的提交,hack 成功可获得加分,失败则扣分,比赛过程中 hack 成功的数据,在赛后将再次用于对所有已提交通过的代码进行测试,这一赛制可极大概率为出题者补全测试用例数据。感兴趣的同学可以前往 contests 一试。
If you go to http://www.jfree.org/jcommon/index.php, you will find the JCommon library.
如果你访问 http://www.jfree.org/jcommon/index.php,就能找到 JCommon 类库。
友情提示:网址已挂,SerialDate 的源代码清单在附录 B,未读过本书的同学可以对照代码清单 B-1 来看本章内容,我第一次看没找到源码,看得云里雾里,走马观花地看了一通。
作者花了一些心思将 date.addDays
改成 date.plusDays
,原因是 addDays
可能产生对原 date
进行修改的误解,而改成 plusDays
,就单纯只有“加”的意思了,第一次看到这里感觉并没有很明显,都是“加上”的意思,这时候感觉 C++
中的运算符重载是如此地方便,表意清晰,毕竟有谁会用错 +=
与 +
这两个符号呢?
后来再次读这段话时,发现原来是自己英语的语感不够,date.addDays
有“加上”和“加上了”的意思,而 date.plusDays
就只有“加”的意思了。(瞧这贫瘠的英语水平)
最后放上一段书中出现的很有意思的话吧:
Fool me once, shame on you. Fool me twice, shame on me!
愚我一次,是你之耻。愚我两次,是我之耻!
鲍勃大叔是一个很可爱的人啊……