mirror of
https://github.com/cheetahlou/CategoryResourceRepost.git
synced 2025-11-16 22:23:45 +08:00
mod
This commit is contained in:
187
极客时间专栏/代码精进之路/第一模块:代码“规范”篇/01 | 从条件运算符说起,反思什么是好代码.md
Normal file
187
极客时间专栏/代码精进之路/第一模块:代码“规范”篇/01 | 从条件运算符说起,反思什么是好代码.md
Normal file
@@ -0,0 +1,187 @@
|
||||
<audio id="audio" title="01 | 从条件运算符说起,反思什么是好代码" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/59/dc/599d066d3eb9e0730aed1748c260f4dc.mp3"></audio>
|
||||
|
||||
写出优秀的代码是我们每一个程序员的毕生追求,毕竟写代码本身就是个技术活,代码的好坏,其实也就是我们工艺的好坏。作为一个技术类的工种,我们没有理由不去思考如何写出优秀、让人惊叹的代码。
|
||||
|
||||
那什么样的代码才是优秀的代码呢?对于这个问题,我想每个人心中都会有自己的答案。今天我就来和你聊聊我的思考。
|
||||
|
||||
对于条件运算符(?:)的使用,我估摸着你看到过相关的争论,或者自己写代码的时候也不知道到底该不该使用条件运算符,或者什么情况下使用?这些微不足道的小话题随时都可以挑起激烈的争论。
|
||||
|
||||
C语言之父丹尼斯·里奇就属于支持者。在《C程序设计语言》这本书里,他使用了大量简短、直观的条件运算符。
|
||||
|
||||
然而还有一些人,对条件运算符完全丧失了好感,甚至把“永远不要使用条件运算符”作为一条C语言高效编程的重要技巧。
|
||||
|
||||
比如说吧,下面的这个例子,第一段代码使用条件语句,第二段代码使用条件运算符。 你觉得哪一段代码更“优秀”呢?
|
||||
|
||||
```
|
||||
if (variable != null) {
|
||||
return variable.getSomething();
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
return variable != null ? variable.getSomething() : null;
|
||||
|
||||
```
|
||||
|
||||
同样使用条件运算符,你会喜欢下面代码吗?
|
||||
|
||||
```
|
||||
return x >= 90 ? "A" : x >= 80 ? "B" : x >= 70 ? "C" : x >= 60 ? "D" : "E";
|
||||
|
||||
```
|
||||
|
||||
十多年前,作为一名C语言程序员,我非常喜欢使用条件运算符。因为条件运算符的这种压缩方式,使代码看起来简短、整洁、干净。 而且,如果能把代码以最少的行数、最简短的方式表达出来,心里也颇有成就感。
|
||||
|
||||
后来,我的一位同事告诉我,对于我使用的条件运算符的部分代码,他要仔细分析才知道这一小行代码想要表达的逻辑,甚至有时候还要翻翻书、查查操作符的优先级和运算顺序,拿笔画一画逻辑关系,才能搞清楚这一小行代码有没有疏漏。
|
||||
|
||||
这么简单的代码,为什么还要确认运算符的优先级和运算顺序呢?因为只是“看起来”对的代码,其实特别容易出问题。所以,一定要反复查验、确认无误才能放心。
|
||||
|
||||
这么简单的代码,真的需要这么认真检查吗?超级简单的代码的错误,往往是我们最容易犯的一类编码错误。我个人就是犯过很多次这种低级、幼稚的错误,并且以后一定还会再犯。比如下面的这段有问题的代码,就是我最近犯的一个非常低级的代码错误:
|
||||
|
||||
```
|
||||
// Map for debug logging. Enable debug log if SSLLogger is on.
|
||||
private final Map<Integer, byte[]> logMap =
|
||||
SSLLogger.isOn ? null : new LinkedHashMap<>();
|
||||
|
||||
```
|
||||
|
||||
正确的代码应该是:
|
||||
|
||||
```
|
||||
// Map for debug logging. Enable debug log if SSLLogger is on.
|
||||
private final Map<Integer, byte[]> logMap =
|
||||
SSLLogger.isOn ? new LinkedHashMap<>() : null;
|
||||
|
||||
```
|
||||
|
||||
你可能会说,这个代码错误看起来太幼稚、太低级、太可笑了吧? 确实是这样的。这段错误的代码,我的眼睛不知道看过了它们多少次,可是这个小虫子(bug)还是华丽丽地逃脱了我的注意,进入了**JDK 11的最终发布版**。
|
||||
|
||||
如果使用条件语句,而不是条件运算符,这个幼稚错误发生的概率会急剧下降。 **坚持使用最直观的编码方式,而不是追求代码简短,真的可以避免很多不必要的错误**。所以说啊,选择适合的编码方式,强调代码的检查、评审、校验,真的怎么都不算过分。
|
||||
|
||||
现在,如果你要再问我喜欢哪种编码方式,毫无疑问,我喜欢使用条件语句,而不是条件运算符。因为,用条件语句这种编码方式,可以给我确定感,我也不需要挑战什么高难度动作;而看代码的人,也可以很确定,很轻松,不需要去查验什么模糊的东西。
|
||||
|
||||
这种阅读起来的确定性至少有三点好处,第一点是可以减少代码错误;第二点是可以节省我思考的时间;第三点是可以节省代码阅读者的时间。
|
||||
|
||||
**减少错误、节省时间,是我们现在选择编码方式的一个最基本的原则。**
|
||||
|
||||
《C程序设计语言》这本C程序员的圣经,初次发表于1978年。那个年代的代码,多数很简单直接。简短的代码,意味着节省昂贵的计算能力,是当时流行的编码偏好。而现在,计算能力不再是瓶颈,如何更高效率地开发复杂的软件,成了我们首先需要考虑的问题。
|
||||
|
||||
有一些新设计的编程语言,不再提供条件运算符。 比如,Kotlin语言的设计者认为,编写简短的代码绝对不是Kotlin的目标。所以,Kotlin不支持条件运算符。 Go语言的设计者认为,条件运算符的滥用,产生了许多难以置信的、难以理解的复杂表达式。所以,Go语言也不支持条件运算符。
|
||||
|
||||
我们看到,**现实环境的变化,影响着我们对于代码“好”与“坏”的判断标准。**
|
||||
|
||||
## “好”的代码与“坏”的代码
|
||||
|
||||
虽然对于“什么是优秀的代码“难以形成一致意见,但是这么多年的经验,让我对代码“好”与“坏”积累了一些自己的看法。
|
||||
|
||||
比如说,“好”的代码应该:
|
||||
|
||||
<li>
|
||||
容易理解;
|
||||
</li>
|
||||
<li>
|
||||
没有明显的安全问题;
|
||||
</li>
|
||||
<li>
|
||||
能够满足最关键的需求;
|
||||
</li>
|
||||
<li>
|
||||
有充分的注释;
|
||||
</li>
|
||||
<li>
|
||||
使用规范的命名;
|
||||
</li>
|
||||
<li>
|
||||
经过充分的测试。
|
||||
</li>
|
||||
|
||||
“坏”的代码包括:
|
||||
|
||||
<li>
|
||||
难以阅读的代码;
|
||||
</li>
|
||||
<li>
|
||||
浪费大量计算机资源的代码;
|
||||
</li>
|
||||
<li>
|
||||
代码风格混乱的代码;
|
||||
</li>
|
||||
<li>
|
||||
复杂的、不直观的代码;
|
||||
</li>
|
||||
<li>
|
||||
没有经过适当测试的代码。
|
||||
</li>
|
||||
|
||||
当然,上面的列表还可以很长很长,长到一篇文章都列不完、长到我们都记不住的程度。
|
||||
|
||||
## 优秀的代码是“经济”的代码
|
||||
|
||||
大概也没人想记住这么多条标准吧?所以,**关于优秀代码的特点,我想用“经济”这一个词语来表达**。这里的“经济”,指的是使用较少的人力、物力、财力、时间、空间,来获取较大的成果或收益 。或者简单地说,**投入少、收益大、投资回报高**。为了方便,你也可以先理解为节俭或者抠门儿的意思。
|
||||
|
||||
当然,使用一个词语表达肯定是以偏概全的。但是,比起一长串的准则,一个关键词的好处是,更容易让人记住。我想这点好处可以大致弥补以偏概全的损失。
|
||||
|
||||
该怎么理解“经济”呢?这需要我们把代码放到软件的整个生命周期里来考察。
|
||||
|
||||
关于软件生命周期,我想你应该很熟悉了,我们一起来复习一下。一般而言,一个典型的软件生命周期,大致可以划分计划、分析和设计、代码实现、测试、运营和维护这六个阶段。在软件维护阶段,可能会有新的需求出现、新的问题产生、旧问题的浮现,这些因素可能就又要推动新一轮的计划,分析、设计、实现、测试、运营。这样,这个周期就会反复迭代,反复的循环,像一个周而复始的流水线。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/14/04/144679d37d552e4d5c436cab88582f04.png" alt="">
|
||||
|
||||
当我们说投入少的时候,说的是这整个生命周期,甚至是这个周而复始的生命周期的投入少。 比如说,代码写得快,可是测试起来一大堆问题,就不是经济的。
|
||||
|
||||
现代的大型软件开发,一般都会有比较细致的分工,在各个阶段参与的人是不同的;甚至在相同的阶段,也会有多人参与。一个稍有规模的软件,可能需要数人参与设计和实现。而为了使测试相对独立,软件测试人员和软件实现人员也是相对独立的,而且他们具备不同的优势和技能。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/89/df/89bb4250efc6702f638f8981030927df.png" alt="">
|
||||
|
||||
所以,当我们考虑投入的时候,还要考虑这个生命周期里所有的参与人员。这些参与人员所处的立场、看问题的角度,所具有的资源禀赋,可能千差万别。比如说,如果客户需要阅读代码,才知道系统怎么使用,就不是经济的。
|
||||
|
||||
是不是所有的软件都有这六个阶段呢?显然不是的,我本科的毕业论文程序,就完全没有运营和维护阶段,甚至也不算有测试阶段。我当时的毕业论文是一个关于加快神经网络学习的数学算法。只要验证了这个算法收缩得比较快,程序的使命就完成了,程序就可以退出销毁了。 所以,运营和维护阶段,甚至测试阶段,对当时的我而言,都是不需要投入的阶段。
|
||||
|
||||
在现代商业社会里,尤其我们越来越倾向于敏捷开发、精益创业,提倡“快速地失败、廉价地失败”,很多软件走不到维护阶段就已经结束了。而且,由于人力资源的限制,当然包括资金的限制,一个程序员可能要承担很多种角色,甚至从开始有了想法,到软件实现结束,都是一个人在战斗,哪里分什么设计人员、测试人员。
|
||||
|
||||
对软件开发流程选择的差异,就带来了我们对代码质量理解,以及对代码质量重视程度的千差万别。 比如说,一个创业公司是万万不能照搬大型成熟软件的开发流程的。因为,全面的高质量、高可靠、高兼容性的软件可能并不是创业公司最核心的目标。如果过分纠缠于这些代码指标,创始人的时间、投资人的金钱可能都没有办法得到最有效的使用。
|
||||
|
||||
当然,越成熟的软件开发机制越容易写出优秀的代码。但是,**最适合当前现实环境的代码,才是最优秀的代码。**
|
||||
|
||||
所以,当我们考虑具体投入的时候,还要考虑我们所处的现实环境。 如果我们超出现实环境去讨论代码的质量,有时候会有失偏颇,丧失我们讨论代码质量的意义。
|
||||
|
||||
既然具体环境千差万别,那我们还有必要讨论什么是优秀的代码吗?优秀的代码还能有什么共同的规律吗? 即使一个人做所有的事情,即使代码用完一次就废弃,我们长期积累下来的编写优秀代码的经验,依然可以帮助到很多人。
|
||||
|
||||
比如说,虽然创业公司的软件刚开始最核心的追求不是全面的高可靠性。可是,你也要明白,创业的目的不是为了失败,一旦创业公司稳住了阵脚,这个时候如果它们没有高可靠性的软件作为支撑,很快就会有反噬作用。 而程序员背锅,就是反噬的其中一个后果。
|
||||
|
||||
如何使用最少的时间、最少的资源,提供最可靠的软件,什么时候开始把可靠性提高到不可忽视的程度,有没有可能一开始就是高可靠的, 这些就都是一个富有经验的创业公司技术负责人不得不考虑的问题。而我们总结出来的编写代码的经验,毫无疑问,可以为这些问题提供一些思路和出路。
|
||||
|
||||
为什么我们要从“经济”这个角度来衡量优秀的代码呢? 因为这是一个可以让我们更加理性的概念。
|
||||
|
||||
一个营利性的公司,必须考虑投入产出比,没有人愿意做亏本的买卖,股东追求的是利润最大化。作为程序员,我们也必须考虑投入和产出。 首先,我们的产出必须大幅度大于公司对我们的投入,否则就有随时被扫地出门的风险。然后,我们必须使用好我们的时间,在单位时间内创造更多的价值,否则,真的是没有功劳,只有徒劳。
|
||||
|
||||
编写代码的时候,如果遇到困惑或者两难,你要想一想,怎么做才能做到投资少、收益大?
|
||||
|
||||
即便具体环境千差万别,我还是有一些例子,可以和你一起分享:
|
||||
|
||||
<li>
|
||||
代码写得又快又好,是“经济”的;代码写得快,但是错误多,不是一个“经济”的行为。
|
||||
</li>
|
||||
<li>
|
||||
代码跑得又快又好,是“经济”的;代码跑得快,但是安全问题突出,不是一个“经济”的行为。
|
||||
</li>
|
||||
<li>
|
||||
代码写得精简易懂,是“经济”的;代码写得精简,但是没人看得懂,不是一个“经济”的行为。
|
||||
</li>
|
||||
|
||||
## 总结
|
||||
|
||||
对于所有的程序员来说,每个人都会遇到两个有名的捣蛋鬼,一个捣蛋鬼是“合作”,另一个捣蛋鬼是“错误”。
|
||||
|
||||
要合作,就需要用大部分人都舒服的方式。程序员间合作交流最重要的语言便是代码,换句话说,这就需要我们规范地编写代码,使用大家都接受的风格。不规范的代码,我们可能节省了眼前的时间,但是测试、运营、维护阶段,就需要更多的时间。而一旦问题出现,这些代码会重新返工,又回到我们手里,需要阅读、修改,再一次浪费我们自己的时间。对于这些代码,每一点时间的付出,都意味着投入,意味着浪费,意味着我们损失了做更有意义事情的机会。
|
||||
|
||||
人人都会犯错误,代码都会有bug,可是有些错误的破坏力是我们无法承受的,其中,最典型的就是安全问题。很多安全问题对公司和个人造成不容忽视的恶劣影响。我见过因为安全问题破产的公司。这时候,甚至都不要谈什么投入产出比、经济效益了,所有的投入归零,公司破产,员工解散。这需要我们分外地卖力,拿出十二分的精神来处理潜在的安全威胁,编写安全的代码。
|
||||
|
||||
如果我们把规范和安全作为独立的话题,优秀的代码需要具备三个特征: 经济、规范、安全。这些内容就是我们接下来要在专栏里一起学习的主体。
|
||||
|
||||
好了,今天我们一口气聊了很多,主要是在探讨到底什么样的代码才是优秀的代码。这个问题你之前考虑过吗?和我今天讲的是否一样呢?欢迎你在留言区写写自己的想法,我们可以进一步讨论。也欢迎你把今天的文章分享给跟你协作的同学,看看你们之间的理解是否一致。
|
||||
|
||||
|
||||
199
极客时间专栏/代码精进之路/第一模块:代码“规范”篇/02 | 把错误关在笼子里的五道关卡.md
Normal file
199
极客时间专栏/代码精进之路/第一模块:代码“规范”篇/02 | 把错误关在笼子里的五道关卡.md
Normal file
@@ -0,0 +1,199 @@
|
||||
<audio id="audio" title="02 | 把错误关在笼子里的五道关卡" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/2f/e0/2fd3e92923e6f7a37cb1ad248658c9e0.mp3"></audio>
|
||||
|
||||
上一讲中,我们一起讨论了什么是优秀的代码。简而言之,优秀的代码是经济、规范、安全的代码。在平时的工作中,我们要朝着这个方向努力,时常站在团队、流程、个人能力的角度去思考优秀代码。
|
||||
|
||||
作为一名软件工程师,我们都想写出优秀的代码。可是,怎么才能编写出经济、规范、安全的代码呢?这是个大话题,相信你之前也有过思考。
|
||||
|
||||
## 无心的过失
|
||||
|
||||
开始之前,我先给你讲个曾经发生过的真实案例。2014年2月,苹果公司的iOS和OS X操作系统爆出严重的安全漏洞,聪明的黑客们可以利用这一漏洞,伪装成可信网站或者服务,来拦截用户数据。而造成这一漏洞的原因,也让业界专家大跌眼镜。
|
||||
|
||||
下面我用 C语言的伪代码来给你简单描述下当时的漏洞情况。
|
||||
|
||||
```
|
||||
if ((error = doSomething()) != 0)
|
||||
goto fail;
|
||||
goto fail;
|
||||
if ((error= doMore()) != 0)
|
||||
goto fail;
|
||||
fail:
|
||||
return error;
|
||||
|
||||
```
|
||||
|
||||
其实这段代码非常简单,它有两个判断语句,如果判断条件成立,那就执行“goto fail”语句,如果不成立,那就跳过判断语句继续执行。上面的“goto fail”语句,它的意思是略过它之后的所有语句,直接跳转到标有“fail”语句的地方,也就是第6行。
|
||||
|
||||
我们来分析下,第一个判断条件(第一行和第二行),如果error不等于零,那就跳转到fail语句,这逻辑上没什么问题。而第三行,没有任何附加条件,就能直接跳转到fail语句,也就是说,它下面的代码永远也无法执行,这里是不是有问题?是的,漏洞就是出在这里。
|
||||
|
||||
这一行多余的代码就是导致苹果操作系统那个安全漏洞的罪魁祸首。2014年2月21日,苹果发布了相关的安全补丁,你随便一搜“GoTo Fail漏洞”就能找到相关的细节,我这里不赘述了。
|
||||
|
||||
我们每天仰慕的苹果操作系统出现这样“低级”的错误,你是不是有点惊讶?这么一个“简单”的错误,引发了一个非常严重的安全漏洞,是不是也有点出乎意料?上面的错误,简单看,就是复制的时候多复制了一行,或者因为时间关系,或者因为粗心大意,苹果的工程师硬是没检查出来。这在我们平时的工作中,也经常出现。
|
||||
|
||||
这个具有重大杀伤力的bug是如此的“幼稚”,如此的“好玩”,如此的“萌萌哒”,以至于到现在,人们还可以买到印有“GoTo Fail”的T恤衫,更别提业界对于这个问题的兴趣了。有很多文章,专门研究这一个“低级”安全漏洞;甚至有人探讨这个“低级”错误对于计算机软件教育的积极影响。
|
||||
|
||||
所有的危机都不应该被浪费,这一次也不例外。这些年,我也一直在思考**为什么我们会犯如此“低级”的错误**?即使是在苹果这样的大公司。反过来再想,我们应该如何尽可能避免类似的错误呢?
|
||||
|
||||
## 人人都会犯错误
|
||||
|
||||
没有人是完美的,人人都会犯错误。这应该是一个共识。这里面既有技术层面的因素,也有人类的行为模式的因素,也有现实环境的影响。我们在此不讨论人类进化和心智模式这样的严肃研究成果。但是,有两三个有意思的话题,我想和你聊聊。
|
||||
|
||||
第一个比较普遍的观点是好的程序员不会写坏的代码,要不然,就是他还不足够优秀。我尊重这个观点背后代表的美好愿望,但是这个观点本身我很难认同。它一定程度上忽视了人类犯错误的复杂性,和影响因素的多样性。
|
||||
|
||||
我认为,即使一个非常优秀的程序员,他主观上非常认真,能力又非常强,但他也会犯非常“低级”、“幼稚”的错误。所以,你不能因为苹果那个程序员,犯了那个非常低级的错误,就一棒子把他“打死”,认为他不是一个好的程序员。
|
||||
|
||||
第二个更加普遍的观点是同样的错误不能犯第二次。作为一名程序员,我同样尊重这个观点背后代表的美好期望。但是,我想给这个观点加一点点限制。这个观点应该是我们对自身的期望和要求;对于他人,我们可以更宽容;**对于一个团队,我们首先要思考如何提供一种机制,****以减少此类错误的发生**。如果强制要求他人错不过三,现实中,我们虽然发泄了怨气,但是往往错失了工作机制提升的机会。
|
||||
|
||||
第三个深入人心的观点是一个人犯了错误并不可怕,怕的是不承认错误。同样的,我理解这个观点背后代表的美好诉求。这是一个深入人心的观点,具有深厚的群众基础,我万万不敢造次。在软件工程领域,我想,在犯错这件事情上,我们还是要再多一点对自己的谅解,以及对他人的宽容。错误并不可怕,你不必为此深深自责,更不应该责备他人。要不然,**一旦陷入自责和指责的漩涡,很多有建设意义的事情,我们可能没有意识去做;或者即使意识到了,也没法做,做不好**。
|
||||
|
||||
我这么说,你是不是开始有疑惑了:人人都会犯错误,还重复犯,还不能批评,这怎么能编写出优秀的代码呢?换句话说就是,**我们怎么样才会少犯错误呢?**
|
||||
|
||||
## 把错误关在笼子里
|
||||
|
||||
人人都会犯错误,苹果的工程师也不例外。所以,“GoTo Fail”的“幼稚”漏洞,实在是在情理之中。可是,这样的漏洞是如何逃脱重重“监管”,出现在最终的发布产品中,这多多少少让我有点出乎意料。
|
||||
|
||||
我们先来看看,这个错误是经过了怎样的“工序”,穿越了多少障碍,需要多少运气,最终才能被“发布”出来。
|
||||
|
||||
我把这样的工序总结为“五道关卡”。
|
||||
|
||||
**第一道关:程序员**
|
||||
|
||||
**提高程序员的修养,是一个永不过时的课题。从别人的失败和自己的失败中学习、积累、提高,是一个程序员成长的必修课**。我知道,这是你和我一直都在努力做的事情。
|
||||
|
||||
第三行的“GoTo Fail”,实在算得上“漏网之鱼”,才可以逃过哪怕最平凡的程序员的眼睛,堂而皇之地占据了宝贵的一行代码,并且狠狠地玩耍了一把。
|
||||
|
||||
现在我们可以再回过来看看那段错误代码,如果是你写,你会怎么写呢?从你的角度来看,又有哪些细节可以帮助你避免类似的错误呢?这两个问题,你可以先停下来1分钟,想一想。
|
||||
|
||||
在我看来,上面那段代码,起码有两个地方可以优化。如果那位程序员能够按照规范的方式写代码,那“GoTo Fail”的漏洞应该是很容易被发现。我们在遇到问题的时候,也应该尽量朝着规范以及可持续改进的角度去思考错误背后的原因,而非一味地自责。
|
||||
|
||||
首先,**他应该正确使用缩进**。你现在可以再看下我优化后的代码,是不是第三行的代码特别刺眼,是不是更容易被“逮住”?
|
||||
|
||||
```
|
||||
if ((error = doSomething()) != 0)
|
||||
goto fail;
|
||||
goto fail;
|
||||
if ((error= doMore()) != 0)
|
||||
goto fail;
|
||||
fail:
|
||||
return error;
|
||||
|
||||
```
|
||||
|
||||
其次,**他应该使用大括号**。使用大括号后,这个问题是不是就自动消失了?虽然,多余的这一行依然是多余的,但已经是没有多大危害的一行代码了。
|
||||
|
||||
```
|
||||
if ((error = doSomething()) != 0) {
|
||||
goto fail;
|
||||
goto fail;
|
||||
}
|
||||
if ((error= doMore()) != 0) {
|
||||
goto fail;
|
||||
}
|
||||
fail:
|
||||
return error;
|
||||
|
||||
```
|
||||
|
||||
从上面这个例子里,不知道你有没有体会到,好的代码风格带来的好处呢?工作中,像苹果公司的那位程序员一样的错误,你应该没少遇到吧?那现在,你是不是可以思考如何从代码风格的角度来避免类似的错误呢?
|
||||
|
||||
魔鬼藏于细节。很多时候, **优秀的代码源于我们对细节的热情和执着**。可能,你遇到的或者想到的问题,不是每一个都有完美的答案或者解决办法。但是,**如果你能够找到哪怕仅仅是一个小问题的一个小小的改进办法,都有可能会给你的代码质量带来巨大的提升和改变**。
|
||||
|
||||
当然,你可能还会说,我代码风格不错,但是那个问题就是没看到,这也是极有可能的事情。是这样,所以也就有了第二道工序:编译器。
|
||||
|
||||
**第二道关:编译器**
|
||||
|
||||
编译器在代码质量方面,作为机器,恪尽职守,它可以帮助我们清除很多错误。还是以上面的漏洞代码为例子, 这一次其实编译器的防守并没有做好,因为它毫无察觉地漏过了多余的“GoTo Fail”。
|
||||
|
||||
在Java语言里,对于无法访问的代码(第三行后的代码), Java编译器就会及时报告错误。而在2014年2月的GCC编译器里,并没有提供这样的功能。
|
||||
|
||||
至今,GCC社区对于无法访问代码的检查,还没有统一的意见 。然而,GCC社区并没有完全浪费这个“GoTo Fail”的问题 。为解决类似问题,从GCC 6开始,GCC社区为正确使用缩进提供了一个警告选项( -Wmisleading-indentation )。如果代码缩进格式没有正确使用,GCC就会提供编译时警告。现在,如果我们启用并且注意到了GCC编译器的警告,犯类似错误的机会应该会大幅度地降低了。
|
||||
|
||||
在这里,我要提醒你的是,对于编译器的警告,我们一定要非常警觉。能消除掉所有的警告,你就应该消除掉所有的警告。就算实在没有办法消除掉编译警告,那你也一定要搞清楚警告产生的原因,并确认编译警告不会产生任何后续问题。
|
||||
|
||||
**第三道关:回归测试 (Regression Testing)**
|
||||
|
||||
一般地,软件测试会尽可能地覆盖**关键逻辑和负面清单**,以确保关键功能能够正确执行,关键错误能够有效处理。一般情况下,无论是开发人员,还是测试人员,都要写很多测试代码,来测试软件是否达到预期的要求。
|
||||
|
||||
另外,这些测试代码还有一个关键用途就是做回归测试 。如果有代码变更,我们可以用回归测试来检查这样的代码变更有没有使代码变得更坏。
|
||||
|
||||
上述的“GoTo Fail”这样的代码变更,涉及到一个非常重要的负面检查点。遗憾的是,该检查点并没有包含在回归测试中;或者,在这个变更交付工程中,回归测试并没有被执行。
|
||||
|
||||
软件测试没有办法覆盖所有的使用场景。但是,我们千万要覆盖关键逻辑和负面清单。一个没有良好回归测试的软件,很难保证代码变更的质量;也会使得代码变更充满不确定性,从而大幅地提高代码维护的成本。
|
||||
|
||||
**第四道关:代码评审 (Code Review)**
|
||||
|
||||
**代码评审是一个有效的在软件研发过程中抵御人类缺陷的制度**。通过更多的眼睛检查软件代码,被忽视的错误更容易被逮住,更好的设计和实现更容易浮现出来。
|
||||
|
||||
那代码评审是怎么实现的呢?一般情况下,代码评审是通过阅读代码变更进行的。而代码变更一般通过某种形式的工具呈现出来。比如OpenJDK采用的[Webrev](http://openjdk.java.net/guide/webrevHelp.html) 。你可以访问[我的一个代码评审使用的代码变更页面](http://cr.openjdk.java.net/~xuelei/8171337/webrev.00/) ,感受下这种呈现方式。
|
||||
|
||||
回到上面那个“GoTo Fail”的代码变更,看起来什么样子呢?下面是其中的一个代码变更版本示例:
|
||||
|
||||
```
|
||||
if ((err = SSLHashSHA1.update(&hashCtx, &signedParams)) != 0)
|
||||
goto fail;
|
||||
+ goto fail;
|
||||
if ((err = SSLHashSHA1.final(&hashCtx, &hashOut)) != 0)
|
||||
goto fail;
|
||||
|
||||
```
|
||||
|
||||
添加的这行代码,还是相当刺眼的。多一些眼睛盯着这些代码,多一些形式展现这些变更,就会大幅度地降低问题藏匿的几率。
|
||||
|
||||
上述的“GoTo Fail”这样的代码变更,怎么就逃过代码评审者的眼睛呢?我想说的是,评审者也是人,我们不能期望评审者能发现所有的问题。
|
||||
|
||||
**第五道关:代码分析 (Code Analysis)**
|
||||
|
||||
静态代码分析(Static Code Analysis)是通过对源代码的检查来发现潜在问题的一种软件质量保障方式。有很多静态代码分析工具可以帮助你检查代码缺陷,比如说商业软件Coverity,以及开源软件FindBugs。你可以试试看,有哪些工具可以检测到这个“GoTo Fail”问题。
|
||||
|
||||
代码覆盖率(Code Coverage)是一个反映测试覆盖程度的指标。它不仅仅量化测试的指标,也是一个检测代码缺陷的好工具。如果你的代码覆盖率测试实现了行覆盖(Line Coverage),这个“GoTo Fail”问题也很难闯过这一关。
|
||||
|
||||
很显然,苹果的这一关也没有拦截住“GoTo Fail”。这样,“GoTo Fail”就像千里走单骑的关云长,闯过了五关(有些软件开发流程,也许会设置更多的关卡)。
|
||||
|
||||
## 代码制造的流水线
|
||||
|
||||
我们分析了这重重关卡,我特别想传递的一个想法就是,**编写优秀的代码,不能仅仅依靠一个人的战斗**。代码的优秀级别,依赖于每个关卡的优秀级别。高质量的代码,依赖于高质量的流水线。每道关卡都应该给程序员提供积极的反馈。这些反馈,在保障代码质量的同时,也能帮助程序员快速学习和成长。
|
||||
|
||||
可是,即使我们设置了重重关卡,“GoTo Fail”依然“过关斩将”,一行代码一路恣意玩耍。这里面有关卡虚设的因素,也有我们粗心大意的因素。我们怎么样才能打造更好的关卡,或者设置更好的笼子?尤其是,**身为程序员,如何守好第一关?**
|
||||
|
||||
欢迎你在留言区说说自己的思考。下一讲,我们再接着聊这个话题。
|
||||
|
||||
## 一起来动手
|
||||
|
||||
下面的这段代码,有很多疏漏的地方。你看看自己读代码能发现多少问题?上面我们讨论的流程能够发现多少问题。不妨把讨论区看作代码评审区,看看在讨论区都有什么不同的发现。
|
||||
|
||||
```
|
||||
package com.example;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import javax.net.ssl.SNIServerName;
|
||||
|
||||
class ServerNameSpec {
|
||||
final List serverNames;
|
||||
|
||||
ServerNameSpec(List serverNames) {
|
||||
this.serverNames = Collections.unmodifiableList(serverNames);
|
||||
}
|
||||
|
||||
public void addServerName(SNIServerName serverName) {
|
||||
serverNames.add(serverName);
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
if (serverNames == null || serverNames.isEmpty())
|
||||
return "<no server name indicator specified>";
|
||||
|
||||
StringBuilder builder = new StringBuilder(512);
|
||||
for (SNIServerName sn : serverNames) {
|
||||
builder.append(sn.toString());
|
||||
builder.append("\n");
|
||||
}
|
||||
|
||||
return builder.toString();
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
你也可以把这篇文章分享给你的朋友或者同事,一起来讨论一下这道小小的练习题。
|
||||
|
||||
|
||||
112
极客时间专栏/代码精进之路/第一模块:代码“规范”篇/03 | 优秀程序员的六个关键特质.md
Normal file
112
极客时间专栏/代码精进之路/第一模块:代码“规范”篇/03 | 优秀程序员的六个关键特质.md
Normal file
@@ -0,0 +1,112 @@
|
||||
<audio id="audio" title="03 | 优秀程序员的六个关键特质" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/d3/86/d33039b81e141192724d5281dc08c686.mp3"></audio>
|
||||
|
||||
上一讲我们从“五道关卡”的角度讨论了如何写出优秀代码这个话题。对我们每个程序员个体来说,其实第一道“程序员”关卡最重要。没有优秀的程序员,就没有优秀的代码。那么,什么样的程序员才是优秀的程序员呢?
|
||||
|
||||
如果去问一个程序员,得到的答案有可能是,“写的一手好代码,做过几个大项目,设计、编程样样不在话下”。如果去问程序员的同学,得到的答案有可能是,“刚毕业就去了大厂,中秋节发的月饼羡慕死个人” 。如果去问程序员的同事,得到的答案有可能是,“人挺好,干活挺快的” 。如果去问HR,得到的答案有可能是“省钱、出活”。
|
||||
|
||||
你看,这些答案都没什么毛病,各有各的道理。你的答案是怎样的呢?我自己工作中见过特别多优秀的程序员,从他们身上发现了不少共性的特质,我总结为以下六项,下面一一给你介绍。
|
||||
|
||||
## 掌握一门编程语言
|
||||
|
||||
第一点很重要,优秀的程序员需要写的一手好代码,再简单来说,最起码需要能够熟练操控一门编程语言。
|
||||
|
||||
我们都会使用筷子,吃饭的时候,我们不需要有意识地控制着筷子的力度、开合和角度,也能准确地使用它。这个使用筷子的效率,是我们小时候长期练习的结果。每个人拿筷子的方法可能会有些差异,但是都不影响我们现在精准地、高效地使用筷子。
|
||||
|
||||
编写程序也是这样。熟练了之后,很多语法、语句在我们编写程序的时候,会下意识地就流露出来。如果我们设计一个线程类,下面的代码里,我觉得至少class和extends这两个关键字的使用是不需要大脑有意识地参与的。
|
||||
|
||||
```
|
||||
public class MyThread extends Thread {
|
||||
...
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
如果把编程语言看成一套功法,比如降龙十八掌,这套功法练得越多,练得越纯熟,用起来越得心应手。武侠小说里,一套功法只有练了全套,才有最大的威力。对于编程语言,我们了解得越多,熟知的招式就越多,可选择的范围就越大,我们就有更多的活动空间和解决问题的办法。
|
||||
|
||||
编程语言,基本上是相通的。掌握了第一门编程语言后,第二门语言学起来就快很多,第三门语言学起来更快。现在我们几乎都是多语言使用者,但一定要先精通一门语言,达到像用筷子那样的熟练程度。
|
||||
|
||||
## 解决现实的问题
|
||||
|
||||
掌握了一门编程语言,然后呢?代码是要解决具体的问题的,我们需要通过编程语言把解决问题的办法和思路表达出来。
|
||||
|
||||
要解决具体的问题,仅仅做到熟练使用编程语言是远远不够的,我们还需要更多工具。如果做前端,需要理解HTML和浏览器;如果做后端,需要掌握数据库和操作系统;如果做云计算,需要掌握Kubernetes等等。就像学了分筋错骨手,还要学降龙十八掌;学了七十二路空明拳,还要学左右互搏。俗话说,艺多不压身,工具箱永远都不嫌满。
|
||||
|
||||
有了工具还不够,优秀的程序员还要深入理解问题,懂得问题的最核心价值。只有理解了问题,看到了解决问题的价值,我们才能够真正解决好问题,并且从中获得满满的成就感。**我们一定要记得,程序员的存在不是为了写代码,而是为了解决现实问题,实现现实价值**。
|
||||
|
||||
真实的作品,都带着我们对于现实问题的理解。而打磨一个这样的作品,需要缜密的逻辑、突破创新和贯彻执行。通过使用合适的工具,把简单的、一行一行的代码,耐心地粘合、打磨成优秀的作品。
|
||||
|
||||
**如果说花样的工具是外家功夫,思维能力和行为能力可以算是内功**。
|
||||
|
||||
优秀的程序员,是一个内外双修的程序员。如果一个程序员可以熟练使用工具,有清晰的解决问题思路,能明晰地传达产品价值,那么他编写代码就不存在什么巨大的困难了。
|
||||
|
||||
## 发现关键的问题
|
||||
|
||||
有了工具,遇到问题能解决掉,我们就可以做事情了。优秀的程序员还有一项好本领,就是发现关键的问题。**能够发现关键的问题,我觉得是一个好程序员和优秀程序员的分水岭**。
|
||||
|
||||
优秀的程序员,能够发现一门编程语言的缺陷,一个顺手工具的局限。所以,他知道该怎么选择最合适的工具,该怎么避免不必要的麻烦。
|
||||
|
||||
优秀的程序员,能够发现解决方案背后的妥协和风险。所以,他可以预设风险防范措施,设置软件的适用边界。
|
||||
|
||||
优秀的程序员,能够敏锐地观察到产品的关键问题,或者客户未被满足的需求。所以,他可以推动产品持续地进步和演化。
|
||||
|
||||
**能够发现关键的问题,意味着我们可以从一个被动的做事情的程序员,升级为一个主动找事情的程序员**。
|
||||
|
||||
能够发现关键的问题,往往需要我们对一个领域有很深入的研究和深厚的积累,并且对新鲜事物保持充分的好奇心和求知欲。
|
||||
|
||||
掌握一门编程语言,解决现实的问题,能发现关键的问题,做到这三点,你就已经是一名优秀的程序员了。如果说优秀程序员有一个评价标准的话,这三条一定是硬性指标,接下来再介绍三条软性指标。
|
||||
|
||||
## 沉静的前行者
|
||||
|
||||
首先,优秀的程序员,一定是懂得妥协,懂得选择,一步一步把事情沉静地朝前推动的人。
|
||||
|
||||
如果真的较起真来,每一行代码,就像孔乙己的茴香豆,都有不止四样的写法。可是,最终的程序,只能选择唯一的一种。优秀的程序员都有在不断平衡、不断妥协中推动事物前行的能力和修为。
|
||||
|
||||
如果一个人说要一个完美的代码、完美的算法,完美的程序、完美的产品,我立刻就会非常紧张。完美是不存在的,所以我们才追求完美。对完美的过分追求,可能是一个代价高昂,收获甚小的行为。很多时候,我们不需要完美的东西。如果我只是想看看泰山山顶的日出,你就不要问我是爬上去的还是乘索道上去的了。
|
||||
|
||||
对完美的理解,也是千差万别的。如果你的完美和我的完美发生碰撞,一定有一方需要妥协,我们才可以共同迈出下一步。
|
||||
|
||||
而且,完美也可能意味着不承认缺陷,不承认未知。这样,我们可能在心理上就不会对代码的未知风险做出充分的预判,留出足够的安全缓冲空间。
|
||||
|
||||
我们写的每一行代码,都可能存在问题。有时候,我发现别人的代码的问题;有时候,别人发现我的代码的问题。我们最后都会明白,要坦诚地面对别人的问题,也要坦然地面对自己的问题。在解决问题和帮助别人解决问题中,我们把一个产品变得越来越好,问题越来越少。
|
||||
|
||||
## 可以依赖的伙伴
|
||||
|
||||
其次,优秀的程序员是他人可以依赖的伙伴。
|
||||
|
||||
如果我们把软件开发看成一个循环的流水线,参与其中的每个人,都要接受来自上一级的输入内容,在当前环节和同事合作,创造面向下一级的输出内容。优秀的程序员,知道团队合作的重要性,是一个优秀的团队成员。他在团队中能够快速学习、成长,变得越来越优秀,也能够帮助其他团队成员变得越来越优秀。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/00/d6/009586150d3d895bb129ab2d0e8ee9d6.png" alt="">
|
||||
|
||||
优秀的程序员是一个领导型的人。他能够倾听,持续地获取他人的优秀想法,以及不同的意见。他能够表达,准确地传递自己的想法,恰当地陈述自己的意见。他是一个给予者,给别人尊重,给别人启发,给别人指导,给别人施展才华的空间。他是一个索取者,需要获得尊重,需要获得支持,需要持续学习,需要一个自主决策的空间。他能够应对压力,承担责任,积极主动,大部分时候保持克制和冷静,偶尔也会表达愤怒。他具有一定的影响力,以及良好的人际关系,能够和各种类型的人相处,能够引发反对意见,但是又不损害人际关系。他知道什么时候可以妥协,什么时候应该坚持。
|
||||
|
||||
上面的这些,通常称为“软技能”。**如果说,编程语言、花样工具、逻辑思维、解决问题这些“硬技能”可以决定我们的起点的话,影响力、人际关系这些“软技能”通常影响着我们可以到达的高度。**因为,无论我们是加入他人的团队,或者组建自己的团队,我们只有在团队中才能变得越来越出色,做的事情越来越重要。所以,我们需要成为优秀的团队成员,接受影响,也影响他人。
|
||||
|
||||
## 时间管理者
|
||||
|
||||
最后我想和你分享的一点是,优秀的程序员是高效的时间管理者。
|
||||
|
||||
时间总是我们最大的障碍,优秀的程序员更是如此。没完没了的会议,没完没了的讨论,没完没了的学习,没完没了的需求,没完没了的bug,时间拦住了我们的雄心壮志和大好宏图。
|
||||
|
||||
时间面前,人人平等,没有人一天的时间比别人多一秒。优秀的程序员会更好地管理时间,或者提高效率,或者用好时间。
|
||||
|
||||
你有没有听说过这样的故事? 一家工厂的发动机坏了,请了很多人都没有修好。无奈,请了一位工程师,他听了听声音,在发动机上画了一道线,说:“打开,把线圈拆了”。果然,发动机就修好了。不管这个小故事是真的也好,假的也好,类似的事情在软件公司时时刻刻都在发生。有经验的程序员三分钟就能发现的问题,外行可能需要折腾好几天。持续地提高我们的硬技能和软技能,可以让我们做事情更快更好。
|
||||
|
||||
坚持把时间用在对的地方,用在价值更大的地方。事情总是做不完的。一般的工程师,都有一种打破砂锅问到底的精气神,这是好事。可是,这顺便带来了一点点的副作用,很多人有一点点小小的强迫症,很多事情,喜欢自己动手整个清楚明白。可是,事情又特别多,很多事情根本就顾不上。怎么办呢?
|
||||
|
||||
**要做只有你才能做的事情**。是的,有很多事情,只有你可以做,只有你做得最快最好。其他的同事也是一样的,有很多事情,只有他们能做,只有他们做得最快最好。选择最合适的人做最合适的事,这不仅是领导的工作分配,也可以是我们自己的协商选择。
|
||||
|
||||
事情做不完,就需要面临选择。**要坚持做需要做的事情**。不需要的、不紧急的、价值不大的,我们可以暂时搁置起来。一个人,能做的事情是有限的,能把最重要的事情最好,就已经很了不起了。
|
||||
|
||||
学会选择,是我们进阶道路上的一个必修课。
|
||||
|
||||
## 总结
|
||||
|
||||
最后,总结一下,优秀的程序员是什么样的? 优秀的程序员可以熟练地使用必要的工具,发现和解决复杂的现实问题;优秀的程序员可以在一个团队里,高效沉静地把项目和团队一步一步地朝前推进。
|
||||
|
||||
现在,把我们今天讲的优秀程序员六大特质当作一套自测题,和自己对照一下,自己在哪些方面做得比较好?在哪些方面还需要继续精进呢?
|
||||
|
||||
欢迎你留言聊聊自己的经验,如果有什么问题或困惑,也可以提出来我们一起讨论。
|
||||
|
||||
如果今天的内容对你有帮助,也请你分享给身边的朋友,和他一起精进。
|
||||
|
||||
|
||||
173
极客时间专栏/代码精进之路/第一模块:代码“规范”篇/04 | 代码规范的价值:复盘苹果公司的GoToFail漏洞.md
Normal file
173
极客时间专栏/代码精进之路/第一模块:代码“规范”篇/04 | 代码规范的价值:复盘苹果公司的GoToFail漏洞.md
Normal file
@@ -0,0 +1,173 @@
|
||||
<audio id="audio" title="04 | 代码规范的价值:复盘苹果公司的GoToFail漏洞" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/bd/32/bd836398129aeea6d06b16b24a5cba32.mp3"></audio>
|
||||
|
||||
我们在上一讲中讨论了一个优秀的程序员都需要具备哪些良好的品质,第一点就是要熟练掌握一门编程语言。
|
||||
|
||||
作为每天都要和代码打交道的人,光是熟练掌握还不够。我们需要像文字写作者一样,对代码有一种“洁癖”,那就是强调代码的规范化。
|
||||
|
||||
## 什么是编码规范?
|
||||
|
||||
要回答为什么需要编码规范,我们首先要了解编码规范指的是什么。
|
||||
|
||||
编码规范指的是针对特定编程语言约定的一系列规则,通常包括文件组织、缩进、注释、声明、语句、空格、命名约定、编程实践、编程原则和最佳实践等。
|
||||
|
||||
一般而言,一份高质量的编码规范,是严格的、清晰的、简单的,也是权威的。但是我们有时候并不想从内心信服,更别提自觉遵守了。你可能想问,遵循这样的约定到底有什么用呢?
|
||||
|
||||
编码规范可以帮我们选择编码风格、确定编码方法,以便更好地进行编码实践。 简单地说,**一旦学会了编码规范,并且严格地遵守它们,可以让我们的工作更简单,更轻松,少犯错误**。
|
||||
|
||||
这个问题弄明白了,我们就能愉快地遵守这些约定,改进我们的编程方式了。
|
||||
|
||||
## 规范的代码,可以降低代码出错的几率
|
||||
|
||||
**复杂是代码质量的敌人**。 越复杂的代码,越容易出现问题,并且由于复杂性,我们很难发现这些隐藏的问题。
|
||||
|
||||
我们在前面已经讨论过苹果公司的安全漏洞(GoToFail漏洞),接下来再来看看这个bug的伪代码。这个代码很简单,就是两个if条件语句,如果判断没问题,就执行相关的操作。
|
||||
|
||||
```
|
||||
if ((error = doSomething()) != 0)
|
||||
goto fail;
|
||||
goto fail;
|
||||
if ((error= doMore()) != 0)
|
||||
goto fail;
|
||||
fail:
|
||||
return error;
|
||||
|
||||
```
|
||||
|
||||
这段代码没有正确地使用缩进和大括号,不是一段符合编码规范的源代码。 如果我们使用符合规范的编码方式,这个安全漏洞就自然消失了。你可以看到,下面的代码里,我给if语句中加了大括号,代码看起来一下子就简单很多了。
|
||||
|
||||
```
|
||||
if ((error = doSomething()) != 0) {
|
||||
goto fail;
|
||||
goto fail;
|
||||
}
|
||||
if ((error= doMore()) != 0) {
|
||||
goto fail;
|
||||
}
|
||||
fail:
|
||||
return error;
|
||||
|
||||
```
|
||||
|
||||
所以**在编码的时候,我们应该尽量使代码风格直观、逻辑简单、表述直接**。 如果遵守编码规范,我们就可以更清楚、直接地表述代码逻辑。
|
||||
|
||||
## 规范的代码,可以提高编码的效率
|
||||
|
||||
还记得我们在前面讨论过代码“出道”的重重关卡吗?这些关卡,构成了代码制造的流水线。优秀的代码,来源于优秀的流水线。
|
||||
|
||||
如果我们都遵守相同的编码规范,在每一道关卡上,会产生什么样的质变呢?
|
||||
|
||||
在程序员编写代码这道关,如果我们规范使用缩进、命名、写注释,可以节省我们大量的时间。比如,如果使用规范的命名,那么看到名字我们就能知道它是一个变量,还是一个常量;是一个方法,还是一个类。
|
||||
|
||||
在编译器这道关,我们可以避免额外的警告检查,从而节省时间。还记得我们前面讨论过的GCC关于正确使用缩进的编译警告吗? 如果有编译警告出现,我们一般都要非常慎重地检查核对该警告有没有潜在威胁。这对我们的精力和时间,其实是不必要的浪费。
|
||||
|
||||
还记得GCC由于老旧的编程风格的原因,不支持无法访问代码编译错误吗? 过度自由的编码风格,有时候甚至会阻碍编译器开发一些非常有用的特性,使得我们无心的过失行为越积累越不好解决。
|
||||
|
||||
在代码评审这道关,如果我们不遵守共同的编码规范,这多多少少会影响评审者阅读代码的效率。为什么呢?因为评审者和编码者往往有着不一样的审美偏好。一条评审意见,可能要花费评审者很长时间来确认、评论。 然后,源代码编写者需要分析评审意见,再回到流水线的第一关,更改代码、编译、测试,再次提交评审,等待评审结果。
|
||||
|
||||
审美偏好一般都难以协调,由此导致的重复工作让编码的效率变得更低了。
|
||||
|
||||
在代码分析这道关,编码规范也是可以执行检查分析的一个重要部分。类似于编译器,如果有警告出现,分析警告对我们的精力是一种不必要的浪费; 如果过度自由,同样会阻碍代码分析工具提供更丰富的特性。
|
||||
|
||||
只要警报拉响,不管处在哪一个关卡,源代码编写者都需要回到流水线的第一关,重新评估反馈、更改代码、编译代码、提交评审、等待评审结果等等。每一次的返工,都是对时间和精力的消耗。
|
||||
|
||||
**总结一下,在代码制造的每一道关卡,规范执行得越早,问题解决得越早,整个流水线的效率也就越高。**
|
||||
|
||||
前一段时间,阿里巴巴发表了《阿里巴巴Java开发手册》。我相信,或许很快,执行阿里巴巴Java编码规约检查的工具就会出现,并且成为流水线的一部分。 对于违反强制规约的,报以错误;对于违反推荐或者规约参考的,报以警告。这样,流水线才会自动促进程序员的学习和成长,修正不符合规范的编码。
|
||||
|
||||
## 规范的代码,降低软件维护成本
|
||||
|
||||
代码经过重重关卡,好不容易“出道”了,这样就结束了吗?
|
||||
|
||||
恰恰相反,“出道”之后,才是代码生命周期的真正开始。
|
||||
|
||||
如果是开源代码,它会面临更多眼光的挑剔。即使是封闭代码,也有可能接受各种各样的考验。"出道”的代码有它自己的旅程,有时候超越我们的控制和想象。在它的旅程中,会有新的程序员加入进来,观察它,分析它,改造它,甚至毁灭它。软件的维护,是这个旅程中最值得考虑的部分。
|
||||
|
||||
有统计数据表明,**在一个软件生命周期里,软件维护阶段花费了大约80%的成本**。这个成分,当然包括你我投入到软件维护阶段的时间和精力。
|
||||
|
||||
举例来说吧,让我们一起来看看,一个Java的代码问题,在OpenJDK社区会发生什么呢?
|
||||
|
||||
在Java的开发过程中,当需要代码变更时,我们需要考虑一个问题:使用这些代码的应用是否可以像以前一样工作?
|
||||
|
||||
一旦出现了问题,一般有两种可能:要么是Java的代码变更存在兼容性问题,要么存在应用使用Java规范不当的问题。这就需要确认问题的根源到底是什么。
|
||||
|
||||
由于OpenJDK是开源代码,应用程序的开发者往往需要调试、阅读源代码。阅读源代码这件事情,在一定程度上,类似于代码评审的部分工作。如果代码是规范的,那么他们的阅读心情就会好一些,效率也就更高。
|
||||
|
||||
如果发现了任何问题,可以提交问题报告。问题报告一般需要明确列出存在的具体问题。 对于问题报告,也会有专门的审阅者进行研究分析,这个问题描述是否清晰?它是不是一个真正的问题?由谁解决最合适?
|
||||
|
||||
很多情况下,报告的审阅者也需要阅读、调试源代码。良好的编码规范,可以帮助他们快速理解问题,并且找到最合适的处理人员。
|
||||
|
||||
如果确定了问题,开发人员或者维护人员会进一步评估、设计潜在的解决方案。如果原代码的作者不能提供任何帮助,比如已经离职,那么他们可以依靠的信息,就只有代码本身了。
|
||||
|
||||
你看,这个代码问题修改的过程重包含了很多角色:代码的编写者、代码的使用者、问题的审阅者以及问题的解决者, 这些角色一般不是同一个人。在修改代码时,不管我们是其中的哪一个角色,遵守规范的代码都能够节省我们的时间。
|
||||
|
||||
**很多软件代码,其生命的旅程超越了它的创造者,超越了团队的界限,超越了组织的界限,甚至会进入我们难以预想的领域**。即使像空格缩进这样的小问题,随着这段代码的扩散,以及接触到这段代码人数的增加,由它造成的效率问题也会对应的扩散、扩大。
|
||||
|
||||
而严格遵守共同的编码规范,提高代码的可读性,可以使参与其中的人更容易地理解代码,更快速地理解代码,更快速地解决问题。
|
||||
|
||||
## 编码规范越使用越高效
|
||||
|
||||
除了上面我们说道的好处,编码规范还有一个特点,就是越使用越高效。
|
||||
|
||||
比如我们小时候都背诵过乘法口诀,如果我问你,3乘3得几? 我相信,你立即就会告诉我,答案是9。 不管这时候你是在开车、还是在走路;是在吃饭,还是在玩游戏。
|
||||
|
||||
如果我问你,13乘以23,结果是多少? 除非你经过非常特殊的训练,你不会立即就有答案,甚至你走路的时候,不停下脚步,就算不出这个结果。
|
||||
|
||||
如果我问一个还没学过乘法的小孩子呢? 3乘3的算术,对于小孩子,也许是一个不小的难题。
|
||||
|
||||
对于背诵过乘法口诀的我们来说,3乘3的算术根本就不需要计算,我们的大脑可以快速地、毫不费力地、无意识地处理这样的问题。 这种系统是我们思维的快系统。 快系统勤快、省力,我们喜欢使用它。
|
||||
|
||||
而对于13乘以23的算术,我们的大脑需要耗费脑力,只有集中注意力,才能运算出来。这种系统是我们思维的慢系统。慢系统懒惰、费劲,我们不愿意使用它。
|
||||
|
||||
快系统和慢系统分工协作,快系统搞不定的事情,就需要慢系统接管。 快系统处理简单、固定的模式,而慢系统出面解决异常状况和复杂问题。
|
||||
|
||||
比如上面苹果公司安全漏洞的那个例子,如果我们像乘法表一样熟练使用编码规范,一旦遇到没有使用大括号的语句,我们立即就会非常警觉。 因为,不使用大括号的编码方式不符合我们习以为常的惯例,快系统立即就能判别出异常状况,然后交给慢系统做进一步的思考。 如果我们没有养成编码规范的习惯,我们的快系统就会无视这样的状况,错失挽救的机会。
|
||||
|
||||
所以,我们要尽早地使用编码规范,尽快地培养对代码风格的敏感度。 良好的习惯越早形成,我们的生活越轻松。
|
||||
|
||||
## 小结
|
||||
|
||||
对于编码规范这件事,我特别想和你分享盐野七生在《罗马人的故事》这套书里的一句话:“**一件东西,无论其实用性多强,终究比不上让人心情愉悦更为实用。**”
|
||||
|
||||
严格地遵守编码规范,可以使我们的工作更简单,更轻松,更愉快。 记住,**优秀的代码不光是给自己看的,也是给别人看的,而且首先是给别人看的**。
|
||||
|
||||
你有什么编码规范的故事和大家分享吗? 欢迎你在留言区写写自己的想法,我们可以进一步讨论。也欢迎你把今天的文章分享给跟你协作的同学,看看编码规范能不能让你们之间的合作更轻松愉快。
|
||||
|
||||
## 一起来动手
|
||||
|
||||
下面的这段代码,我们前面用过一次,我稍微做了点修改。我们这次重点来看编码的规范,有哪些地方你看着不顺眼,你会怎么改进?
|
||||
|
||||
```
|
||||
package com.example;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import javax.net.ssl.SNIServerName;
|
||||
|
||||
class ServerNameSpec {
|
||||
final List<SNIServerName> serverNames;
|
||||
|
||||
ServerNameSpec(List<SNIServerName> serverNames) {
|
||||
this.serverNames = Collections.<SNIServerName>unmodifiableList(serverNames);
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
if (serverNames == null || serverNames.isEmpty())
|
||||
return "<no server name indicator specified>";
|
||||
|
||||
StringBuilder builder = new StringBuilder(512);
|
||||
serverNames.stream().map((sn) -> {
|
||||
builder.append(sn.toString());
|
||||
return sn;
|
||||
}).forEachOrdered((_item) -> {
|
||||
builder.append("\n");
|
||||
});
|
||||
|
||||
return builder.toString();
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
你也可以把这篇文章分享给你的朋友或者同事,一起来讨论一下这道小小的练习题。
|
||||
|
||||
|
||||
224
极客时间专栏/代码精进之路/第一模块:代码“规范”篇/05 | 经验总结:如何给你的代码起好名字?.md
Normal file
224
极客时间专栏/代码精进之路/第一模块:代码“规范”篇/05 | 经验总结:如何给你的代码起好名字?.md
Normal file
@@ -0,0 +1,224 @@
|
||||
<audio id="audio" title="05 | 经验总结:如何给你的代码起好名字?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/02/38/023c6cf24a09291b116399de57677f38.mp3"></audio>
|
||||
|
||||
上一节我们讲了编码规范的重要性,而编码规范,从起一个好名字开始。但起名字,也没有我们想得那么简单。有个流传很广的戏言:“计算机科学只有两件难事,废弃缓存和取名字。”
|
||||
|
||||
之所以说是戏言,因为取名字这件事无论如何都不算是高深的学问;之所以广泛流传,因为取名字真的就是一件很难的事情,而且起名字是关乎代码质量的大事。
|
||||
|
||||
给小孩取过名字的人都知道,取个好名字有多难,又要合八字,又要算五行,还要避尊者讳。 写程序给代码取名字更难,每天都要想很多名字。给孩子取名字,父母喜欢就行,给代码取名字,还要别人也喜欢。
|
||||
|
||||
## 为什么需要一个好名字?
|
||||
|
||||
名字要准确地代表它背后的东西,并且还能让代码干净漂亮。不然,我们的思路就会受到干扰,影响我们的思考和心情。
|
||||
|
||||
比如说,对于答案只有是与非两个选择的时候,我们通常使用布尔类型(boolean)。所以,取名字的时候,我们通常需要一个表达疑问的前缀,比如是不是“is”。
|
||||
|
||||
```
|
||||
public boolean isEmpty(); // String.isEmpty()
|
||||
|
||||
```
|
||||
|
||||
但如果我们把这样的疑问前缀,使用到一个非布尔类型上,会有什么效果?
|
||||
|
||||
```
|
||||
public byte[] isEmpty();
|
||||
|
||||
```
|
||||
|
||||
你是不是觉得如鲠在喉,对于代码要干什么百思不得其解? 反正,我写这个例子的时候,感觉像是吃了五百只苍蝇!
|
||||
|
||||
名字就是沟通的方式,错误的命名很难让我们清楚地理解代码真实的意图。所以,混淆的命名很难让我们阅读和理解代码。
|
||||
|
||||
虽然编译器不关心命名的好坏,但是我们却可以从一个好名字中获得巨大的好处。
|
||||
|
||||
## 为什么需要命名规范?
|
||||
|
||||
虽然起一个好名字的重要性不言而喻,但命名规范的选择,以及执行程度,却是一个有争议的话题。有人喜欢这种规范,有人喜欢那种规范,有人干脆认为规范都太教条,真是众口难调。此外,即使已知且明确定义了命名规范,某些组织也无法始终如一地遵守它们,从而导致不一致和混淆。如果命名规范内部不一致,任意且难以记忆,这些挑战还会加剧。
|
||||
|
||||
所以使用一个好的命名规范是非常重要的,我们都能获得哪些好处呢?
|
||||
|
||||
<li>
|
||||
为标识符提供附加的信息,赋予标识符现实意义。帮助我们理顺编码的逻辑,减少阅读和理解代码的工作量;
|
||||
</li>
|
||||
<li>
|
||||
使代码审核变得更有效率,专注于更重要的问题,而不是争论语法和命名规范这类小细节,提高开发效率;
|
||||
</li>
|
||||
<li>
|
||||
提高代码的清晰度、可读性以及美观程度;
|
||||
</li>
|
||||
<li>
|
||||
避免不同产品之间的命名冲突。
|
||||
</li>
|
||||
|
||||
## 有哪些常见的命名方法?
|
||||
|
||||
尽管不同的编程环境、不同编程语言也需要沟通,但遗憾的是,到目前为止,还没有一种通用的命名方法。 在不同的场景下,程序员们有着不同的偏好。我们需要阅读很多代码,多了解一些命名方法,这样我们才能更好地理解不同风格的代码。
|
||||
|
||||
我来一一介绍下几种常见的命名方法。
|
||||
|
||||
**1.驼峰命名法(CamelCase)**
|
||||
|
||||
驼峰命名法指的是使用大小写混合的格式,单词之间不使用空格隔开或者连接字符连接的命名方式。它有两种格式:大驼峰命名法(UpperCamelCase)和小驼峰命名法(lowerCamelCase)。
|
||||
|
||||
大驼峰命名法的第一个单词以大写字母开始,其余的和小驼峰命名法相同。 比如:LastName, InputStream。
|
||||
|
||||
小驼峰命名法的第一个单词以小写字母开始,其他单词以大写字母开始,其余字母使用小写字母。 比如:firstName, toString。
|
||||
|
||||
有时候,一个名字可能有不只一种合理形式,比如缩略语(IPv6)或者异常的结构(iOS)。 为了减少这种不确定性,Google定义了以下的转换规则:
|
||||
|
||||
<li>
|
||||
从正常的表达形式开始,把短语转换成ASCII码,并且移除单引号。 例如,“Müller’s algorithm”转换为“Muellers algorithm”;
|
||||
</li>
|
||||
<li>
|
||||
如果上述结果含有其他标点符号,比如连字符,在该符号处,把这个结果切分成单词形式。 如果某个单词已经是驼峰形式,也相应地切分开来。 例如,“AdWords”切分成“ad words”,“non-current assets”切分成“non current assets”;
|
||||
</li>
|
||||
<li>
|
||||
将所有字母转换为小写字母,然后将每个单词的首字母大写,这样就得到了大驼峰式命名的形式; 如果第一个单词的首字母小写,就得到了小驼峰式命名的形式;
|
||||
</li>
|
||||
<li>
|
||||
将所有的单词连在一起,就是最后的标识符命名。
|
||||
</li>
|
||||
|
||||
下面的表格列出了不同例子的正确转换形式,和容易出错的转换形式 (出自“Google Java Style Guide”)。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/f2/1d/f28217dc672df8bc968eccb57ce19c1d.png" alt=""><br>
|
||||
**2.蛇形命名法(snake_case)**
|
||||
|
||||
在蛇形命名法中,单词之间通过下划线“_”连接,比如“out_of_range”。
|
||||
|
||||
**3.串式命名法(kebab-case)**
|
||||
|
||||
在蛇形命名法中,单词之间通过连字符“-”连接,比如“background-color”。
|
||||
|
||||
**4.匈牙利命名法**
|
||||
|
||||
在匈牙利命名法中,标识符由一个或者多个小写字母开始,这些字母用来标识标识符的类型或者用途。标识符的剩余部分,可以采取其他形式的命名法,比如大驼峰命名法。
|
||||
|
||||
如果起始的小字母用来表示标识符的数据类型,这种命名法也被称为系统匈牙利命名法。 比如:
|
||||
|
||||
<li>
|
||||
lAccountNum标识一个_长整数_(首字母“l”,long)。
|
||||
</li>
|
||||
<li>
|
||||
szName标识一个_零字符结束的字符串_(首字母“sz”,zero-terminated string)。
|
||||
</li>
|
||||
|
||||
如果起始的小字母用来表示标识符的实际用途,这种命名法也被称为应用匈牙利命名法。 比如:
|
||||
|
||||
<li>
|
||||
rwPosition标识一个_行_(首字母“rw”,row)。
|
||||
</li>
|
||||
<li>
|
||||
usName标识一个_非安全字符串_(首字母“us”, unsafe string)。
|
||||
</li>
|
||||
|
||||
由于在微软产品中的广泛使用,匈牙利命名法曾经是一种流行的命名形式。然而,由于这种命名会带来不必要的记忆负担和阅读障碍,导致命名规则的执行和名称的维护都很困难,微软已经抛弃了这种命名形式。
|
||||
|
||||
由于历史的原因,还有很多代码使用这种命名形式。阅读这些代码时,你可以选择性地忽略这些表示类型或者用途的字母前缀。
|
||||
|
||||
## Java命名规范
|
||||
|
||||
一段代码,是不是只能使用一种命名方法? 一般来说,一个编码规范会组合使用这些命名方法,每一种命名方法都被规定了适用的范围。 这样就形成了命名规范。
|
||||
|
||||
比如,Java的命名规范可以使用下表来表示。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/19/b1/19cd98970ceceaed8247a586ba3895b1.png" alt="">
|
||||
|
||||
需要注意的是,常量必须是真的不能改变的量,不打算改变或者能够改变的量都不能算作常量。
|
||||
|
||||
比如,下面的例子声明的是常量:
|
||||
|
||||
```
|
||||
static final short MAX_VALUE = 32767;
|
||||
static final Set<String> EMPTY_NAMES =
|
||||
Collections.unmodifiableSet(Collections.emptySet());
|
||||
|
||||
```
|
||||
|
||||
下面的例子声明的就不是常量,它们的值都可以改变:
|
||||
|
||||
```
|
||||
static short nonFinalShort = 32767;
|
||||
static final Set<String> mutableNames = Collections.emptySet();
|
||||
static final String[] names = { "Alice", "Bob", "Tom" };
|
||||
|
||||
```
|
||||
|
||||
需要注意的是,方法标识符使用动词或者动词短语,这是传统的方法命名。如果能够分隔开配置(set)和使用(get),使用名词的方法标识符。比如Builder模式的接口设计。这个接口设计和命名惯例,我们以后再讨论。
|
||||
|
||||
## 怎么取好名字?
|
||||
|
||||
了解了命名方法后,你是不是想知道怎么取好名字呢?一般来说,给代码取名字,需要遵守如下三条原则。
|
||||
|
||||
**1.要有准确的意义**
|
||||
|
||||
名字要能够准确、完整地表达出它代表的意义,可以见字知意,名副其实。
|
||||
|
||||
比如,表达式“a = b - c”的语法是没有什么问题,可是该表达式代表的实际含义并不清楚。相比而言,“grossIncome = grossRevene - costOfGoodsSold”就有很准确、清晰的现实意义。这样的命名更容易阅读和理解。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/07/43/077720a9965c6daf354a3bc2518e4843.png" alt="">
|
||||
|
||||
**2.严格遵守命名规范**
|
||||
|
||||
不同的编程环境,偏爱不同的命名规范,比如Java倾向于使用驼峰命名法,C语言倾向于使用蛇形命名法,CSS使用串式命名法。 尽管如此,如果定义了个性化的命名规范,请严格遵守自定义的命名规范,如果没有定义个性化的命名规范,我们就需要严格遵守业界普遍公认的命名规范。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/75/e3/7510f9e45dffa6664c410d0e72b534e3.png" alt=""><br>
|
||||
**3.可读性优先**
|
||||
|
||||
名字的可读性一定要优先考虑,一般需要注意以下几点。
|
||||
|
||||
<li>
|
||||
可读性强的名字优先于简短的名字,尽量使用完整的词汇。
|
||||
</li>
|
||||
<li>
|
||||
不要使用缩写、简写、缩略词,除非这些词语被广泛使用。
|
||||
</li>
|
||||
<li>
|
||||
不要使用太短的名字,比如一个字母,除非是广泛接受的特例(i/j/k/m/n表示临时使用的整数,c/d/e表示临时使用的字符)。
|
||||
</li>
|
||||
<li>
|
||||
避免含糊、混淆或者误导。
|
||||
</li>
|
||||
|
||||
另外,不要混合使用英文和汉语拼音。由于很多类库使用的是英文,如果使用汉语拼音命名,会造成事实上的拼音名字与英文名字的混用,所以也要尽量避免使用拼音命名。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/99/16/99fbf25bc5c0aedb88cefe76c67d7816.png" alt="">
|
||||
|
||||
## 小结
|
||||
|
||||
简言之,取名字要做到“信、达、雅”(准确、直观、优美)。“信”和“达”是基本要求,有才气的你可以有“雅”的追求。
|
||||
|
||||
取好名字是编写优秀代码最基础也是最重要的一项修炼。 你不妨试试上述的原则和规范,将它们用于新代码,或者整理老代码。 仅仅因为名字的优化,你就会立刻感受到代码质量的大幅度提升!
|
||||
|
||||
## 一起来动手
|
||||
|
||||
所以为了让你更好地实践,我找了一段Java代码。你来试试,这段代码中有哪些名字可以优化? 欢迎你把优化的代码发在评论里,我们亲自感受下如何优化代码名字。
|
||||
|
||||
```
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
class Solution {
|
||||
/**
|
||||
* Given an array of integers, return indices of the two numbers
|
||||
* such that they add up to a specific target.
|
||||
*/
|
||||
public int[] twoSum(int[] nums, int target) {
|
||||
Map<Integer, Integer> map = new HashMap<>();
|
||||
for (int i = 0; i < nums.length; i++) {
|
||||
int complement = target - nums[i];
|
||||
if (map.containsKey(complement)) {
|
||||
return new int[] { map.get(complement), i };
|
||||
}
|
||||
map.put(nums[i], i);
|
||||
}
|
||||
throw new IllegalArgumentException("No two sum solution");
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
备注:代码选自[https://leetcode.com/problems/two-sum/](https://leetcode.com/problems/two-sum/)
|
||||
|
||||
你也可以把这篇文章分享给你的朋友或者同事,一起来讨论一下这道小小的练习题。
|
||||
|
||||
|
||||
379
极客时间专栏/代码精进之路/第一模块:代码“规范”篇/06 | 代码整理的关键逻辑和最佳案例.md
Normal file
379
极客时间专栏/代码精进之路/第一模块:代码“规范”篇/06 | 代码整理的关键逻辑和最佳案例.md
Normal file
@@ -0,0 +1,379 @@
|
||||
<audio id="audio" title="06 | 代码整理的关键逻辑和最佳案例" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/b2/c3/b2bdfa6199b50cfd2f050f37fc3fd8c3.mp3"></audio>
|
||||
|
||||
写一篇文章,要层次清楚、段落分明,写代码也是这样。杂志排版,要布局合理,阅读舒适,代码的编排也要这样。
|
||||
|
||||
可是很多人想的是,代码能工作就行,不用讲究格式;时间紧,哪有时间整理代码。
|
||||
|
||||
可事实上,干净整洁的代码带给我们的,远远不止格式上的赏心悦目,它更可以减少错误,提高我们的编程速度与效率。因为代码的层次结构、格式部署,是我们对自己思维的整理,也是我们思考逻辑的展现。
|
||||
|
||||
很多人不愿意整理代码,觉得这项工作很麻烦。其实代码的整理并不复杂,也不需要什么高深的技巧。有效地使用空行、空格和缩进,简单的分割与对齐,就可以使得代码的视觉效果甚至质量发生巨大的变化,理顺编码的思路,提高编码的效率。这是一个付出少、收获大,值得养成的好习惯。
|
||||
|
||||
## 给代码分块
|
||||
|
||||
其实整理代码,就是给代码分块。我们的大脑接收信息是按照分块的模式进行的。一块就是一个可识别的信息,它被安放到短期记忆的“插槽”里。信息块的实际信息量是可变的,但是越简单、越具体的信息块越容易被记住和理解。
|
||||
|
||||
一份好的报纸或者杂志,文章本身往往很简短,或者被分割为简短的块。使用小段落、子标题,大量留白,版面切割,尽一切可能分解文章、版面,让眼睛舒适,让大脑有时间停留下来接受每个信息快。
|
||||
|
||||
一个代码文件,可以看做是一个杂志版面。在这个版面里,我们要把代码整理成清晰的模块,这样每一小块在视觉上和逻辑上都是可以区分的,可以帮助我们阅读和记忆,减轻眼睛和大脑的疲劳。
|
||||
|
||||
比较下面的两种形式的语句。这两种形式,在实际逻辑上是完全相同的。但是,第二个语句使用了更细化的块,这样视觉形式和逻辑形式都是清晰直观的。
|
||||
|
||||
```
|
||||
if (firstName != null && lastName != null)
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
if ((firstName != null) && (lastName != null))
|
||||
|
||||
```
|
||||
|
||||
如果一段代码放眼望去都是大块大块的,那对于一个初学者来说,阅读和理解就非常困难。他需要将复杂的表达式再次分解,分解到可以单独理解的变量和运算符,再重新组合。
|
||||
|
||||
你看,这样是不是很麻烦?
|
||||
|
||||
一个好的程序员,要识别并且区分代码块,让它们易于阅读和辨认。程序代码分块时,我们需要注意以下三个点。
|
||||
|
||||
<li>
|
||||
**保持代码块的单一性,一个代码块只能有一个目标**。代码块内所有的内容都是为了一个目标服务的,不能把无关的内容放在同一个代码块里。同一个代码块里语句的相互联系比与相邻代码块里的语句关系更为紧密;
|
||||
</li>
|
||||
<li>
|
||||
**注意代码块的完整性**。代码块是一个完整的信息块。一个代码块要表达一个相对完整的意思,不能一个意思没说完就分块了,就像话说了半句一样;
|
||||
</li>
|
||||
<li>
|
||||
**代码块数量要适当**。代码块过多,会让人觉得路径太长,逻辑复杂,不容易阅读理解。一个基础的代码块最好不要超过25行(通常显示屏小半个页面),否则就会有增加阅读理解的困难。
|
||||
</li>
|
||||
|
||||
我们读文章的时候,如果段落划分合理,那么读一次差不多就能清楚地记住了。日常阅读的新闻、时事、娱乐报道,和学术著作相比,通常会使用更短的段落。这就是有意缩短信息快,使报道更加适合快速阅读的需要。
|
||||
|
||||
同样的,我们划分代码块,就是要让人清楚地阅读和理解。如果一个代码块太长,说明我们还可以进一步细分它。
|
||||
|
||||
## 使用空白空间
|
||||
|
||||
给代码分块的办法之一,就是有效地使用空白空间。空白的空间虽然没有内容,但会给我们重要的信息提示。因此我们写代码的时候,一定要合理地运用空白。
|
||||
|
||||
为什么呢?
|
||||
|
||||
靠近的代码会形成一个视觉块,并且具有隐含的关联。分开的代码,意味着上下两段代码的关联没有那么紧密。这种视觉效果会给我们暗示,代码靠得越近,代码之间的关系越紧密。
|
||||
|
||||
因此,空白区域,不仅仅可以用来分割代码,还展示了代码之间的关系。
|
||||
|
||||
代码的分块,通过空格、缩进和空行来整理,或者分割。其中空格可以进行逻辑区隔,用于同一行代码内部的信息分块。缩进用于水平分割,用于表示代码块的级别。空行用于垂直分割,用于分开同级别的不同代码块。
|
||||
|
||||
下面这个简图,可以直观地表示这种代码块的布局方式。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/4d/b7/4d810b8acbdc3a5413f53a432dc08eb7.png" alt=""><br>
|
||||
或者,也可以参考下面的代码示例。 这段代码,也包含了使用空格区分同一行代码内部的信息块。
|
||||
|
||||
```
|
||||
package coding;
|
||||
|
||||
public class CodingFormat {
|
||||
public static void main(String[] args) {
|
||||
System.out.println(
|
||||
"German say hello with "
|
||||
+ Greeting.GERMAN.getGreeting());
|
||||
}
|
||||
|
||||
private static enum Greeting {
|
||||
ENGLISH ("English", "Hello"),
|
||||
SPANISH ("Spanish", "Hola"),
|
||||
GERMAN ("German", "Hallo"),
|
||||
MANDARIN ("Mandarin", "Ni Hao");
|
||||
|
||||
private final String language;
|
||||
private final String greeting;
|
||||
|
||||
private Greeting(String language, String greeting) {
|
||||
this.language = language;
|
||||
this.greeting = greeting;
|
||||
}
|
||||
|
||||
private String getGreeting() {
|
||||
return greeting;
|
||||
}
|
||||
|
||||
private String getLanguage() {
|
||||
return language;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
那么到底如何利用空白空间呢?可以分为下面四个方法,我来一一讲解一下。
|
||||
|
||||
**同级别代码块靠左对齐**
|
||||
|
||||
我们阅读的习惯顺序是从左到右,代码也如此。因此不同行,但同级别的代码要靠左对齐。
|
||||
|
||||
比如,上面的CodingFormat例子中,main()方法和Greeting枚举类都是CodingFormat的下一级内容,属于同一级别的两个块。 两个代码块的左侧要对齐。
|
||||
|
||||
上面的CodingFormat例子中的枚举常量、枚举类的变量、枚举类的方法,也是属于同一级别的内容。 对应地,左侧要对齐。
|
||||
|
||||
**同级别代码块空行分割**
|
||||
|
||||
我们阅读代码总是从上往下读,不同行的同级别的代码块之间,要使用空行分割。
|
||||
|
||||
当我们读到一个空行的时候,我们的大脑就会意识到这部分的信息结束了,可以停留下来接受这段信息。 另外,我们阅读代码的时候,碰到空白行,我们也可以暂停,往回看几行,或者重新回顾一下整个代码块,梳理逻辑、加深理解。
|
||||
|
||||
比如,上面的CodingFormat例子中,main()方法和Greeting枚举类之间的空白行,getGreeting()和getLanguage()方法之间的空行,都是用来分割不同的信息块的。greeting变量和Greeting构造方法之间的空白行,表示变量声明结束,下面是开始定义类的方法,同样起到分割信息块的作用。
|
||||
|
||||
**下一级代码块向右缩进**
|
||||
|
||||
我们上面讲了同级别的代码格式,那么不同级别的呢?
|
||||
|
||||
区分不同行的不同级别的代码,可以使用缩进。缩进的目的是为了让我们更直观地看到缩进线,从而意识到代码之间的关系。
|
||||
|
||||
缩进虽然是一个共识,但怎么缩进是一个富有争议的话题。不同的语言,不同的程序员,有着不同的偏好。
|
||||
|
||||
我刚开始工作时,编写代码必须使用制表符缩进,并且要把制表符设置为四个字符空间。那个时候,还没有什么IDE可以依赖,大家都使用vi编辑器编写代码。缺省的,vi制表符使用八个字符空间。所以,那个时候我们看代码的第一件事,就是看看vi配置有没有把制表符设置为四个字符空间。 要不然的话,代码就凌乱得找不到头绪。
|
||||
|
||||
现在,OpenJDK的代码里已经不允许存在制表符了,缩进统一使用四个空格。也有的缩进规范使用两个空格,或者八个空格。
|
||||
|
||||
上面的CodingFormat例子中,我们使用了四个空格作为一个缩进单元。下面,我们看看两个空格的缩进,以及八个空格的缩进效果。
|
||||
|
||||
两个空格的缩进:
|
||||
|
||||
```
|
||||
package coding;
|
||||
|
||||
public class CodingFormat {
|
||||
public static void main(String[] args) {
|
||||
System.out.println(
|
||||
"German say hello with " + Greeting.GERMAN.getGreeting());
|
||||
}
|
||||
|
||||
private static enum Greeting {
|
||||
ENGLISH ("English", "Hello"),
|
||||
SPANISH ("Spanish", "Hola"),
|
||||
GERMAN ("German", "Hallo"),
|
||||
MANDARIN ("Mandarin", "Ni Hao");
|
||||
|
||||
private final String language;
|
||||
private final String greeting;
|
||||
|
||||
private Greeting(String language, String greeting) {
|
||||
this.language = language;
|
||||
this.greeting = greeting;
|
||||
}
|
||||
|
||||
private String getGreeting() {
|
||||
return greeting;
|
||||
}
|
||||
|
||||
private String getLanguage() {
|
||||
return language;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
两个空格的缩进,可以为每一行语句释放出更多的空间,从而减少换行,方便我们使用较长标识符或者语句。
|
||||
|
||||
两个空格的缩进,视觉上,缩进线靠得太近,相对而言,更容易混淆代码的分块级别。这是两个空格缩进的一个小缺点。
|
||||
|
||||
我们再来看下八个空格的缩进:
|
||||
|
||||
```
|
||||
package coding;
|
||||
|
||||
public class CodingFormat {
|
||||
public static void main(String[] args) {
|
||||
System.out.println(
|
||||
"German say hello with "
|
||||
+ Greeting.GERMAN.getGreeting());
|
||||
}
|
||||
|
||||
private static enum Greeting {
|
||||
ENGLISH ("English", "Hello"),
|
||||
SPANISH ("Spanish", "Hola"),
|
||||
GERMAN ("German", "Hallo"),
|
||||
MANDARIN ("Mandarin", "Ni Hao");
|
||||
|
||||
private final String language;
|
||||
private final String greeting;
|
||||
|
||||
private Greeting(String language, String greeting) {
|
||||
this.language = language;
|
||||
this.greeting = greeting;
|
||||
}
|
||||
|
||||
private String getGreeting() {
|
||||
return greeting;
|
||||
}
|
||||
|
||||
private String getLanguage() {
|
||||
return language;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
八个空格的缩进,视觉上可以看到缩进线明显,缩进的级别也容易辨认。
|
||||
|
||||
但过多的缩进空格也有缺点。它很容易超过每行字数的限制,比如屏幕每行可容纳字符的限制。特别是当我们使用较长标识符或者语句的时候,如果每行字符空间较小(手机屏幕),就很容易导致自动换行,让代码变得难以阅读。
|
||||
|
||||
另外,如果我们使用八个空格作为一个缩进单元,为了代码的整洁性,我们往往会被迫使用最少的缩进嵌套,这也导致了额外的复杂性,可读性就降低了。
|
||||
|
||||
由于我们倾向于使用有准确意义的命名,标识符的长度往往是一个不能忽视的因素。现在的编码规范,四个空格的缩进最为常见,二个空格的缩进次之,八个空格的缩进使用的较少。
|
||||
|
||||
**同行内代码块空格区隔**
|
||||
|
||||
我们上面讲的都是不同行的代码该如何注意格式。位于同一行内的代码块,同样需要注意。我们可以使用空格区分开不同的逻辑单元。
|
||||
|
||||
比如,逗号分隔符后要使用空格,以区分开下一个信息:
|
||||
|
||||
```
|
||||
String firstName, lastName;
|
||||
|
||||
```
|
||||
|
||||
双目运算符或者多目运算符,运算符前后都要使用空格:
|
||||
|
||||
```
|
||||
firstName != null
|
||||
|
||||
(firstName != null) && (lastName != null)
|
||||
|
||||
```
|
||||
|
||||
## 一行一个行为
|
||||
|
||||
上面我们讨论了代码块的一些整理办法。 那对于每一行代码,有没有整理办法呢?
|
||||
|
||||
当然是有的。
|
||||
|
||||
一个重要的原则是,每一行代码仅仅表示一个行为。这样每一行的代码才是一个常规大小的、可以识别的基础信息块。
|
||||
|
||||
比如说,下面的这行代码就包含了两个行为,一个是判断行为,一个是执行行为。 两个行为放在一行,这样的代码不仅看起来有些乱,我们的大脑处理起来也有些懵。
|
||||
|
||||
```
|
||||
if (variable != null) variable.doSomething();
|
||||
|
||||
```
|
||||
|
||||
如果分隔开这两个行为,信息块的区隔会更明显,代码会更清晰:
|
||||
|
||||
```
|
||||
if (variable != null) {
|
||||
variable.doSomething();
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
一般一个完整的表达式可以看作是一个独立的行为。
|
||||
|
||||
编辑器的宽度,屏幕的宽度,都是有限制的。当一个完整的表达式比较长时,就需要换行。
|
||||
|
||||
**基本的换行原则**
|
||||
|
||||
我们前面讨论的代码分块的基本思想,同样适用于换行。基本的换行规范需要考虑以下三点。
|
||||
|
||||
1. 每行代码字符数的限制。一般情况下,每行代码不要超出80个字符( 80个字符是传统终端的宽度,比如vi编译器)。由于屏幕尺寸和代码阅读终端的变化,现在的很多规范,开始使用120个字符的限制。所以我们编码的时候,需要留意一下;
|
||||
1. 如果一行不足以容纳一个表达式,就需要换行;
|
||||
1. 一般的换行原则包括以下五点。
|
||||
|
||||
- 在逗号后换行。
|
||||
|
||||
```
|
||||
String variable = anObject.getSomething(longExpressionOne,
|
||||
longExpressionTwo, longExpressionThree);
|
||||
|
||||
```
|
||||
|
||||
- 在操作符前换行。
|
||||
|
||||
```
|
||||
String varibale = longStringOne + longStringTwo
|
||||
+ longStringThree;
|
||||
|
||||
```
|
||||
|
||||
- 高级别的换行优先。
|
||||
|
||||
```
|
||||
anObject.methodOne(parameterForMethodOne,
|
||||
anObject.methodTwo(parameterForMethodTwo));
|
||||
|
||||
/ conventional indentation
|
||||
int runningMiles = runningSpeedOne * runningTimeOne
|
||||
+ runningSpeedTwo * runningTimeTwo;
|
||||
|
||||
// confusing indentation
|
||||
int runningMiles = runningSpeedOne
|
||||
* runningTimeOne + runningSpeedTwo
|
||||
* runningTimeTwo;
|
||||
|
||||
```
|
||||
|
||||
- 新的换行与上一行同级别表达式的开头对齐。
|
||||
|
||||
```
|
||||
anObject.methodOne(parameterOne,
|
||||
parameterTwo,
|
||||
parameterTwo);
|
||||
|
||||
```
|
||||
|
||||
- 如果上述规则导致代码混乱或者代码太靠右,使用8个空格作为缩进(两个缩进单位)。
|
||||
|
||||
```
|
||||
anObject.methodOne(parameterForMethodOne,
|
||||
anObject.methodTwo(parameterOneForMethodTwo,
|
||||
parameterTwoForMethodTwo,
|
||||
parameterThreeForMethodTwo));
|
||||
|
||||
// bad indentation
|
||||
if ((conditionOne && conditionTwo)
|
||||
|| (conditionThree && conditionFour)) {
|
||||
doSomething();
|
||||
}
|
||||
|
||||
// a better indentation, using 8 spaces for the indentation
|
||||
if ((conditionOne && conditionTwo)
|
||||
|| (conditionThree && conditionFour)) {
|
||||
doSomething();
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## 小结
|
||||
|
||||
今天,我给你讲了整理代码的重要性以及一些方法。其实归根结底,整理代码有一个基本的思想,那就是把代码分割成大脑能够有效识别并记忆的信息块,通过合理地使用空行、空格和缩进,把这些信息块清晰地呈现出来。清晰的代码结构,可以帮助我们理顺编码的思路,提高编码的效率,减少编码的错误,提高代码的可读性,降低代码的维护成本。
|
||||
|
||||
总之,整理代码带给我们的好处,是很多的。
|
||||
|
||||
## 一起来动手
|
||||
|
||||
还记得我们上一节的练习题吗?上次我们改名字,这次我们来修改一下代码的编排。欢迎你把优化的代码公布在讨论区,也可以写下你的优化思路,我们一起来看看编排优化后的代码是不是更好阅读了呢?
|
||||
|
||||
```
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
class Solution {
|
||||
/**
|
||||
* Given an array of integers, return indices of the two numbers
|
||||
* such that they add up to a specific target.
|
||||
*/
|
||||
public int[] twoSum(int[] nums, int target) {
|
||||
Map<Integer, Integer> map = new HashMap<>();
|
||||
for (int i = 0; i < nums.length; i++) {
|
||||
int complement = target - nums[i];
|
||||
if (map.containsKey(complement)) {
|
||||
return new int[] { map.get(complement), i };
|
||||
}
|
||||
map.put(nums[i], i);
|
||||
}
|
||||
throw new IllegalArgumentException("No two sum solution");
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
欢迎你把这篇文章分享给你的朋友或者同事,一起来探讨吧!
|
||||
|
||||
|
||||
226
极客时间专栏/代码精进之路/第一模块:代码“规范”篇/07 | 写好注释,真的是小菜一碟吗?.md
Normal file
226
极客时间专栏/代码精进之路/第一模块:代码“规范”篇/07 | 写好注释,真的是小菜一碟吗?.md
Normal file
@@ -0,0 +1,226 @@
|
||||
<audio id="audio" title="07 | 写好注释,真的是小菜一碟吗?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/42/23/42889ad5d5bfbdf640c65b6497528b23.mp3"></audio>
|
||||
|
||||
上一讲中我们讲了如何整理代码,但有些时候,即便我们取好了名字,编排好格式,但代码还是让我们抓狂,不明出处,不好理解。这时候,就需要注释登场了。
|
||||
|
||||
顾名思义,注释就是对代码的解释。注释不需要运行,它是用来提高代码的可读性和可维护性的。不好的注释会使代码变得更糟糕,使人更抓狂。
|
||||
|
||||
理想虽丰满,现实很骨感。注释虽小,写好不易。那写注释有哪些注意事项?有没有什么技巧呢?今天我们就来聊聊写注释这个话题。
|
||||
|
||||
当然了,不同的语言,注释的语法差别很大。为方便起见,我们统一使用Java语言的注释语法,来解释说明写好注释的基础原则。
|
||||
|
||||
## 注释是无奈的妥协
|
||||
|
||||
那你是不是有这样一个问题,源代码一定需要解释吗?
|
||||
|
||||
其实在理想状况下,代码不需要注释。理想的代码,命名恰当,结构清晰,逻辑顺畅,含义显而易见。但正如一个作家无法预料他的读者能否清晰地理解他的文字一样,一个程序员也不能判断他的读者能否清晰地理解他写的代码。所以,写注释其实是下巧功夫。
|
||||
|
||||
可是,注释也是一个麻烦鬼,可能会给我们带来三个麻烦。
|
||||
|
||||
首先,**因为注释不需要运行,所以没有常规的办法来测试它**。 注释对不对?有没有随着代码变更?这些问题都是写注释需要注意的地方。**注释难以维护,这是使用注释带来的最大的麻烦**。
|
||||
|
||||
另一个麻烦是,**注释为我们提供了一个借口**。使用注释来解释代码,是注释的本意。但是,我们有时候会过度依赖解释,从而放弃了潜在的替代方案,比如更准确的命名,更清晰的结构,更顺畅的逻辑等等。 **注释,被我们用成万能的狗皮膏药,有时会让代码更糟糕**。
|
||||
|
||||
比如,下面的代码和注释,看起来没毛病,但读起来很吃力。
|
||||
|
||||
```
|
||||
String name1; // first name
|
||||
String name2; // last name
|
||||
|
||||
```
|
||||
|
||||
如果使用准确、有意义的命名,我们就可以去掉没有意义的注释了。
|
||||
|
||||
```
|
||||
String firstName;
|
||||
String lastName;
|
||||
|
||||
```
|
||||
|
||||
还有一个麻烦,就是**注释的滥用**。 由于注释部分不被执行,那么就可以被用来注释掉一些不需要的东西。比如,在正式的产品代码中,注释掉调试信息、代码块、俏皮话等等。
|
||||
|
||||
比如说,看到下面的注释,你是不是立即就转移注意力了? 我理解这个注释的初衷是幽默一下,但是众口难调,这样的注释有些人感觉到的不是幽默,而是散漫和业余。
|
||||
|
||||
```
|
||||
// 哈哈,有没有人姓好,叫“好名字”?
|
||||
String firstName;
|
||||
String lastName;
|
||||
|
||||
```
|
||||
|
||||
讲了这么多,总结一下,注释是代码的一部分,是需要阅读的内容,目的是让其他人能更好地理解我们的代码,写注释需要我们有“用户思维”。虽然也有过度依赖注释的情况,但是,对于大部分程序员来说,问题还是注释太少,而不是太多。
|
||||
|
||||
## 几种常见注释类型
|
||||
|
||||
接下来,我们就聊聊几种常见的注释类型。一个典型的源代码文件,一般包含不同类型的注释。不同类型的注释,有着不相同的要求,适用于不同的注释风格和原则。
|
||||
|
||||
**第一种类型,是记录源代码版权和授权的**,一般放在每一个源文件的开头,说明源代码的版权所有者,以及授权使用的许可方式,或者其他的公共信息。比如,如果是个人的代码,版权信息可以写成:
|
||||
|
||||
```
|
||||
/*
|
||||
* Copyright (c) 2018, FirstName LastName. All rights reserved.
|
||||
*/
|
||||
|
||||
```
|
||||
|
||||
一般来说,版权和授权信息是固定的。版权和授权信息是法律条款,除了年份,一个字都不能更改。对于每个源代码文件,我们记得复制粘贴在文件开头就行。**需要注意的是,如果文件有变更,记得更改版权信息的年份(比如上例中的2018)**。
|
||||
|
||||
**第二种类型,是用来生成用户文档的**,比如Java Doc。 这部分的作用,是用来生成独立的、不包含源代码的文档。 这些文档帮助使用者了解软件的功能和细节,主要面向的是该软件的**使用者**,而不是该软件的开发者。 比如Java的API规范的文档。
|
||||
|
||||
**第三种类型,是用来解释源代码的**。换句话说,就是帮助代码的阅读者理解代码。这是大家默认的注释类型,也是我们今天讨论的重点。
|
||||
|
||||
## 简化注释的风格
|
||||
|
||||
上面我们介绍了三种常见的注释类型,下面就针对这三种注释类型,再给你介绍**三种风格**的注释。
|
||||
|
||||
**针对第一种注释类型,也就是固定的版权和授权信息,使用一般的星号注释符(/**-**/)**。注释块的首行和尾行只使用星号注释符,中间行以缩进一个空格的星号开始,文字和星号之间使用一个空格。注释的每行长度限制,和代码块的每行长度限制保持一致。
|
||||
|
||||
比如:
|
||||
|
||||
```
|
||||
/*
|
||||
* Copyright (c) 2018, FirstName LastName. All rights reserved.
|
||||
*/
|
||||
|
||||
```
|
||||
|
||||
**针对第二种注释类型,即生成用户文档的注释,使用Javadoc要求的格式,文档注释符(/**-*/)**。 除了首行使用特殊的文档注释符(/**),其他的格式和第一种风格保持一致。
|
||||
|
||||
比如:
|
||||
|
||||
```
|
||||
/**
|
||||
* A {@code Readable} is a source of characters. Characters from
|
||||
* a {@code Readable} are made available to callers of the read
|
||||
* method via a {@link java.nio.CharBuffer CharBuffer}.
|
||||
*
|
||||
* @since 1.5
|
||||
*/
|
||||
public interface Readable {
|
||||
...
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
**针对第三种注释类型,也就是代码解释注释,只使用行注释符(//)**。 每行长度限制,和代码块的每行长度限制保持一致。
|
||||
|
||||
比如:
|
||||
|
||||
```
|
||||
// Verify that the buffer has sufficient remaining
|
||||
private static void verifyLength(
|
||||
ByteBuffer buffer, int requiredLength) {
|
||||
...
|
||||
}
|
||||
|
||||
String myString; // using end-to-line comment
|
||||
|
||||
// This is a multiple line comment. This is a multiple
|
||||
// line comment.
|
||||
if (!myString.isEmpty()) {
|
||||
...
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
写代码注释时,我一般只用这三种风格。它们各自有固定的使用范围,简单直观,规避不必要的代码错误。也不会让别人混淆注释的类型。
|
||||
|
||||
我不会使用如下的注释,因为这种注释风格可能和有效的代码混淆在一起。 注释越长,错误越容易隐藏起来。
|
||||
|
||||
```
|
||||
/*
|
||||
* This is a multiple line comment. This is a multiple
|
||||
* line comment.
|
||||
|
||||
if (programingLanguage.equals("Java")) {
|
||||
...
|
||||
} */
|
||||
|
||||
```
|
||||
|
||||
当然了,肯定有人会喜欢上述的风格,因为这种风格可以注释掉不用的代码。这一点,方便我们调试分段代码。我自己在调试的时候也喜欢使用这种注释方式,但是一旦调试结束,我就会清理掉这些注释。
|
||||
|
||||
从我自己的经验来看,养成这样的习惯很有帮助:**如果一段代码不再需要,我会清理掉代码,而不会保留这个注释掉的代码块。**不要在源代码里记录代码历史,那是代码版本管理系统该干的事情。
|
||||
|
||||
## 注释的三项原则
|
||||
|
||||
那么,用来解释源代码的注释有什么需要注意的地方吗?为了规避注释的种种麻烦,有没有什么原则我们必需要遵守呢?我总结了以下三点。
|
||||
|
||||
<li>
|
||||
**准确**,错误的注释比没有注释更糟糕。
|
||||
</li>
|
||||
<li>
|
||||
**必要**,多余的注释浪费阅读者的时间。
|
||||
</li>
|
||||
<li>
|
||||
**清晰**,混乱的注释会把代码搞得更乱。
|
||||
</li>
|
||||
|
||||
比如,当我们说编程语言时,一定不要省略“编程”这两个字。否则,就可能会被误解为大家日常说话用的语言。这就是准确性的要求。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/b3/64/b3ed2cdcf1fc5f1c381e6db887390664.png" alt="">
|
||||
|
||||
如果代码已经能够清晰、简单地表达自己的语义和逻辑,这时候重复代码语义的注释就是多余的注释。注释的维护是耗费时间和精力的,所以,不要保留多余的、不必要的注释。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/2a/74/2a407359a04137abad0bb2c4b45adc74.png" alt=""><br>
|
||||
如果注释和代码不能从视觉上清晰地分割,注释就会破坏代码的可读性。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/44/38/445897c1f024f87b510e3ec3c84d9b38.png" alt=""><br>
|
||||
另外,不要在代码里标注你想要做的工作和已经做过的工作。比如使用TODO,记录代码更改记录等等。这些信息会干扰代码的阅读者。
|
||||
|
||||
特别需要注意的是,我们可以使用临时的调试语句,但是,不要把代码的调试语句保留在提交的代码里。这些调试语句,既不容易维护,也不容易阅读。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/4b/06/4b9a766c98b2441f04711b7062cffa06.png" alt="">
|
||||
|
||||
## 注释用英文还是汉字呢?
|
||||
|
||||
你会注意到,上面的代码案例中,我基本使用的是英文注释,在这里我也建议你使用英文注释。
|
||||
|
||||
为什么呢?
|
||||
|
||||
因为使用中文注释,不是一个所有人都能接受的风格。一部分人,一部分公司,并不接受中文注释。特别是国际化的项目,比如说贡献给Apache的项目,就没有办法使用中文注释了。而且,如果是去面试,我也会尽最大的努力不使用中文注释,以免踩到坑。
|
||||
|
||||
除了接受度之外,汉字带来的真正困扰是,它会影响到编码风格的偏好。比如命名的问题,到底是应该使用拼音还是英文? 由于代码命名只能使用ASCII字符,注释里的拼音、英文、汉字混杂的问题该怎么处理?代码编辑时,字符的切换也是一个麻烦事。比如,空格使用汉字全角,编译器会报错,但是肉眼看不到,问题排查起来也很心累。
|
||||
|
||||
那么什么情况下使用汉字呢?
|
||||
|
||||
面对国内的需求文档的时候。因为很多项目的需求文档一般是汉字书写的。程序的编码,当然需要按照需求来。如果需求的引用还要翻译成英文,那就太麻烦了。
|
||||
|
||||
还有一种状况,就是团队的英文水平不太好。与其使用难以读懂的蹩脚英文,不如使用大家更熟悉的中文注释来的便捷。不过,我对这种状况的担心越来越少,现在大部分年轻软件工程师的英语水平是可以让人放心的。
|
||||
|
||||
试着对比下面的几种注释,你喜欢哪一种呢?
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/31/d2/31f138eb37d91b78c4e44c03e0b4c2d2.jpg" alt="">
|
||||
|
||||
上面的五种不同的风格,我个人比较喜欢第一种和第二种,第三种也可以接受。 但是我会尽量避免第四种和第五种风格。
|
||||
|
||||
总结一下,今天我们讨论了怎么写好注释这个话题,希望你能理解一个基本的原则:注释是用来提高代码的可读性和可维护性的。 不要让注释偏离了这个原则,破坏了代码的逻辑和可读性。你也可以实践一下我们讨论的“三项原则”和“三种风格”,看看能不能让你的代码变得更友好?
|
||||
|
||||
## 一起来动手
|
||||
|
||||
还记得我们上一节的练习题吗?前面,我们改了名字,改了编排。这一次,我们来修改注释。认真读一下这段代码,看看有需要增加或者修改注释的地方吗?欢迎你把优化的代码公布在讨论区,我们一起来感受修改后的代码是不是更好阅读,更好维护。
|
||||
|
||||
```
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
class Solution {
|
||||
/**
|
||||
* Given an array of integers, return indices of the two numbers
|
||||
* such that they add up to a specific target.
|
||||
*/
|
||||
public int[] twoSum(int[] nums, int target) {
|
||||
Map<Integer, Integer> map = new HashMap<>();
|
||||
for (int i = 0; i < nums.length; i++) {
|
||||
int complement = target - nums[i];
|
||||
if (map.containsKey(complement)) {
|
||||
return new int[] { map.get(complement), i };
|
||||
}
|
||||
map.put(nums[i], i);
|
||||
}
|
||||
throw new IllegalArgumentException("No two sum solution");
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
欢迎点击“请朋友读”,把这篇文章分享给你的朋友或者同事,一起来探讨吧!
|
||||
224
极客时间专栏/代码精进之路/第一模块:代码“规范”篇/08 | 写好声明的“八项纪律”.md
Normal file
224
极客时间专栏/代码精进之路/第一模块:代码“规范”篇/08 | 写好声明的“八项纪律”.md
Normal file
@@ -0,0 +1,224 @@
|
||||
<audio id="audio" title="08 | 写好声明的“八项纪律”" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/0c/5d/0c7707731d88791ecbbd2685513c265d.mp3"></audio>
|
||||
|
||||
我们在前面讨论了该怎么取一个好名字。在编程语言里,我们使用标识符来表示不同的逻辑和对象。声明就是用来定义这些标识符的。标识符声明的地方,就是取名字和第一次使用名字的地方。这一次,我们聊一聊该怎么声明一个标识符。
|
||||
|
||||
“声明”是我们和标识符初次见面的地方,第一印象就显得特别重要。如果我们忘记了,回头能够清晰地找到它,也很重要。如果我们印象模糊了,回头能够重新认识它,对于我们阅读程序也有很大的帮助。
|
||||
|
||||
一个标识符,不仅仅只是一个名字。 像人分男女、高矮胖瘦一样,标识符也可以有附加信息,用来增强人们对它的认识。
|
||||
|
||||
一个声明,一般至少包含两个部分,一个是标识符的名字,一个是标识符的类型。 比如:
|
||||
|
||||
```
|
||||
int size;
|
||||
|
||||
```
|
||||
|
||||
有的声明,还有修饰和限定部分,比如Java的访问控制修饰符(private,public等):
|
||||
|
||||
```
|
||||
private int size;
|
||||
|
||||
```
|
||||
|
||||
或者C语言的存储类别限定符(auto,extern等):
|
||||
|
||||
```
|
||||
auto int size;
|
||||
|
||||
```
|
||||
|
||||
写声明很简单,但写好声明也并非易事。我们以Java语言为例来讨论声明的编码风格。在Java语言里,声明可以用来定义类、方法、类变量、局部变量和常量。不同声明的语法有着巨大的差别,但是也有很多共通的地方,你可以把这些思路用在自己熟悉的语言上。
|
||||
|
||||
接下来,我会带你一起来学写好声明的“八项纪律”。
|
||||
|
||||
## 取一个好名字
|
||||
|
||||
既然是标识符,就涉及到取名字的问题。我们前面已经讨论过“怎么取好名字”这个话题了,你可以复习一下。
|
||||
|
||||
## 一行一个声明
|
||||
|
||||
我们在前面讨论过“一行一个行为”的代码整理规则。这一规则,同样适用于标识符的声明。不推荐在同一行里声明多个变量,即使这一行很短。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/42/7c/4228bf7f045d5962b6047035eea1247c.png" alt=""><br>
|
||||
这样可以很方便地变更和维护代码。 比如,下面的两个代码变更,第二种格式更容易发现大意的错误。
|
||||
|
||||
```
|
||||
- int size;
|
||||
+ int size, length;
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
int size;
|
||||
+ int length;
|
||||
|
||||
```
|
||||
|
||||
下面这个代码变更例子,也是同理。
|
||||
|
||||
```
|
||||
- int size, length;
|
||||
+ int size; // size of the list
|
||||
+ int length;
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
- int size;
|
||||
+ int size; // size of the list
|
||||
int length;
|
||||
|
||||
```
|
||||
|
||||
另外,不要在同一行声明不同类型的标识符。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/bb/fb/bb3ac2546108163f635308d9a61d5efb.png" alt=""><br>
|
||||
需要注意的是,表示数组的中括号“[]”是类型的一部分,而不是标识符的一部分。无论是Java语言,还是在C语言的代码中,我都建议把数组符号放在类型该在的地方。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/4e/1d/4e288c08c438de096b4fb82b7acdbf1d.png" alt="">
|
||||
|
||||
## 局部变量需要时再声明
|
||||
|
||||
标识符的声明应该和它的使用尽可能地靠近,特别是局部变量的标识符声明。这样在视觉上,标识符的定义和使用,可以方便我们阅读和记忆。
|
||||
|
||||
比如,在下面的例子中,GroceryStore的声明就太早了。如果代码再长一些,等我们读到代码的末尾时,前面声明的变量很可能都忘记了,如果还要返回来查看这个具体的声明,那就太麻烦了。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/f6/8a/f6cefda34bfb97302b6fa13fbb92508a.jpg" alt="">
|
||||
|
||||
## 类属性要集中声明
|
||||
|
||||
同样是为了阅读和记忆,类变量的声明则要集中。因为类变量无论是私密变量,还是公开变量,在类的方法实现中,随时都可以调用。我们需要把这些变量放在一起,以便于修改和查找。
|
||||
|
||||
在下面的例子中,变量的声明散乱在类的代码块里。如果我们想要新加一个方法,或者调整方法声明的顺序,代码马上就会变得混乱不堪。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/7e/d0/7ed641489967eee1fb1d865d04eefbd0.jpg" alt="">
|
||||
|
||||
## 声明时就初始化
|
||||
|
||||
除非变量的初始值依赖于更多的条件,或者涉及到一定的计算,否则,声明时就应该完成初始化。声明时初始化,可以防止初始化的遗漏或者不必要的代码重复。
|
||||
|
||||
在下面的例子中,你可以设想一下,如果变量_isNegotiated_是后来加入的属性,哪一种编码方式更容易变更、更容易维护?
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/4d/43/4dfa095cb5cf497c3475492e3afdf443.png" alt="">
|
||||
|
||||
## 尾随的花括号
|
||||
|
||||
一般来说,类声明和方法声明后,要使用花括号把实现的代码包括进来。花括号的使用语法很随意。我不建议代码中混杂地使用不同的格式。我只推荐一种方法:
|
||||
|
||||
<li>
|
||||
左括号不要单独成行,要紧随在语句尾部,以一个空格隔开;
|
||||
</li>
|
||||
<li>
|
||||
右括号单独一行。
|
||||
</li>
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/a4/68/a4d1ff61709c798cb4c13f430a82f868.png" alt="">
|
||||
|
||||
## 靠紧的小括号
|
||||
|
||||
小括号的使用语法也可以很随意。小括号一般用来识别一个标识符是不是方法标识符,所以建议小括号要紧靠着标识符,中间不要有空格。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ea/b9/ea7c2e84ea29dedb0495ffddf26ee9b9.png" alt="">
|
||||
|
||||
## 搜索优化的换行
|
||||
|
||||
搜索优化是我们编写代码时要考虑的一个因素。搜索优化既包括针对搜索引擎的优化(SEO),也包括针对编辑器(vi, Netbeans)以及系统工具(grep)的搜索优化。
|
||||
|
||||
常见的搜索模式有:
|
||||
|
||||
<li>
|
||||
“public class”
|
||||
</li>
|
||||
<li>
|
||||
“abstract class”
|
||||
</li>
|
||||
<li>
|
||||
“class TheClassName”
|
||||
</li>
|
||||
<li>
|
||||
“extends TheClassName”
|
||||
</li>
|
||||
<li>
|
||||
“implements TheInterfaceName”
|
||||
</li>
|
||||
<li>
|
||||
“theMethodName(”
|
||||
</li>
|
||||
|
||||
这些常用的搜索模式给了我们一个很好的启示:语义相关的词语,常见的搜索模式,要尽量放在同一行。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/2f/d9/2f84dcbcb1a3c1080417d0cda402cad9.png" alt="">
|
||||
|
||||
## 小结
|
||||
|
||||
我们讲完了写好声明的八项原则,虽然相对来说比较细致,但基本的大原则主要有两个:
|
||||
|
||||
<li>
|
||||
取好名字
|
||||
</li>
|
||||
<li>
|
||||
容易识别
|
||||
</li>
|
||||
|
||||
掌握住这两条大原则,你会慢慢积累更多的最佳实践案例和纪律细节,让自己的代码声明越来越好读、越来越好用。
|
||||
|
||||
## 一起来动手
|
||||
|
||||
所以为了让你更好地实践,我找了一段Java代码。你来试试看,怎么把这段代码改得更漂亮?欢迎你把优化的代码公布在讨论区,我们一起来看看这段代码都有哪些可以改进的地方。
|
||||
|
||||
```
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
class Solution {
|
||||
/**
|
||||
* Return a list of the words in words that match the given pattern.
|
||||
*
|
||||
* A word matches the pattern if there exists a permutation of
|
||||
* letters p so that after replacing every letter x in the pattern
|
||||
* with p(x), we get the desired word.
|
||||
*
|
||||
* Example:
|
||||
* Input: words = ["abc","deq","mee","aqq","dkd","ccc"],
|
||||
* pattern = "abb"
|
||||
* Output: ["mee","aqq"]
|
||||
*
|
||||
* Explanation: "mee" matches the pattern because there is
|
||||
* a permutation {a -> m, b -> e, ...}.
|
||||
*
|
||||
* "ccc" does not match the pattern because
|
||||
* {a -> c, b -> c, ...} is not a permutation, since a
|
||||
* and b map to the same letter.
|
||||
*/
|
||||
public List<String> findAndReplacePattern(String[] words, String pattern) {
|
||||
List<String> ans = new ArrayList();
|
||||
for (String word: words)
|
||||
if (match(word, pattern))
|
||||
ans.add(word);
|
||||
return ans;
|
||||
}
|
||||
|
||||
public boolean match(String word, String pattern) {
|
||||
Map<Character, Character> M = new HashMap();
|
||||
for (int i = 0; i < word.length(); ++i) {
|
||||
char w = word.charAt(i);
|
||||
char p = pattern.charAt(i);
|
||||
if (!M.containsKey(w)) M.put(w, p);
|
||||
if (M.get(w) != p) return false;
|
||||
}
|
||||
boolean[] seen = new boolean[26];
|
||||
for (char p: M.values()) {
|
||||
if (seen[p - 'a']) return false;
|
||||
seen[p - 'a'] = true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
备注:代码选自[https://leetcode.com/problems/find-and-replace-pattern/](https://leetcode.com/problems/find-and-replace-pattern/)
|
||||
|
||||
欢迎你把这篇文章分享给你的朋友或者同事,一起来探讨吧!
|
||||
197
极客时间专栏/代码精进之路/第一模块:代码“规范”篇/09 | 怎么用好Java注解?.md
Normal file
197
极客时间专栏/代码精进之路/第一模块:代码“规范”篇/09 | 怎么用好Java注解?.md
Normal file
@@ -0,0 +1,197 @@
|
||||
<audio id="audio" title="09 | 怎么用好Java注解?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/74/e4/74e952dc66da5f5405a38722331485e4.mp3"></audio>
|
||||
|
||||
如果你使用面向对象的概念和技术有一段时间了,不知道你会不会有这样的困惑: 面向对象技术带来的麻烦,一点都不比它带来的好处少!
|
||||
|
||||
比如说,我们辛辛苦苦继承了一个类,重写了它的方法。过几天,这个类居然修改了它的接口,而且没人通知我们。然后,我们写的子类还可以美滋滋地编译,运行,就是总出错误,怎么调试都没发现这个子类的实现有什么不妥。直到有人说,父类变了!这时候,我们就想找杯咖啡暖暖手,一个人静静。
|
||||
|
||||
面向对象技术确实有它值得傲娇的地方。但是,只有把类似上面的小麻烦解决掉,它的使用才更合理。 比如说,父类做了修改,能不能立即就通知我? 别等到问题出现了,我们还被蒙在鼓里。
|
||||
|
||||
Java注解就可以帮助我们。
|
||||
|
||||
## 什么是Java注解
|
||||
|
||||
Java注解是Java 1.5引入的一个工具,类似于给代码贴个标签,通过注解可以为代码添加标签信息。这些标签信息可以添加在字段、方法和类上。开发工具、部署工具或者运行类库,可以对这些标签信息进行特殊的处理,从而获得更丰富的功能。
|
||||
|
||||
经过十多年的发展,注解已经成了Java生态系统一个非常重要的技术。使用注解可以大幅度降低我们的开发强度,提高工作效率,减少潜在的错误。像Java类库一样,注解也有了越来越丰富的定义和规范,成了我们需要掌握的重要技术之一。
|
||||
|
||||
**我们这里只讨论编写规范的代码时,该怎么合理地使用注解,具体就是Override、Deprecated、SuppressWarnings这三个注解**。更详细的Java注解技术和规范,以及如何自定义注解,需要你参考相关的文档。
|
||||
|
||||
## 在声明继承关系中,Java注解该如何使用?
|
||||
|
||||
在代码编写中,继承和重写是面向对象编程的两个重要的机制。这两个机制,在给我们带来便利的同时,也顺便带来了一些麻烦,这就需要我们用到注解了。
|
||||
|
||||
**第一个麻烦是,识别子类的方法是不是重写方法**。比如下面的例子,在一般情况下,对代码阅读者来说,最直觉的感受就是,getFirstName()这个方法不是重写方法,父类Person没有定义这个方法。
|
||||
|
||||
```
|
||||
class Student extends Person {
|
||||
// snipped
|
||||
public String getFirstName() {
|
||||
// snipped
|
||||
}
|
||||
// snipped
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
通常如果一个方法是重写方法,一定要使用Override注解,清楚地标明这个方法是重写的方法。 使用Override 注解的另一个好处是,如果父类更改了方法,子类的编译就会出错。这样我们就能在第一时间获得通知,既可以及时地变更子类,也可以使父类的变更更加合理。
|
||||
|
||||
```
|
||||
class Student extends Person {
|
||||
// snipped
|
||||
@Override
|
||||
public String getFirstName() {
|
||||
// snipped
|
||||
}
|
||||
// snipped
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
为什么要识别重写方法呢?这是因为继承的第二个麻烦。
|
||||
|
||||
**第二个麻烦是,重写方法可以不遵守父类方法的规范**。面向对象编程的机制,理想的状况是,父类定义了方法和规范,子类严格地遵守父类的定义。 比如Person.getFirstName()要求返回值是一个人的名,不包括姓氏部分,而且不可以是空值。但是子类Student.getFirstName()的实现完全有可能没有严格遵守这样的规范,不管是有意的,或者是无意的。 比如,返回了姓氏,或者返回了包括姓氏的姓名,或者可以返回了空值。
|
||||
|
||||
```
|
||||
class Student extends Person {
|
||||
// snipped
|
||||
@Override
|
||||
public String getFirstName() {
|
||||
return null;
|
||||
}
|
||||
// snipped
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
编译器无法检查重写到底该怎么实现,保持重写方法的行为一致需要我们凭借经验、肉眼识别。一般来说,一个重写方法不应该改变父类定义的规范。如果的确需要改变,就要有充足的理由,以及面对潜在兼容问题的具体的解决办法。
|
||||
|
||||
比如上面的例子中,如果Person.getFirstName()不允许返回空值,应用程序可以很安心地使用返回值,而不需要检查空值。
|
||||
|
||||
```
|
||||
boolean isAlice(Person person) {
|
||||
return person.getFirstName().equals("Alice");
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
但是,有了可以返回空值的Studen.getFirstName()的重写,上面的代码就可能抛出NullPointerException。一段简单的、严格遵守规范的代码,就变得危机四伏。
|
||||
|
||||
既然需要肉眼的判断,第一步就是要识别出重写方法。 识别方法越简单越好。
|
||||
|
||||
所以,重写的方法,一定要加上Override注解。这个注解,既可以提醒代码的阅读者,也提醒代码的书写者,要谨慎对待该方法在父类定义的规范。
|
||||
|
||||
识别出重写方法后,第二步就要判断重写的方法和父类规范的定义有没有冲突和抵触。
|
||||
|
||||
虽然一般情况下,子类的重写方法不应该改变父类的规范。但是,编写代码处处充满了无奈和妥协。极少数情况下,除了变更方法的规范,我们可能别无选择。 一旦这种情况发生,一定要明确标明,并注明潜在的后果。
|
||||
|
||||
如果重写方法既没有改变父类规范,也没有其他情况需要重点说明,重写方法就不应该有规范描述部分的存在。这样,可以减少规范描述对于阅读者的误导。我们当然需要了解具体的规范,但是应该查找、阅读父类的规范描述。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/3a/70/3ac5f9860961e2e570a6dfe298290970.jpg" alt=""><br>
|
||||
继承和重写还有一些其他的麻烦,我们后面的章节接着再聊。
|
||||
|
||||
## 在废弃退役接口的情况下,如何使用注解?
|
||||
|
||||
一个软件,部署得越广泛,生命力越悠久,就越需要不断地改进、升级。而废弃不合理的设计,拥抱更新的思想,也是软件改进的一部分。
|
||||
|
||||
然而,软件接口的废弃,不是一件简单的事情。越是广泛使用的接口,它的废弃、退役越困难。
|
||||
|
||||
比如,下面的String构造方法,是1994年Java 1.0设计实现的方法。很快,人们发现了这个方法的局限性。在1997年发布的Java 1.1中,废弃了该构造方法以及其他相关的方法。到现在,已经废弃20多年了,但Java依然没有删除这些方法,因为String的使用太广泛了!
|
||||
|
||||
```
|
||||
@Deprecated(since="1.1")
|
||||
public String(byte ascii[], int hibyte) {
|
||||
this(ascii, hibyte, 0, ascii.length);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
无论对于软件的维护者,还是软件的使用者,废弃的接口都是不值得让我们继续耗费精力的。
|
||||
|
||||
如果软件的维护者继续在废弃的接口上投入精力,意味着这个接口随着时间的推移,它的实现可能会存在各种各样的问题,包括严重的安全问题,就连使用者也要承担这些风险。而且还会有用户持续把它们运用到新的应用中去,这就违背了废弃接口的初衷。更多的使用者加入危险的游戏,也增加了删除废弃接口的难度。
|
||||
|
||||
这就要求我们做好两件事情。
|
||||
|
||||
**第一件事情是,如果接口的设计存在不合理性,或者新方法取代了旧方法,我们应该尽早地废弃该接口**。
|
||||
|
||||
及时止损!
|
||||
|
||||
做好这件事情,需要我们使用Deprecated注解,并且用一切可以使用的办法,广而告之。对于代码而言,要在声明中使用Deprecated注解;在规范描述中,说明废弃的原因以及替代的办法;对于有计划要删除的接口,要注明计划删除的版本号。
|
||||
|
||||
下面是两个可以参照的Java代码废弃接口的例子:
|
||||
|
||||
```
|
||||
java/lang/String.java:
|
||||
|
||||
/**
|
||||
* Counts the number of stack frames in this thread. The thread must
|
||||
* be suspended.
|
||||
*
|
||||
* @return the number of stack frames in this thread.
|
||||
* @throws IllegalThreadStateException if this thread is not
|
||||
* suspended.
|
||||
* @deprecated The definition of this call depends on
|
||||
* {@link #suspend}, which is deprecated. Further,
|
||||
* the results of this call were never well-defined.
|
||||
* This method is subject to removal in a future
|
||||
* version of Java SE.
|
||||
* @see StackWalker
|
||||
*/
|
||||
@Deprecated(since="1.2", forRemoval=true)
|
||||
public native int countStackFrames();
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
java.security.Certificate.java:
|
||||
|
||||
/**
|
||||
* <p>This is an interface of abstract methods for managing a
|
||||
* variety of identity certificates.
|
||||
*
|
||||
* ... snipped ...
|
||||
*
|
||||
* @deprecated This class is deprecated and subject to removal
|
||||
* in a future version of Java SE. It has been replaced by
|
||||
* {@code java.security.cert.Certificate} and related classes.
|
||||
* @see java.security.cert.Certificate
|
||||
*/
|
||||
@Deprecated(since="1.2", forRemoval=true)
|
||||
public interface Certificate {
|
||||
// snipped
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
**第二件事情是,如果我们在现有的代码中使用了废弃的接口,要尽快转换、使用替换的方法**。等到废弃方法删除的时候,再去更改,就太晚了,**不要等到压力山大的时候才救火**。
|
||||
|
||||
如果一个接口被废弃,编译器会警告继续使用的代码。Java提供了一个不推荐使用的注解,SuppressWarnings。这个注解告诉编译器,忽略特定的警告。警告是非常有价值的信息,忽略警告永远不是一个最好的选项。
|
||||
|
||||
再次强调,除非万不得已,不要使用SuppressWarnings。如果万不得已来临,请参考下面的例子。
|
||||
|
||||
```
|
||||
@SuppressWarnings("deprecation")
|
||||
private boolean myMethodUseDeprecatedMethod() {
|
||||
// snipped
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
当然,这样的使用带来了一系列的后遗症。 由于,废弃的编译警告被无视,我们使用了废弃接口的事实就被淹没在代码的海洋里,再也进入不了我们的视野。不到废弃接口被删除的那一天,我们都意识不到我们的代码里使用了废弃的接口,我们的应用程序都要承担着废弃接口维护不足的种种风险,包括严重的安全风险。
|
||||
|
||||
后面我们还会谈到,不要轻易地更改现有的代码,即使这些代码很丑陋,散发着浓浓的腐臭味。但是,有一个例外,如果看到了使用SuppressWarnings的代码,我们要尽可能地想办法把相关的警告消除掉、把这个注解去掉,越快越好。
|
||||
|
||||
## 小结
|
||||
|
||||
Java注解的功能很丰富,了解注解可以使得我们编码的工作更轻松。 这一次,希望我们记住三个基本的实践:
|
||||
|
||||
1. 重写的方法,总是使用;
|
||||
1. 过时的接口,尽早废弃;
|
||||
1. 废弃的接口,不要使用。
|
||||
|
||||
## 一起来动手
|
||||
|
||||
Java的注解非常丰富,功能也很强大。借这个机会,我想让大家互相分享一下,你最经常使用的注解是什么?什么情况下使用这个注解?这个注解给你带来哪些便利?欢迎你把你的经验发布在评论区,我们一起来学习更多的注解,一起来进步。
|
||||
|
||||
也欢迎你把这篇文章分享给你的朋友或者同事,一起来探讨吧!
|
||||
|
||||
|
||||
280
极客时间专栏/代码精进之路/第一模块:代码“规范”篇/10 | 异常处理都有哪些陷阱?.md
Normal file
280
极客时间专栏/代码精进之路/第一模块:代码“规范”篇/10 | 异常处理都有哪些陷阱?.md
Normal file
@@ -0,0 +1,280 @@
|
||||
<audio id="audio" title="10 | 异常处理都有哪些陷阱?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/f4/f8/f4372f7fddfce6791660edce83d152f8.mp3"></audio>
|
||||
|
||||
上一讲中我们聊了聊怎么用好Java注解,今天我们谈谈怎么处理异常。
|
||||
|
||||
处理好异常状况是掌握一门编程语言的基础,也是我们编程离不开的基本功。相信你对异常处理的机制已经很熟悉了。异常处理便捷、灵活、好用。但是,越好用的东西,我们越容易忽视它的缺陷。异常处理就有很多我们容易忽视的陷阱。今天,我们来聊聊这些问题,以及该怎么处理这些问题。
|
||||
|
||||
## 异常就是非正常
|
||||
|
||||
异常状况,就是不正常的状况。
|
||||
|
||||
异常状况的处理会让代码的效率变低。一个流畅的业务,它实现代码的执行路径,理想的状况就是没有任何异常状况发生。否则,业务执行的效率就会大打折扣。既然这样,我们就不应该使用异常机制来处理正常的状况。
|
||||
|
||||
这一点不难理解。可是,由于一门语言无法理解什么样的状况是正常状况,什么样的状况又是异常状况,也就无法限制异常机制使用的具体场景。所以作为程序员,我们需要自己解决好这个问题,不要滥用了异常机制。
|
||||
|
||||
比如说,很多API的设计有检查参数有效性的方法。如果参数通过检验,就没有异常抛出,否则就会抛出异常。在使用这个方法的代码时,我们需要检查有没有抛出异常来确认参数是否有效。
|
||||
|
||||
```
|
||||
/**
|
||||
* Check if the user name is a registered name.
|
||||
*
|
||||
* @throws IllegalArgumentException if the user name is invalid or
|
||||
* not registered.
|
||||
*/
|
||||
void checkUserName(String userName) {
|
||||
// snipped
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
这是一个糟糕的设计!
|
||||
|
||||
在这个例子中,如果userName字符串不符合规范,这是一个异常状况; 如果userName不是一个注册用户,这通常是一个正常状况。 在正常状况下使用异常处理,无疑会降低系统的效率,以及编码的效率。
|
||||
|
||||
所以正常的状况和异常的状况一定要分开、分清,不能混用。
|
||||
|
||||
```
|
||||
/**
|
||||
* Check if the user name is a registered name.
|
||||
*
|
||||
* @return true if the userName is a registered name.
|
||||
* @throws IllegalArgumentException if the user name is invalid
|
||||
*/
|
||||
boolean isRegisteredUser(String userName) {
|
||||
// snipped
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## 分清异常的类别
|
||||
|
||||
我们上面讲了异常的一些基本概念,现在我们来讲讲异常的类别。在Java语言里,异常状况分为三类。
|
||||
|
||||
<li>
|
||||
**非正常异常**(Error):这类异常的命名以Error结尾,比如OutOfMemoryError,NoSuchMethodError。这类异常,编译器编译时不检查,应用程序不需要处理,接口不需要声明,接口规范也不需要纪录;
|
||||
</li>
|
||||
<li>
|
||||
**运行时异常**(RuntimeException):这类异常的命名通常以Exception结尾,比如IllegalArgumentException,NullPointerException。这类异常,编译器编译时不检查,接口不需要声明,但是应用程序可能需要处理,因此接口规范需要记录清楚;
|
||||
</li>
|
||||
<li>
|
||||
**非运行时异常**:除了运行时异常之外的其他的正常异常都是非运行时异常,比如InterruptedException,GeneralSecurityException。和运行时异常一样,命名通常以Exception结尾。这类异常,编译器编译时会检查异常是否已经处理或者可以抛出,接口需要声明,应用程序需要处理,接口规范需要记录清楚。
|
||||
</li>
|
||||
|
||||
通常我们说的异常,指的是运行时异常和非运行时异常。对于非正常异常,我们通常使用“错误”这个词汇而不是“异常”来表示。 由于非正常异常不需要特别处理,使用“错误”这个词汇,并不会导致使用方面的误导。
|
||||
|
||||
非运行时异常还有一个别名,叫作检查型异常(CheckedException)。对应地,运行时异常的别名是非检查型异常(UncheckedException)。 为了便于识别,我们通常使用检查型异常指代非运行时异常,使用运行时异常指代非检查型异常。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/b3/b5/b3ef298ce41f79d3d2f54bc4581bd9b5.png" alt=""><br>
|
||||
我们来看看两个例子,感受下运行时异常和检查型异常不同的处理方式。
|
||||
|
||||
下面这个例子中,IllegalArgumentException是运行时异常。虽然方法的声明中没有出现IllegalArgumentException,但是在方法的规范中,需要使用记录什么情况下抛出该异常。只有这样,方法的调用者才能知道什么时候异常会抛出,该采取什么样的处理办法。
|
||||
|
||||
```
|
||||
/**
|
||||
* Check if the user name is a registered name.
|
||||
*
|
||||
* @return true if the userName is a registered name.
|
||||
* @throws IllegalArgumentException if the user name is invalid
|
||||
*/
|
||||
boolean isRegisteredUser(String userName) {
|
||||
// snipped
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
下面这个例子中,CloneNotSupportedException是检查型异常。这样的异常,一定要出现在对应方法的声明中。
|
||||
|
||||
```
|
||||
/**
|
||||
* Returns a clone if the implementation is cloneable.
|
||||
*
|
||||
* @return a clone if the implementation is cloneable.
|
||||
*
|
||||
* @throws CloneNotSupportedException if this is called on an
|
||||
* implementation that does not support {@code Cloneable}.
|
||||
*/
|
||||
public Object clone() throws CloneNotSupportedException {
|
||||
// snipped
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
这三类异常的处理,可以用下表来总结。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/83/fb/83dfc1cc1ff2c5df0a35326ca6ad4afb.png" alt="">
|
||||
|
||||
## 标记清楚抛出异常
|
||||
|
||||
应用程序需要处理异常(CheckedException和RuntimeException),就需要我们在方法的规范描述文档中清楚地标记异常。没有标记的异常,应用程序没有办法通过文档了解哪些异常需要处理、什么状况下会抛出异常以及该怎么处理这些异常。
|
||||
|
||||
对于检查型异常,编译器或者IDE会友好地提醒使用合适的声明。我们一般不会遗漏检查型异常的声明。既然声明不会遗漏,异常的标记也通常不容易遗漏。 比如上面clone()方法的例子,CloneNotSupportedException已经在方法定义部分声明了。在方法规范描述部分,只要不遗漏这个异常的描述就好了。
|
||||
|
||||
然而,对于运行时异常,我们就没有这么幸运了。目前我们使用的编译器或者IDE,还没有提醒运行时异常遗漏的功能。由于没有工具的帮助,我们就很难发现运行时异常,这就很容易导致代码效率降低,错误增多。
|
||||
|
||||
我举个例子,在上面的检查用户名的例子中,如果我们不在方法的规范描述中记录抛出的运行时异常,该方法的使用立即就会遇到问题。
|
||||
|
||||
```
|
||||
/**
|
||||
* Check if the user name is a registered name.
|
||||
*
|
||||
* @return true if the userName is a registered name.
|
||||
*/
|
||||
boolean isRegisteredUser(String userName) {
|
||||
// snipped
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
其中最常见的问题包括:
|
||||
|
||||
<li>
|
||||
如果参数userName是一个无效引用(null),会发生什么状况,该怎么处理?
|
||||
</li>
|
||||
<li>
|
||||
如果参数userName是一个空字符串(“”),会发生什么状况,该怎么处理?
|
||||
</li>
|
||||
<li>
|
||||
如果参数userName不是一个规范的用户名,会发生什么状况,该怎么处理?
|
||||
</li>
|
||||
|
||||
每一个问题,都会降低使用者的效率,让使用者陷入难以摆脱的困扰。
|
||||
|
||||
如果代码的层次结构再多一层,这个问题就会更加严重:
|
||||
|
||||
```
|
||||
/**
|
||||
* Check if the {@code userName} is an authenticated user.
|
||||
*
|
||||
* @return true if the {@code userName} is an authenticated user.
|
||||
*/
|
||||
boolean isAuthenticatedUser(String userName, String password) {
|
||||
// WRONG, should catch or describe the thrown exception.
|
||||
if (isRegisteredUser(userName)) {
|
||||
// snipped
|
||||
} else {
|
||||
// snipped
|
||||
}
|
||||
// snipped
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
如果一个方法既没有异常的声明,又没有异常的规范描述,调用者一般不会进行异常处理,也不在规范描述中加入抛出异常的描述。 这样的层次结构,只要稍微多个一两层,运行时异常虽然在代码和规范描述层面消失得无影无踪,但它并没有真正消失,依然会在运行时准时出现。
|
||||
|
||||
即使调用者拥有源代码,可以阅读源代码,也不容易意识到有运行时异常需要谨慎对待。代码的阅读者也不会有足够的精力和动力去深挖所有的层次,来确认有没有运行时异常。
|
||||
|
||||
由于编译器或者IDE并不能给我们太多的帮助,我们更需要谨慎、严格地处理好运行时异常。具体该怎么做呢?
|
||||
|
||||
<li>
|
||||
对于所有的可能抛出运行时异常,都要有清晰的描述,一个也不要错过;
|
||||
</li>
|
||||
<li>
|
||||
查看所有的调用方法的规范描述,确认抛出的异常要么已经处理,要么已经规范描述。
|
||||
</li>
|
||||
|
||||
如果我们都遵循了这两条运行时异常处理的办法,我们只需要查看一层代码的异常就可以了,这样无论是编码还是阅读代码,都会轻松不少。
|
||||
|
||||
## 处理好捕获异常
|
||||
|
||||
要想处理好异常,我们需要了解异常机制的基本原理。 我们一起回顾一下Java异常的四个要素:
|
||||
|
||||
<li>
|
||||
异常类名(IllegalArgumentException, FileNotFoundException)
|
||||
</li>
|
||||
<li>
|
||||
异常描述(“Invalid file path”)
|
||||
</li>
|
||||
<li>
|
||||
异常堆栈(at sun.security.ssl.InputRecord.read(InputRecord.java:504))
|
||||
</li>
|
||||
<li>
|
||||
异常转换(Caused by: javax.net.ssl.SSLException: Unrecognized SSL message, plaintext connection?)
|
||||
</li>
|
||||
|
||||
这四个要素满足了三个现实的异常处理问题:
|
||||
|
||||
<li>
|
||||
出了什么错?
|
||||
</li>
|
||||
<li>
|
||||
什么地方出了错?
|
||||
</li>
|
||||
<li>
|
||||
为什么会出错?
|
||||
</li>
|
||||
|
||||
其中,异常类名解决了“出了什么错”的问题;异常描述解决了“为什么会出错”的问题;异常堆栈解决了“什么地方出了错”的问题;而异常转换记录了不同场景对这三个问题的不同理解和不同处理。
|
||||
|
||||
其中JVM自动帮我们处理异常堆栈,我们需要特别关注的就只有三点了。
|
||||
|
||||
1.对于异常类名,我们要准确地选择异常类。
|
||||
|
||||
Exception类是一个包罗万象的超级异常类,如果我们使用Exception作为声明和抛出的异常,就不方便用户精准定位,从而解读和判断“出了什么错”。 类似的超级异常类还有RuntimeException、IOException等。 除非是超级的接口,否则我们应该尽量减少超级异常类的使用,而是选择那些意义明确、覆盖面小的异常类,比如FileNotFoundException。
|
||||
|
||||
2.对于异常描述,我们要清晰地描述异常信息。
|
||||
|
||||
虽然Java异常允许使用没有具体异常信息的异常,但是这种使用却容易丢失用户对于“为什么会出错”这个问题更精准的解读。 所以我不推荐使用没有描述信息的异常。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/b9/3c/b9485b59a003d569f4a3a2de46f0d43c.png" alt=""><br>
|
||||
3.对于异常转换,我们要恰当地转换异常场景。
|
||||
|
||||
随着应用场景的转换,我们还需要转换异常的类型和描述。 比如,SQLException这种涉及具体实现细节的异常类就不太适合直接抛给最终的用户应用。 用户关心的是商业的逻辑,并不是实现的细节,这就需要我们随着使用场景调整异常。如果一股脑儿地把所有的异常抛到底,业务逻辑就会很混乱,用户体验也不好。
|
||||
|
||||
但是随着场景调整异常也不是没有代价的。这是一个妥协的选择,会带来一些负面的情况。
|
||||
|
||||
第一个情况,就是需要编写转换的代码,这当然没有异常一抛到底方便。
|
||||
|
||||
第二个情况,就是信息的冗余。如果转换场景有两三层,异常打印出来的堆栈信息就会很长,而最有用的信息其实只有最原始的异常。
|
||||
|
||||
第三个情况,就是信息的丢失。有些信息的丢失是有意的,比如对敏感信息的过滤而丢掉的异常信息。有些信息的丢失是无意的过失。信息的丢失很难让我们排查出异常问题,于是错误的源头被硬生生地隐匿了起来。所以,除非有明确的需求,我们要尽量保留所有的异常信息以及转换场景。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/08/3f/08ef911dcf7311485b8b7831c422e43f.png" alt=""><br>
|
||||
由于这些负面情况的存在,我们就更要谨慎地使用异常场景转换,特别是要认真权衡信息丢失的问题。
|
||||
|
||||
## 小结
|
||||
|
||||
今天我给你讲了处理异常的一些情况,它的小陷阱很多。单就编写规范的代码来说,我们要记住三条准则:
|
||||
|
||||
<li>
|
||||
不要使用异常机制处理正常业务逻辑;
|
||||
</li>
|
||||
<li>
|
||||
异常的使用要符合具体的场景;
|
||||
</li>
|
||||
<li>
|
||||
具体的异常要在接口规范中声明和标记清楚。
|
||||
</li>
|
||||
|
||||
你还知道哪些异常处理的陷阱呢?欢迎你分享在留言区,我们一起来学习。
|
||||
|
||||
## 一起来动手
|
||||
|
||||
下面的这段Java代码,它的异常处理有没有违反我们上面讨论的原则?你能够改进下面的代码吗?
|
||||
|
||||
欢迎你把优化的代码公布在讨论区,我们一起来讨论,看哪些地方我们可以做得更好。也欢迎你把今天的内容分享给协作的小伙伴,和他一起进步。
|
||||
|
||||
```
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
class Solution {
|
||||
/**
|
||||
* Given an array of integers, return indices of the two numbers
|
||||
* such that they add up to a specific target.
|
||||
*/
|
||||
public int[] twoSum(int[] nums, int target) {
|
||||
Map<Integer, Integer> map = new HashMap<>();
|
||||
for (int i = 0; i < nums.length; i++) {
|
||||
int complement = target - nums[i];
|
||||
if (map.containsKey(complement)) {
|
||||
return new int[] { map.get(complement), i };
|
||||
}
|
||||
map.put(nums[i], i);
|
||||
}
|
||||
throw new IllegalArgumentException("No two sum solution");
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
256
极客时间专栏/代码精进之路/第一模块:代码“规范”篇/11 | 组织好代码段,让人对它“一见钟情”.md
Normal file
256
极客时间专栏/代码精进之路/第一模块:代码“规范”篇/11 | 组织好代码段,让人对它“一见钟情”.md
Normal file
@@ -0,0 +1,256 @@
|
||||
<audio id="audio" title="11 | 组织好代码段,让人对它“一见钟情”" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/0c/60/0c13fec0ee512896cf2bc8d225feac60.mp3"></audio>
|
||||
|
||||
当我们看到一个事物的时候,它的轮廓首先进入视野,给了我们第一印象。如果第一印象没有吸引到我们,那我们就不会集中注意力去关注它,也不会想去认识它。
|
||||
|
||||
我觉得有个俗语非常好地概括了这个认知习惯。这个俗语就是“不起眼”,更通俗一点的说法是“放在人群里认不出来”。
|
||||
|
||||
不管我们愿不愿意,第一印象特别影响我们的判断和心情。我们看到美好的东西,自己也跟着高兴;看到乱糟糟的东西,自己也感觉乱糟糟的。
|
||||
|
||||
代码也是这样的。如果我们看到整齐、清爽的代码,我们就对它有好感,愿意阅读,也愿意改进。 如果代码一团糟,风格混乱,我们就特别想静静地走开,一行都不想看。
|
||||
|
||||
前面的几讲中,我们讨论了注释、声明、格式、注解等编码规范。现在,是时候把这些零散的知识放到一块儿了。这些规范组合在一起,就会让代码既有让人喜悦的轮廓,也有让人清爽的细节。
|
||||
|
||||
这一次,我们将使用大量的例子,从代码文件头部结构、对象结构、类的内部结构、方法的代码结构、限定词的使用以及空行的使用这六大维度,来说明到底该怎么组织一个源代码文件。
|
||||
|
||||
## 代码文件头部结构
|
||||
|
||||
一般来说,一个源代码文件的开始部分包含三项内容。 按照出现顺序,分别为:
|
||||
|
||||
<li>
|
||||
版权和许可声明;
|
||||
</li>
|
||||
<li>
|
||||
命名空间(package);
|
||||
</li>
|
||||
<li>
|
||||
外部依赖(import)。
|
||||
</li>
|
||||
|
||||
下面就是一个例子。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ec/98/ec8f6ca100a22f3e2bf1e266b2cc3798.png" alt=""><br>
|
||||
在版权部分中,“2003, 2013”中的2003表示的是代码的诞生日期,2013表示的是代码的最近更改日期。需要注意的是,每次代码变更,都要更改代码的最近更改日期。
|
||||
|
||||
## 代码文件对象结构
|
||||
|
||||
紧随着源代码的头部,是对目标类的定义,这一部分包含三个部分。 按照出现顺序,分别为:
|
||||
|
||||
<li>
|
||||
类的规范;
|
||||
</li>
|
||||
<li>
|
||||
类的声明;
|
||||
</li>
|
||||
<li>
|
||||
类的属性和方法。
|
||||
</li>
|
||||
|
||||
这一部分中,我们需要注意的是,对于公共类,需要使用since标签,标明从哪一个版本开始定义了这个类。 这样的标明,方便我们对类进行版本管理,减少我们进行代码变更时的工作量。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/31/57/31f6b3e4d01dfed309ba567d68ab4c57.png" alt=""><br>
|
||||
比如在上面的例子中,如果需要修改Readable,当看到“since 1.5”的标签时,我们就不需要检查Java 1.4的代码了。
|
||||
|
||||
你也许会问,为什么Java 1.4不能使用Readable呢?since标签同样可以给你答案,因为Readable是从Java 1.5开始才有的接口。
|
||||
|
||||
这些问题虽然简单,但是如果没有使用since标签,答案就没有这么直观明显了。
|
||||
|
||||
## 类的内部代码结构
|
||||
|
||||
类的属性和方法,一般是一个代码文件的主要组成部分。类的结构,推荐的编码顺序依次为:
|
||||
|
||||
<li>
|
||||
类的属性;
|
||||
</li>
|
||||
<li>
|
||||
构造方法;
|
||||
</li>
|
||||
<li>
|
||||
工厂方法;
|
||||
</li>
|
||||
<li>
|
||||
其他方法。
|
||||
</li>
|
||||
|
||||
我摘抄了一段JDK的代码,你看这样是不是更干净整洁?
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ea/18/ea29c410fa71bd1b8639f3014fab0118.jpg" alt=""><br>
|
||||
上面的代码案例中没有用到工厂方法,我来讲一下这个。
|
||||
|
||||
类似于构造方法,工厂方法也是用来构造一个类的实例。不同的是,工厂方法有具体的返回值。它可以是静态方法,也可以是实例方法。
|
||||
|
||||
如果是实例方法,工厂方法还可以被子类重写。这是工厂方法和构造方法的另外一个重要的区别。由于工厂方法也扮演着构造类实例的功能,我们一般把它放在构造方法的后面,其他方法的前面。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/e4/53/e4e61e838d6ac10db8dbab2b8a621053.jpg" alt="">
|
||||
|
||||
## 方法的代码结构
|
||||
|
||||
讲完了类的内部代码结构,我们再来讲讲方法的代码结构。一般来说,一个方法需要包含三项内容:
|
||||
|
||||
<li>
|
||||
方法的规范;
|
||||
</li>
|
||||
<li>
|
||||
方法的声明;
|
||||
</li>
|
||||
<li>
|
||||
方法的实现。
|
||||
</li>
|
||||
|
||||
内部类的内部方法,可以没有第一部分。但对于公开类的公开方法,方法的规范一定不能缺失。 一个典型的规范,应该包含以下十个部分:
|
||||
|
||||
<li>
|
||||
方法的简短介绍;
|
||||
</li>
|
||||
<li>
|
||||
方法的详细介绍(可选项);
|
||||
</li>
|
||||
<li>
|
||||
规范的注意事项 (使用apiNote标签,可选项);
|
||||
</li>
|
||||
<li>
|
||||
实现方法的要求 (使用implSpec标签,可选项);
|
||||
</li>
|
||||
<li>
|
||||
实现的注意事项 (使用implNote标签,可选项);
|
||||
</li>
|
||||
<li>
|
||||
方法参数的描述;
|
||||
</li>
|
||||
<li>
|
||||
返回值的描述;
|
||||
</li>
|
||||
<li>
|
||||
抛出异常的描述:需要注意的是,抛出异常的描述部分,不仅要描述检查型异常,还要描述运行时异常;
|
||||
</li>
|
||||
<li>
|
||||
参考接口索引(可选项);
|
||||
</li>
|
||||
<li>
|
||||
创始版本(可选项)。
|
||||
</li>
|
||||
|
||||
下面的这个例子,也是来自JDK的源代码。你可以清晰地看到,这段代码中的规范是非常典型的。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/5e/60/5e56601fedf15a44ec604175f1008d60.jpg" alt=""><br>
|
||||
如果方法的创始版本和它所属类的创始版本一致,方法的创始版本描述可以省略。 要不然,一定要加入方法的创始版本标签。
|
||||
|
||||
像下面这个例子,就添加了创始版本标签。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/3e/45/3e7e6fb89f1805ab562a8e09506e5a45.png" alt="">
|
||||
|
||||
## 按顺序使用限定词
|
||||
|
||||
在声明一个类、方法或者方法属性时,为了更准确地描述和理解声明的适用场景,我们通常要使用修饰性的关键词。这些修饰性的关键词,我们通常称它们是修饰符或者限定词。 一个声明,可以使用多个限定词。
|
||||
|
||||
Java的语法中,限定词的使用顺序没有强制性规定。但是,限定词的使用顺序有一个约定俗成的规则。按照这个规则使用限定词,一般来说,我们最关切的修饰符最先进入我们的视野,和标识符最密切的位置最靠近标识符。使用一致性的顺序,我们就能更快速地理解一个声明。
|
||||
|
||||
限定词推荐使用顺序:
|
||||
|
||||
<li>
|
||||
public/private/protected (访问控制限定词,制定访问权限)
|
||||
</li>
|
||||
<li>
|
||||
abstract (抽象类或者抽象方法,具体实现由子类完成)
|
||||
</li>
|
||||
<li>
|
||||
static (静态类、方法或者类属性)
|
||||
</li>
|
||||
<li>
|
||||
final (定义不能被修改的类、方法或者类属性)
|
||||
</li>
|
||||
<li>
|
||||
transient(定义不能被序列化的类属性)
|
||||
</li>
|
||||
<li>
|
||||
volatile(定义使用主内存的变量)
|
||||
</li>
|
||||
<li>
|
||||
default(声明缺省的方法)
|
||||
</li>
|
||||
<li>
|
||||
synchronized(声明同步的方法)
|
||||
</li>
|
||||
<li>
|
||||
native(声明本地的方法,也就是Java以外的语言实现的方法)
|
||||
</li>
|
||||
<li>
|
||||
strictfp(声明使用精确浮点运算)
|
||||
</li>
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/4b/ce/4b6a702a1cbbc1f02882b9da0f44d1ce.png" alt="">
|
||||
|
||||
## 使用空行分割代码块
|
||||
|
||||
我们之前讲过怎么整理代码,一个重要的原则就是“给代码分块”,通过空格、缩进、空行实现这个目的。
|
||||
|
||||
再来回顾一下空行的作用,空行用于垂直分割,用于分开同级别的不同代码块。
|
||||
|
||||
我们可以使用空行分割如下的代码块:
|
||||
|
||||
<li>
|
||||
版权和许可声明代码块;
|
||||
</li>
|
||||
<li>
|
||||
命名空间代码块;
|
||||
</li>
|
||||
<li>
|
||||
外部依赖代码块
|
||||
</li>
|
||||
<li>
|
||||
类的代码块;
|
||||
</li>
|
||||
<li>
|
||||
类的属性与方法之间;
|
||||
</li>
|
||||
<li>
|
||||
类的方法之间;
|
||||
</li>
|
||||
<li>
|
||||
方法实现的信息块之间。
|
||||
</li>
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/8e/f3/8ee01400b7a9131bdd717385ded77cf3.png" alt="">
|
||||
|
||||
## 小结
|
||||
|
||||
对于软件开发者来说,组织代码是一项基本技能,也是我们需要养成的好习惯。组织代码有许多不同的习惯和策略,我们要学会辨别这些策略中哪些是有效的,哪些是有害的。
|
||||
|
||||
怎么辨别呢?
|
||||
|
||||
和其他技能一样,最快的提升方法是仔细思考一下为什么我们要做出这样的选择,而不是其他的。知其然远远不够,还要知其所以然。
|
||||
|
||||
你可以试着看看你的项目,源代码是按照这种方式组织的吗?哪些部分采用了合理的组织方式,哪些部分还有改进的空间?哪些是值得分享的经验?欢迎你把想法分享在留言区,我们一起来学习。
|
||||
|
||||
## 一起来动手
|
||||
|
||||
下面的这段Java代码,我们已经很熟悉了。前面,我们对它做过很多方面的修改。这一次,我们把前面的修改集中起来。你试着去找到所有可以改进的地方,然后比较一下修改前和修改后的代码。你有什么感受?
|
||||
|
||||
欢迎你把优化的代码公布在讨论区,让我们一起来感受、来欣赏!
|
||||
|
||||
也欢迎点击“请朋友读”,把这篇文章分享给你的朋友或者同事,一起来体验修改代码的快感。
|
||||
|
||||
```
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
class Solution {
|
||||
/**
|
||||
* Given an array of integers, return indices of the two numbers
|
||||
* such that they add up to a specific target.
|
||||
*/
|
||||
public int[] twoSum(int[] nums, int target) {
|
||||
Map<Integer, Integer> map = new HashMap<>();
|
||||
for (int i = 0; i < nums.length; i++) {
|
||||
int complement = target - nums[i];
|
||||
if (map.containsKey(complement)) {
|
||||
return new int[] { map.get(complement), i };
|
||||
}
|
||||
map.put(nums[i], i);
|
||||
}
|
||||
throw new IllegalArgumentException("No two sum solution");
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
159
极客时间专栏/代码精进之路/第一模块:代码“规范”篇/12丨组织好代码文件,要有“用户思维”.md
Normal file
159
极客时间专栏/代码精进之路/第一模块:代码“规范”篇/12丨组织好代码文件,要有“用户思维”.md
Normal file
@@ -0,0 +1,159 @@
|
||||
<audio id="audio" title="12丨组织好代码文件,要有“用户思维”" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/43/75/43a29ce4d9aac7453a1feb6c1397e275.mp3"></audio>
|
||||
|
||||
上一讲中,我们讲了如何组织代码段,今天我来讲下,如何组织代码文件。
|
||||
|
||||
最开始接触一个项目代码时,我们最渴望的,就是快速揭开项目的面纱。这个项目是干什么的?是怎么做的?该怎么使用?
|
||||
|
||||
有很多这样的问题,排着队等我们处理。我们需要从一个点开始,先撕破一点点皮,然后,像剥洋葱一样,一层一层地阅读,一层一层地了解。
|
||||
|
||||
刚拿到一个项目的代码时,你最想找哪一个文件?面对大量的文件,该从哪里入手?创建一个项目时,各式各样的文件该怎么规整?
|
||||
|
||||
如果一个项目很小,只有三五个文件,我们不用担心上述的问题。
|
||||
|
||||
但事实上,一个典型的软件项目,拥有上百个文件是寻常的事情。如果这些文件组织混乱,就会让整个项目乱糟糟的,我们很难入手去查找、阅读和测试。
|
||||
|
||||
其实文件的组织是一个简单的事情,但这些简单的事情如果不能做得清晰、明了,就会变成一个效率的黑洞。
|
||||
|
||||
文件的组织要层次分明、易于检索、一目了然。要做到这一点,我们可以从用户思考问题的逻辑入手。
|
||||
|
||||
## 逻辑之一:软件是干什么的?
|
||||
|
||||
无论我们开始一个软件项目,还是阅读一个软件的代码,第一个遇到的问题就是,这个软件是干什么的?
|
||||
|
||||
可以回答这个问题的文件叫做README,它的命名全部使用大写字母。需要被放在一个软件工程的根目录里,方便人或者机器第一时间找到,从而轻而易举地找到并进行阅读。
|
||||
|
||||
“软件要用来干什么?”是一个软件工程的启动问题。
|
||||
|
||||
一个软件项目开始时,这个问题的答案可以不是很丰满,但是,最基本的思想一定要有。随着软件的研发进程,它的描述可以越来越清晰。软件成型之前,这个问题必须干脆地解决掉,得到明确的答案。
|
||||
|
||||
这个问题的描述,要记录在代码的工程里。可以让代码的阅读者轻而易举地找到并阅读。
|
||||
|
||||
由于机器的参与,尤其是在线代码管理服务(比如GitHub、Maven)的广泛使用,README的名字和位置也就慢慢地形成了共识。
|
||||
|
||||
## 逻辑之二:软件可以拿来用吗?
|
||||
|
||||
如果我们看到了README,想使用这个软件,那么紧接着的问题就是,这个软件我们可以使用吗?
|
||||
|
||||
所有的软件,都有归属,都受版权的保护。谁拥有这个软件的版权?这是我们需要关注的一个问题。
|
||||
|
||||
有时候,一个软件包含很多贡献者,不同的贡献者有不同的版权诉求。软件的不同部分,就有不同的版权。
|
||||
|
||||
这种情况下,**版权描述一般放在每一个源文件的头部**。不同的源文件可以有不同的版权,同一个源文件也可以有一个以上的版权所有者。
|
||||
|
||||
如果版权来源只有一个,而且源文件头部没有版权描述,我们就需要把版权描述放到最显眼的地方。**这个地方就是软件工程的根目录,命名为COPYRIGHT,全部使用大写字母**。
|
||||
|
||||
没有版权描述的软件,并不是没有版权保护。如果一个软件没有版权描述或者版权描述不清晰,使用起来有很多法律风险。如果这个软件依赖外部的版权,那么问题就会变得更为复杂。
|
||||
|
||||
有了版权保护,不代表我们就不能使用这个软件了。我们能不能使用、怎么使用,是由软件的许可证确定的。
|
||||
|
||||
许可证文件是LICENSE,全部使用大写字母,放在软件工程项目的根目录下。
|
||||
|
||||
当使用软件的时候,不能超越许可证约定的范围。 一个没有许可证的软件,我们是不能使用的,因为不知道许可的范围,也不知道应承担的义务。同样,如果一个软件的许可证不清晰,或者我们不了解,那么使用起来也会有很多法律问题。
|
||||
|
||||
## 逻辑之三:软件是怎么实现的?
|
||||
|
||||
作为程序员,代码是我们看软件世界的语言。我们关心的下一个问题就是,这个软件是怎么实现的?
|
||||
|
||||
代码的位置在现在的软件工程里有了一定的共识。通常来说,源代码存放在根目录下的src目录下。
|
||||
|
||||
当看到src目录的时候,我们就可以确认,这里面是源代码文件。当我们要查找源代码的时候,也是在软件工程文件里查找src目录。我不建议在这里搞创新,不要使用其他的名字或者位置。
|
||||
|
||||
但源代码并不能一股脑地堆在src这一个目录里。src目录下面可以有很多子目录。一般来说,稍具规模、相对正规的软件,都需要有命名空间的区隔。使用命名空间的区隔至少有三个好处:
|
||||
|
||||
<li>
|
||||
可以把一个组织的代码和另外一个组织的代码区隔开;
|
||||
</li>
|
||||
<li>
|
||||
可以把一个项目的代码和另外一个项目的代码区隔开;
|
||||
</li>
|
||||
<li>
|
||||
可以把一个模块的代码和另外一个模块的代码区隔开。
|
||||
</li>
|
||||
|
||||
每一个命名空间的节点,都要对应一个文件目录。比如,我们常用的java.lang这个命名空间,就要相应地使用“java/lang”这两级目录。
|
||||
|
||||
如果软件项目把所有的源代码文件不加分别地放在同一个目录下,说明软件的开发人员并没有厘清代码之间的逻辑关系。纠缠在一起的代码越多,代码越难以维护,代码的安全越难以保证。
|
||||
|
||||
## 逻辑之四:软件该怎么测试?
|
||||
|
||||
如果要使用或者发布一个软件,最值得关注的还有软件的质量。软件的质量,首先要通过软件测试这一关。那么软件该如何测试呢? 这是我们要面对的另一个问题。
|
||||
|
||||
有很多传统的软件项目,测试代码和功能代码是放在同一个目录下的。如果一个项目比较小,那么这样做也许没什么大问题。一旦软件工程变得复杂,这样做就会让项目变得难以管理,尤其是在测试人员和开发人员分离的情况下。
|
||||
|
||||
让测试人员看着实现代码写测试,会误导测试的用例选择和测试效果;让开发人员看着测试代码改实现,也会影响开发的质量和效率。
|
||||
|
||||
既然要分工,不同的代码就要区隔开来。
|
||||
|
||||
如果开发和测试是一个人,或者是同一个小组成员,是不是就可以混在一起了呢? 当然不是,因为看代码的人依然可能是分工的。区隔实现和测试,可以尽可能照顾到每个工程师,方便他们工作。
|
||||
|
||||
幸运的是,现在的很多软件项目,大都采用了分离的组织方式。通常来说,源代码要存放在根目录下的test目录下。
|
||||
|
||||
仅仅放置在对应的目录下还不够,测试文件本身还有一个需要注意的地方,一个测试文件,最好执行一个独立的任务。如果测试出错误,我们就能够快速定位错误。这也要求测试的目标要小,测试的结果要清晰,测试的组织要照应功能代码的组织。
|
||||
|
||||
比如说,对java.io测试的文件,要放在java/io测试目录下;对java.util测试的文件,要放在java/util目录下。这种互相照应的组织方式,从目录名就可以看出测试的范围。这样既可以快速从功能代码找到测试代码,也可以快速地从测试代码找到功能代码。
|
||||
|
||||
## 软件该怎么使用?
|
||||
|
||||
使用软件工程项目文件的,不仅仅只有程序员,还有软件的用户。
|
||||
|
||||
要是只能通过阅读软件代码来揣测软件该怎么使用,这样既没有效率,也容易犯错,更偏离了软件设计者的初衷。
|
||||
|
||||
一个好的软件,要尽可能降低使用门槛。编写使用指南和代码示例是两个常用的办法。一份好的用户文档,应该让软件的用户快速入门,然后再逐步深入地了解整个软件的使用细节,以及潜在的问题。
|
||||
|
||||
软件的文档,需要随着软件的变更不断升级维护。有很多项目会把文档和代码分开管理。
|
||||
|
||||
但实际上,这样做有一些缺陷,它会让文档变得难以维护。
|
||||
|
||||
我们经常看到很多文档和软件脱节的软件,很大一部分是由于项目组织和管理方面的问题导致的。如果代码的变更,也需要相应地变更文档,那么文档和代码统一管理,是一个更高效的组织方式。
|
||||
|
||||
如果你留意,就会注意到现在的很多项目,在根目录下,有一个名字为docs或者doc的目录。 这个目录就是存放软件文档的。
|
||||
|
||||
作为程序员,我们不仅要熟悉源代码,还要熟悉文档。当需要更直观的用户指南或者代码示例时,就要写作这样的软件文档。对于每一行的代码变更,我们都要问,需不需要文档变更?如果代码和文档一致的话,就会节省我们大量的维护时间和维护成本。
|
||||
|
||||
下面的例子,是一个常见的软件组织形式,也是我们对上述讨论的小结。
|
||||
|
||||
```
|
||||
Project directory layout:
|
||||
src/
|
||||
java/
|
||||
lang/
|
||||
io/
|
||||
javax/
|
||||
net/
|
||||
ssl/
|
||||
test/
|
||||
javax/
|
||||
net/
|
||||
ssl/
|
||||
doc/
|
||||
make/
|
||||
README
|
||||
COPYRIGHT
|
||||
LICENSE
|
||||
|
||||
```
|
||||
|
||||
## 小结
|
||||
|
||||
我们的日常软件开发工作,有很多都依赖于集成开发环境(IDE)。主流的IDE有缺省的文件组织形式。一般情况下,我们可以使用缺省的组织形式,然后添加进缺失的内容。
|
||||
|
||||
也有很多软件开发,不依赖于IDE。这就需要我们自己规划好文件的组织原则和基本形式。不同的语言,不同的项目,文件的组织方式差别可能很大。
|
||||
|
||||
如果你需要自己制定组织形式,我建议参考一些成功项目的组织方式。比如,如果你要做一个中间件,为客户提供类库,就可以参考OpenJDK的文件组织方式。
|
||||
|
||||
如果没有什么现成的项目可以参考借鉴的,请记住以下两点:
|
||||
|
||||
<li>
|
||||
文件的组织要一目了然,越直观,越有效率;
|
||||
</li>
|
||||
<li>
|
||||
可维护性要优先考虑。这要求文件组织要层次分明,合理区隔、照应、使用不同的空间。
|
||||
</li>
|
||||
|
||||
## 一起来动手
|
||||
|
||||
由于项目的多样性,项目文件组织的具体形式有很多差异。借这个机会,我也想学习一下大家的项目文件组织经验。你做的项目,是怎么组织文件的?为什么选择这种组织的形式?你最欣赏的这种形式的哪几点?你阅读一个项目代码时,是怎么一步一步深入进去的?欢迎你把你的经验公布在讨论区,我们一起来学习,一起进步。
|
||||
|
||||
欢迎你把这篇文章分享给你的朋友或者同事,一起来探讨吧!
|
||||
|
||||
|
||||
136
极客时间专栏/代码精进之路/第一模块:代码“规范”篇/13 | 接口规范,是协作的合约.md
Normal file
136
极客时间专栏/代码精进之路/第一模块:代码“规范”篇/13 | 接口规范,是协作的合约.md
Normal file
@@ -0,0 +1,136 @@
|
||||
<audio id="audio" title="13 | 接口规范,是协作的合约" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/ca/52/ca12a4817746096cec65df30b2a79952.mp3"></audio>
|
||||
|
||||
一个软件项目,一般需要交付两类文档。一类文档是面向开发者的,另一类文档是面向最终用户的。这两类文档,由于面向用户的不同,无论是内容还是形式,都有巨大的差异。今天我们先来聊聊面向开发者的文档。下一讲中,我们再接着聊面向最终用户的文档。
|
||||
|
||||
## 区分外部接口和内部实现
|
||||
|
||||
为了便于维护和协作,一个软件通常被划分为几个不同的部分。比如我们通常使用的MVC架构,把软件分为模型(Model)、视图(View)和控制器(Controller)三个部分。这样做,可以降低复杂度,让程序结构更加直观。同时,这种架构也很容易对程序进行修改和扩展,并且可以重复利用基础的功能。
|
||||
|
||||
不同功能的分离,让程序员之间产生了分工,专业人员可以更聚焦于个人的专长领域。这是一个多赢的局面,也能让软件的质量得到提升。
|
||||
|
||||
既然有分工,就要有协作。MVC架构把软件拆分为三块,是分工;而MVC模块之间的调用关系,就是协作。
|
||||
|
||||
一个好的软件设计,要区分外部接口和内部实现。外部接口,就是协作的界面,要简单规矩;内部实现,可以是千变万化的复杂小世界。
|
||||
|
||||
这种区分无处不在,即使是最普通的API。比如我们常用的InputStream,一旦我们获得这个对象实例,就可以调用它的read()方法。 我们不用去关心,它的底层实现是一个文件,一段内存,还是一个远程连接。InputStream的接口定义只有十个方法,短短的500多行代码。
|
||||
|
||||
但是它的内部实现却是一个更大的世界,广泛地分布在不同的类库、不同的模块,实现着不同的具体功能,有些实现甚至超出想象的复杂,比如一个安全连接的_InputStream_的实现,一般有着数万行的代码。
|
||||
|
||||
幸运的是,我们区分了接口和实现,调用者就不用去关心这些复杂的实现了,只需要理解接口规范就好。
|
||||
|
||||
提高协作效率的最高技巧不是提高沟通技巧,而是要减少沟通的数量,提高沟通的质量,尤其是要减少数量。如果你参加了工作,没完没了的会议,没完没了的文案,都会加深你对这条原则的理解。软件的设计也是这样,外部接口,要少、要小、要描述清楚。
|
||||
|
||||
## 接口规范是协作合约
|
||||
|
||||
由于外部接口是协作的界面,是调用者和实现者之间的合约,所以对它就有了更加严格的要求。这里我总结了合约的四个原则:成文、清楚、稳定、变更要谨慎。
|
||||
|
||||
具体要怎么实践这些原则呢?
|
||||
|
||||
**合约要成文**
|
||||
|
||||
无论对于调用者,还是实现者来说,外部接口的使用都要有章可循,有规可依。如果调用者需要去看实现代码来理解外部接口,那么外部接口和内部实现的分离还有什么用呢?不就背离了外部接口和内部实现分离的初衷吗?这样做既是对实现者的纵容,也是对调用者的无视。
|
||||
|
||||
比如说,Java的每个版本的API文档和指南,就是Java语言的合约。
|
||||
|
||||
**合约要清楚**
|
||||
|
||||
合约既然是我们协作的依靠,就一定要清晰可靠、容易遵循,不能有模棱两可的地方。如果接口规范描述不清,既误导调用者,也误导实现者。
|
||||
|
||||
如果接口规范复杂难懂,说明接口的设计也很糟糕。
|
||||
|
||||
那么接口规范要怎么描述呢?
|
||||
|
||||
接口规范主要用来描述接口的设计和功能,包括确认边界条件、指定参数范围以及描述极端状况。比如,参数错了会出什么错误?
|
||||
|
||||
这里需要注意的是,接口规范不是我们定义术语、交代概念、提供示例的地方。这些应该在其他文档中解决,比如我们下次要聊的面向最终用户的文档。
|
||||
|
||||
**合约要稳定**
|
||||
|
||||
既然是合约,意味着调用者必须依赖于现有的规范。比如InputStream.read()这个方法,接口规范描述的是读取一个字节(8-bit),返回值是介于0和255之间的一个整数。如果我们要把这一个规范改成返回值是介于-128到127之间的一个整数,或者是读取一个字符(比如一个汉字),都会对现有的使用代码造成灾难性的影响。
|
||||
|
||||
接口的设计和规范的制定,一定要谨慎再谨慎,小心再小心,反复推敲,反复精简。一旦接口合约制定,公布,然后投入使用,就尽最大努力保持它的稳定,即使这个接口或者合约存在很多不足。
|
||||
|
||||
**变更要谨慎**
|
||||
|
||||
世界上哪里有一成不变的东西呢!技术的进步、需求的演进,总是推着我们朝前走。合约也需要跟得上变化。
|
||||
|
||||
可是,接口合约毕竟不是租房合约,可以一年一续,每年变更一次。租房合约的变更成本很小,但软件的接口合约变更的影响要严重得多。特别是兼容性问题,稍微一丁点儿的接口规范变化,都可能导致大面积的应用崩溃。越成功的接口,使用者越多,变更的影响也就越大,变更的成本也就变高,变更也就越困难。你可以试着想一想,如果InputStream.read()这个方法在Java中删除,会造成多大的影响?会有多少应用瘫痪?
|
||||
|
||||
所以,对于接口规范,我们的原则是,能不变更就不变更;必须的变更,一定要反复思量该怎么做才能把影响降到最低。
|
||||
|
||||
## 使用Java Doc
|
||||
|
||||
说完了接口规范的几个原则,我们就来讲一下,如何实践这些原则。接口的规范描述,应该怎么组织?
|
||||
|
||||
从使用者角度出发,包括接口的调用者和实现者,接口的规范应该便于阅读,便于查找。从制定者的角度出发,接口的规范应该便于定义,便于维护。
|
||||
|
||||
JavaDoc就是一种顾及了多方利益的一种组织形式。它通过文档注释的形式,在接口声明的源代码定义和描述接口规范。这种和源代码结合的方式,可以方便我们维护接口规范,也有利于保持接口规范和接口声明的一致性。
|
||||
|
||||
JavaDoc工具可以把文档注释,转换为便于阅读为HTML文档。这样就方便规范的使用者阅读了。
|
||||
|
||||
当然,也不是所有的规范,都一定要使用JavaDoc的形式,特别是冗长的规范。如果有两种以上不同形式的规范组织文档,**我建议一定要互相链接、引用**。比如,冗长的规范可以单独放在一个文件里。然后,在Java Doc对应的文件里,加上改规范的链接。
|
||||
|
||||
比如下面的例子中,“Java Security Standard Algorithm Names Specification”就是一个独立的,较长的规范文档。当需要使用这个文档的时候,就要在对应的接口中指明该文档的位置,这样方便用户进行检索。
|
||||
|
||||
上面的文档注释,经过JavaDoc的处理,就变成了便于用户阅读的文字。
|
||||
|
||||
>
|
||||
protected Signature(String algorithm)
|
||||
Creates a Signature object for the specified algorithm.
|
||||
<p>Parameters:<br>
|
||||
algorithm - the standard string name of the algorithm. See the Signature section in the Java Security Standard Algorithm Names Specification for information about standard algorithm names.</p>
|
||||
|
||||
|
||||
## 谁来制定接口合约?
|
||||
|
||||
这本来不是一个问题。但是由于我们选择在源代码中,需要通过文档注释表达接口合约,这就成了一个很严肃的问题。
|
||||
|
||||
源代码的维护者,是不是对接口合约拥有无限的修改权利呢?
|
||||
|
||||
肯定不是的。
|
||||
|
||||
既然是合约,就是大家都认可并且接受的规范和细节,只有形成共识才能编辑和修订。合约的编写和修订,一般不应该由源代码的维护者一人决定,而应该由参与各方充分沟通和协商。
|
||||
|
||||
“三个臭皮匠,顶个诸葛亮”,我们要充分尊重参与各方的能力,信任充分的沟通可以成就更好的规范。
|
||||
|
||||
一个软件项目,不管大小,只要参与者超过两个,都要讨论清楚彼此之间的分工协作方式。这当然也包括,讨论清楚如何制定、修改程序接口。
|
||||
|
||||
比如,OpenJDK的接口制定和修订,就一定要经过下面的步骤:
|
||||
|
||||
<li>
|
||||
起草接口规范,或者起草提议的修订规范;
|
||||
</li>
|
||||
<li>
|
||||
找相关领域的专家,审议草案,并根据评审意见,修改接口规范;
|
||||
</li>
|
||||
<li>
|
||||
如果领域专家审议通过,提交兼容性和规范性审查程序; 并根据审查意见,相应地修改接口规范;
|
||||
</li>
|
||||
<li>
|
||||
兼容性和规范性审查通过,修改接口合约;
|
||||
</li>
|
||||
<li>
|
||||
按照议定的接口规范,编写最终的实现的代码。
|
||||
</li>
|
||||
|
||||
当然了,你的软件项目,也许和OpenJDK有巨大的差异。你要找到适合自己公司和项目的,接口合约制定和修改的适当方式。
|
||||
|
||||
## 小结
|
||||
|
||||
对于接口规范,我们要有意识地使用下面的这条原则:
|
||||
|
||||
>
|
||||
接口规范是使用者和实现者之间的合约。
|
||||
|
||||
|
||||
我们在工作过程中,如果有和接口相关的迷惑或者争执,可以多想一想上面的这条原则。
|
||||
|
||||
## 一起来动手
|
||||
|
||||
2018年12 月 25 日,部分开发者突然发现他们开发的 Web 网页的界面发生了变化,按钮上方出现“积雪”。这超出开发者的脑洞和认知,难道是圣诞老人的礼物,或者是黑客的祝福?经过探索发现这是前端 UI 组件库 Ant Design(简称 antd)提前埋入一个未经声明的“彩蛋”。事件迅速发酵,引起了巨大争议。
|
||||
|
||||
前人的危机都是后人的财富。该怎么做,才可以避免类似的事情?欢迎你在讨论区留言,我们一起把这个事件转化成我们的见识和能力。
|
||||
|
||||
也欢迎点击“请朋友读”,把这篇文章分享给你的朋友或者同事,一起来交流。
|
||||
|
||||
|
||||
136
极客时间专栏/代码精进之路/第一模块:代码“规范”篇/14 | 怎么写好用户指南?.md
Normal file
136
极客时间专栏/代码精进之路/第一模块:代码“规范”篇/14 | 怎么写好用户指南?.md
Normal file
@@ -0,0 +1,136 @@
|
||||
<audio id="audio" title="14 | 怎么写好用户指南?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/5c/5d/5cd1d4fdbe7bf4b59d7a6353d4dbc95d.mp3"></audio>
|
||||
|
||||
前一段时间,我要买一部家用的跑步机。有一款跑步机看起来配置齐备,商品的标题中指明“需要组装”。
|
||||
|
||||
商品的评论只有两条。其中一条给了三分:“还没有来得及试一试这个新到的跑步机。因为,我一直试着把它组装起来。我做梦都没有想到,‘需要组装’意味着我花了三天时间,都没有组装起来。它也许是一个好的跑步机,可是令人失望的是,这些零件到底该怎么凑在一起!”
|
||||
|
||||
而另一条则给了最低的一分。评论写道:“商品描述不准确。这台机器非常重,长度甚至超过两人沙发。一般的家庭根本放不下这台跑步机。已经退货了”。
|
||||
|
||||
你可以想象,这两条仅有的评论对这款跑步机的销售有多大的杀伤力。它本身的品质无论如何,都不至于沦落到一分、三分的地步。
|
||||
|
||||
问题在哪儿呢?无论是谁,花了三天时间都搞不定组装,肯定有一肚子的不满意。好不容易组装起来,却发现没有空间放置,又要拆掉退货,就会更不满意。
|
||||
|
||||
我了解了一下这款跑步机的用户手册,发现组装非常繁琐,所涉及的部件有很多,还真不是一下子就可以搞定的。
|
||||
|
||||
很显然,用户指南非常重要,但这款跑步机却给我们提供了一个反面的案例,可见写出一份好的用户指南也不是一件容易的事。
|
||||
|
||||
**最好的用户指南,是产品本身**。我们随手拿一只圆珠笔,就知道怎么用。然而,不是所有的产品都能够简单到拿来就用。一份合格的用户指南,要帮助用户减少产品使用的障碍,快速地使用产品。
|
||||
|
||||
作为程序员,我们常和软件的用户指南打交道。软件和的用户指南和家用电器的操作指南一样,是最终用户的参考手册。今天,我就和你聊聊怎么写好软件的用户指南这个话题。
|
||||
|
||||
## 谁是指南的用户?(用户指南写给谁?)
|
||||
|
||||
既然是用户指南,就必须让用户看得懂、用得上。首先我们需要搞懂,谁是用户? 只有这样,我们才能了解用户的日常习惯和知识偏差。
|
||||
|
||||
一般来说,我们把用户的门槛降得越低,潜在用户的群体就越大,软件就越容易推广。
|
||||
|
||||
比如说,访问一个网站时,我们只要在浏览器输入网址,就可以浏览网站的内容。这背后的技术,真的像大海一样深不可测。可是,作为用户,我一点儿也不关心这些技术,我只关心,我能不能访问到这个网站。
|
||||
|
||||
假设一个浏览器的设计者,添加了两个强制的选择项,让用户访问网站时选择:
|
||||
|
||||
<li>
|
||||
使用TCP协议还是UDP协议?
|
||||
</li>
|
||||
<li>
|
||||
使用HTTP还是HTTPS?
|
||||
</li>
|
||||
|
||||
估计很多用户立即就会不知所措。他们大部分都不知道这些协议到底是什么,更别提让他们做出有实际意义的选择了。
|
||||
|
||||
我举这样的一个例子,想说明的是,有了用户的千差万别,才有了软件的千差万别。我们不能想当然地认为,有了详实的用户指南,用户就能够使用产品。
|
||||
|
||||
**事实上,用户指南,不能超越用户的理解能力和操作能力。**
|
||||
|
||||
## 什么时候确定产品用户?
|
||||
|
||||
这是一个老生常谈的问题。之所以常谈,是因为我们很容易就忘了我们的用户。所以,不得不经常拎出来谈一谈,时不时地拽一拽这根弦。
|
||||
|
||||
等到产品出来的时候才确定用户,再写用户指南,就太晚了。 谁是我们的用户?用户该怎么使用我们的产品?这些问题应该是产品还没有影子的时候,就要开始着手解决的问题。 然后,在产品研发的全工程中,反复地调试、测试这两个问题的答案。
|
||||
|
||||
需要注意的是,这两个问题答案,千万不要越来越丰富,而是要简化再简化,越来越简单。
|
||||
|
||||
无论用户手册写得有多好,一个需要组装近百个部件才可以使用的跑步机,都瑜不掩瑕。
|
||||
|
||||
所以在产品设计时,就需要解决掉用户定位和怎么使用这两个问题。
|
||||
|
||||
比如浏览器的设计,我们一定要压制住给用户提供更多选择的冲动,把技术细节隐藏到具体实现中去,而不是让用户来做选择。即便是简单的给浏览器输入地址这一行,你想一想下面的这些地址隐藏了多少细节!
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/9e/df/9ecf311af00d72b57539e48053f7b6df.png" alt=""><br>
|
||||
我们经常使用浏览器,所以讨论浏览器的设计时,可能比较容易理解简单带来的巨大好处。如果换成我们自己的软件产品,看起来就没有那么容易了。
|
||||
|
||||
我们可能生怕遗漏了什么、错过了什么。丢掉东西,尤其是放弃一个看似美妙的想法,不符合我们的思维方式和行为习惯。 但在用户指南这个问题上,我们需要记住“less is more”。如果可以从用户的角度看问题,产品中自我的影子就会少一点点,这些选择也会简单一点点。
|
||||
|
||||
## 保持指南和代码的一致
|
||||
|
||||
和接口规范一样,用户指南一般也是由程序员和其他的作者一同完成的。和接口规范不一样的是,接口规范可以在源代码文件中定义、描述,而用户指南通常是独立于源代码的文档。这种分离的组织形式,经常会让用户指南和源代码脱节。如果维护指南的作者和维护源代码的作者不是同一个人,脱节就会更加严重,导致维护和服务都很困难。
|
||||
|
||||
那么该怎么办呢?程序员一定要和用户指南的维护者保持通畅的沟通,商定协作的方式。
|
||||
|
||||
比如,在OpenJDK中,如果源代码的修改导致了行为和接口的改变,就要考虑是不是需要修改用户指南。如果需要修改用户指南,就要提交一个用户指南修改的提议。在这个提议里,说明代码导致的行为、接口的变更,以及用户指南修改的建议。用户指南的维护者收到提议后,就会跟踪代码的变更,提议具体的修改方案,完成用户指南的修改。源代码修改和用户指南的修改,一定要放在一个连贯的流程里。
|
||||
|
||||
保持用户指南和源代码的一致性是一个很有挑战性的任务。这是一个成本很高的问题,你一定要找到适合自己的办法。比如可以像OpenJDK一样,通过改进开发流程,把用户指南修改和代码变更绑定在一起。用户指南修改修改完成,这个代码变更才算完成。如果工具使用得当,团队合作流畅,这是一个可以尝试的好办法。
|
||||
|
||||
## 开发指南的实践
|
||||
|
||||
开发指南是一类我们经常接触的用户指南,它的典型用户是使用该软件API的软件工程师。
|
||||
|
||||
我们上一次聊到了接口规范。开发指南和接口规范这两类文档是怎么分工的呢? 接口规范的重点在于接口的设计和功能描述上,包括确认边界条件、指定参数范围以及描述极端状况。而开发指南的重点在于定义术语、交代概念、提供示例、问题排查等。
|
||||
|
||||
接口规范描述的是每一个部件的规格,是一个细节、局部的范围。开发指南说的就是怎么理解这些部件之间的关系,怎么把这些部件组合起来,形成更大的部件或者产品。
|
||||
|
||||
对于一个陌生的类库,我们一般要先阅读开发指南,然后检索接口和接口规范。如果开发指南让用户抓狂,你可以回顾一下开头讲到的跑步机的例子,想象下影响会有多糟糕!
|
||||
|
||||
那么合格的开发指南都要符合哪几个规则呢?我总结为三点:需要交代清楚概念,可以快速上手,示例都可操作。
|
||||
|
||||
**交代概念**
|
||||
|
||||
一个合格的开发指南,不要假定用户具有和开发者一样的知识范围。对应的接口规范和开发指南里涉及到的概念,一定要交代清楚。我们可以假定一个程序员了解IP地址这个概念,这是计算机入门的基本概念。但是,不要假定他了解IP地址的计算方式,虽然也是基础知识,但是大部分人记不住知识的细节。
|
||||
|
||||
所以说,交代清楚概念,很方便作者和读者之间建立共识,降低后续文档的阅读负担。
|
||||
|
||||
**快速上手**
|
||||
|
||||
一个好的开发指南,要尽最大可能,让开发者快速上手。
|
||||
|
||||
我们学习一门编程语言,往往从“Hello, World!”这个例子开始。它本身并没有太多玄妙的东西,但可以让一个初学者最快地玩耍起来,然后,再逐步探索更深入的内容。
|
||||
|
||||
这是一个值得学习的方法。很多开发指南,都有一个类似于“Hello, World!”这样的简短的快速入门章节。你也可以试试这个办法。
|
||||
|
||||
但需要注意的是,快速入门的章节一定要简单、靠前。让读者最快接触到,很容易学会,方便“玩耍”。
|
||||
|
||||
**示例都可操作**
|
||||
|
||||
可操作性是开发指南的一个命门。所有成文的方法和示例,都要求可以使用、可以操作、可以验证。虽然说起来简单,但是做到这一点并不简单。
|
||||
|
||||
开发指南,需要适应版本的变迁和代码的修改。第一版的开发指南也许看起来、用起来都很美妙,但到了第二版,就会面临代码和指南脱节的问题。
|
||||
|
||||
指南的方法是否还适用?示例的代码是否需要修改?示例的输出有没有变化?问题排查的办法有没有变更? 这些都是潜在的问题。
|
||||
|
||||
如果我们在开发指南里使用了不能执行的伪代码,这些问题就会更加突出。我们虽然清楚第一版伪代码的逻辑,却没有办法验证这样的逻辑在第二版是不是依然成立。
|
||||
|
||||
如果开发指南里的示例都可以被编译、被使用,那么解决起来就会轻松很多。修订开发指南时,如果我们把这些示例重新编译、运行、查看结果,然后对比文档,就可以解决掉很多潜在的忧患。
|
||||
|
||||
当然,可操作的开发指南,用户不仅仅是可以阅读,也可以照着例子玩耍。
|
||||
|
||||
## 小结
|
||||
|
||||
最后我来总结一下,用户指南的形式和组织方式各式各样,我们没有办法去限定它的具体格式。 一般而言,我们要建立下面的意识:
|
||||
|
||||
<li>
|
||||
从用户的角度出发来思考用户指南,用户指南要容易上手;
|
||||
</li>
|
||||
<li>
|
||||
用户指南和源代码一样,也有开发周期,也是需要维护的。
|
||||
</li>
|
||||
|
||||
## 一起来动手
|
||||
|
||||
今天,我们找两个真实的例子练练手。限于版权,我不能拷贝这两个例子的内容。麻烦大家去看原文。
|
||||
|
||||
第一个例子是腾讯微信的《小程序开发指南》第一小节的["Hello World"示例](https://developers.weixin.qq.com/ebook?action=get_post_info&docid=0002c8979d0678bb00862b8595b00a)。第二个例子是[GitHub页面开发的入门网页](https://pages.github.com/),也是一个Hello World示例。你仔细研究下,看看是不是都容易上手?每一个例子都有什么优点,有什么缺点? 你有哪些改进的建议?
|
||||
|
||||
欢迎在留言区分享你的看法,我们一起来讨论、学习。
|
||||
|
||||
如果你觉得这篇文章有所帮助,欢迎点击“请朋友读”,把它分享给你的朋友或者同事。
|
||||
|
||||
|
||||
274
极客时间专栏/代码精进之路/第一模块:代码“规范”篇/15 | 编写规范代码的检查清单.md
Normal file
274
极客时间专栏/代码精进之路/第一模块:代码“规范”篇/15 | 编写规范代码的检查清单.md
Normal file
@@ -0,0 +1,274 @@
|
||||
<audio id="audio" title="15 | 编写规范代码的检查清单" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/91/8e/91faa30a99fa8958edb4ad1c0690488e.mp3"></audio>
|
||||
|
||||
通过前面十几讲的学习,我们已经把代码“规范”篇的内容学习完了。今天,我们一起把前面讨论到的观点总结一下,并探索一下编写规范代码时的最佳实践检查清单。一份有效的检查清单,可以帮助我们记忆、遵循和执行代码的一系列规范。
|
||||
|
||||
## 标准与指南
|
||||
|
||||
在讨论编码规范之前,我们首先要认识什么是标准,什么是指南,以及它们各自的局限性。这样我们就能知道,什么可以做,什么不可以做。
|
||||
|
||||
标准是既定的做事方式,它定义了一个事物如何始终如一地达到同样水准的细节。标准的特征在于,它既是必需的,也是强制的。
|
||||
|
||||
然而,在现实实践中,能够标准化的东西是有限的。所以就有了指南,它是标准的补充。
|
||||
|
||||
指南告诉我们应该采取的总体方向和最佳实践,它是一个指导原则,是建议,不强制执行。用户可以根据具体情况,决定是否遵循相关的条款。
|
||||
|
||||
我们所说的编码规范,实际上通常是指导原则。虽然不具备强制性,但我们也有必要区分对不同建议条款的态度,比如对使用“强烈推荐”这样字眼的建议来说,我们就应该格外重视。这样,可以避免不必要的争执,降低复杂性。
|
||||
|
||||
## 为什么需要编码规范?
|
||||
|
||||
**1.提高编码的效率**
|
||||
|
||||
在不损害代码质量的前提下,效率可以节省我们的时间和成本。这种节省不仅仅停留在编码阶段,更体现在整个软件的生命周期里。我在第四篇已经详细解释了这一点。
|
||||
|
||||
**2.提高编码的质量**
|
||||
|
||||
代码的质量在于它和预期规范的一致性。一致、简单、规范的代码易于测试。相反,复杂的代码会加大测试的难度,难以达到合适的测试覆盖率。另外,代码的复用也意味着质量的复用,所以代码的问题会随着它的复用成倍地叠加,提高了软件的使用或者维护成本。
|
||||
|
||||
**3.降低维护的成本**
|
||||
|
||||
代码的维护要求代码必须能够修改,增加新功能,修复已知的问题。如果代码结构的清晰、易于阅读理解,那么问题就容易排查和定位。
|
||||
|
||||
**4.扩大代码的影响**
|
||||
|
||||
要想让更多的人参与,就需要一致的编码风格,恰当地使用文档。要方便他们阅读,便于解释。
|
||||
|
||||
使用编码规范可以让一个程序员减少出错,避免不必要的困扰。另外,编写规范的代码也是一种专业、成熟的体现。
|
||||
|
||||
## 编码规范的心理因素
|
||||
|
||||
编码风格最原始的依赖因素是人的行为习惯和心理学基础。通过了解一些基本的心理学原理,我们可以更好地理解编码风格的的基本规则。
|
||||
|
||||
- 两种思维模式
|
||||
|
||||
我们有两种思维模式,自主模式(快系统)和控制模式(慢系统)。自主模式的运行是无意识的、快速的、不怎么耗费脑力;控制模式需要集中注意力,耗费脑力,判断缓慢,如果注意力分散,思考就会中断。
|
||||
|
||||
自主模式在熟悉的环境中是精确的,所作出的短期预测是准确的,遇到挑战时,会第一时间做出反应。然而,它存在成见,容易把复杂问题简单化,在很多特定的情况下,容易犯系统性的错误。比如说,第一印象、以貌取人,就是自主模式遗留的问题。
|
||||
|
||||
当自主模式遇到麻烦时,控制模式就会出面解决,控制模式能够解决更复杂的问题。但刻意掌控会损耗大脑能量,而且很辛苦。处于控制模式中太长时间,人会很疲惫,丧失一部分动力,也就不愿意去做启动控制模式了。比如,很多人害怕数学,多是因为控制模式确实很吃力。
|
||||
|
||||
自主模式和控制模式的分工合作是高效的,损耗最小,效果最好。快速的、习惯性的决断交给勤快省力的自主模式,复杂的、意外的决断由耗时耗力的控制模式接管。
|
||||
|
||||
编码规范中很大一部分内容,是增加共识、减少意外,扩大自主思维模式覆盖的范围,减少控制模式必须参与的内容。熟练掌握编码规范可以逐渐让这一套规则存在于人们的下意识中,这样编码的时候就像我们使用筷子一样,简单又自然。
|
||||
|
||||
- 识别模式
|
||||
|
||||
我们能够在这个世界上存活下来,依靠的不是识别完整的情景,也不是从头开始分析每一个情景,而是通过既定的可识别模式,来匹配这个世界。正常期望以外的模式,常常会让我们感到吃惊和困惑,甚至是生气。
|
||||
|
||||
模式识别的认知是一柄双刃剑,既是福音也是祸害。它可以帮助我们毫不费力地使用经验,但习惯一旦养成就很难改变,我们不愿意打破旧模式,去学习新模式和接受新技术。
|
||||
|
||||
程序员很容易理解和自己编码风格类似的代码。如果编码风格和自己的大相径庭,就会感到焦躁和烦恼。编码风格一旦形成,就难以更改,转变很痛苦。幸运的是,一旦努力转换成新习惯,我们就会喜欢上新的编码风格。
|
||||
|
||||
一份好的编码规范,刚开始接受会有些困难。我们甚至会找很多借口去拒绝。但是,一旦接受下来,我们就成功地改进了我们的识别模式。
|
||||
|
||||
- 猜测模式
|
||||
|
||||
对于既定模式的识别,是通过猜测进行的。对于每一个新场景,大脑立即会把它起始部分当作一个线索,然后通过拟合所有已知模式的起始部分,来预测模式的其余部分,猜测“言外之意”。我们掌握的信息越少,就越有可能犯错误。比如,在医院看到穿白大褂的,我们默认他们是医护人员。但医护人员的判断标准并不是白大褂。
|
||||
|
||||
所以在编写代码时,我们要有意识地提供足够的线索和背景,使用清晰的结构,加快模式的识别,避免造成模式匹配过程中的模糊和混淆带来的理解障碍。
|
||||
|
||||
- 记忆模式
|
||||
|
||||
我们的记忆模式有四种,包括感官、短期、工作和长期记忆。
|
||||
|
||||
感官记忆是对我们感官体验的记忆,非常短暂(大约三秒钟),比如我们刚刚看到的和听到的。
|
||||
|
||||
短期记忆是我们可以回忆的,刚刚接触到的信息的短暂记忆。短期记忆很快,但是很不稳定,并且容量有限。如果中途分心,即便只是片刻,我们也容易忘记短期记忆的内容。
|
||||
|
||||
工作记忆是我们在处理认知任务时,对信息进行短暂存贮并且执行操作的记忆。工作记忆将短期记忆和长期记忆结合起来,处理想法和计划,帮助我们做出决策。
|
||||
|
||||
长期记忆涵盖的记忆范围从几天到几十年不等。为了成功学习,信息必须从感官或短期记忆转移到长期记忆中。和短期记忆相比,长期记忆记忆缓慢,但是保持长久,并且具有近乎无限的容量。
|
||||
|
||||
我们在组织代码时,不要让短期记忆超载,要使用短小的信息快,方便阅读;要适当分割需要长期记忆和短期记忆的内容,比如接口规范和代码实现,帮助读者在工作记忆和长期记忆中组织和归档信息。
|
||||
|
||||
- 眼睛的运动
|
||||
|
||||
当我们看一样东西的时候,我们不是一下子就能看清它的全貌。事实上,我们的眼睛一次只能专注于一个很小的区域,忽视该区域以外的内容。当然,我们可以意识到还有更大的区域,然后快速跳转到其他区域。
|
||||
|
||||
有时候,我们需要反复研读一段代码。如果这段代码可以在一个页面显示,我们的眼睛就很容易反复移动,寻找需要聚焦的目标。如果这段代码跨几个页面,阅读分析就要费力得多。
|
||||
|
||||
当我们阅读时,我们的眼睛习惯从左到右,从上到下移动,所以靠左的信息更容易被接受,而靠右的信息更容易被忽略。
|
||||
|
||||
但是,当我们快速阅读或者浏览特定内容时(比如搜索特定变量),眼睛就会只喜欢上下移动,迅速跳过。聚焦区域小,眼睛倾向于上下移动,这就是报纸版面使用窄的版面分割,而不是整幅页面的原因之一。
|
||||
|
||||
在编码排版时,要清晰分块,保持布局明朗,限制每行的长度,这样可以方便眼睛的聚焦和浏览。
|
||||
|
||||
## 编码规范的检查清单
|
||||
|
||||
下面的这个清单,是我看代码的时候,通常会使用的检查点。如果有检查点没有通过,阅读代码的时候,就要格外留意;编写代码的时候,还要想想有没有改进空间;评审代码的时候,要问清楚为什么这么做,给出改进的建议。
|
||||
|
||||
你也可以参考一下。
|
||||
|
||||
<li>
|
||||
代码是按照编码指南编写的吗?
|
||||
</li>
|
||||
<li>
|
||||
代码能够按照预期工作吗?
|
||||
</li>
|
||||
<li>
|
||||
文件是不是在合适的位置?
|
||||
</li>
|
||||
<li>
|
||||
支撑文档是不是充分?
|
||||
</li>
|
||||
<li>
|
||||
代码是不是易于阅读、易于理解?
|
||||
</li>
|
||||
<li>
|
||||
代码是不是易于测试和调试?
|
||||
</li>
|
||||
<li>
|
||||
有没有充分的测试,覆盖关键的逻辑和负面清单?
|
||||
</li>
|
||||
<li>
|
||||
名字是否遵守命名规范?
|
||||
</li>
|
||||
<li>
|
||||
名字是不是拼写正确、简单易懂?
|
||||
</li>
|
||||
<li>
|
||||
名字是不是有准确的意义?
|
||||
</li>
|
||||
<li>
|
||||
代码的分块是否恰当?
|
||||
</li>
|
||||
<li>
|
||||
代码的缩进是否清晰、整洁?
|
||||
</li>
|
||||
<li>
|
||||
有没有代码超出了每行字数的限制?
|
||||
</li>
|
||||
<li>
|
||||
代码的换行有没有引起混淆?
|
||||
</li>
|
||||
<li>
|
||||
每一行代码是不是只有一个行为?
|
||||
</li>
|
||||
<li>
|
||||
变量的声明是不是容易检索和识别?
|
||||
</li>
|
||||
<li>
|
||||
变量的初始化有没有遗漏?
|
||||
</li>
|
||||
<li>
|
||||
括号的使用是不是一致、清晰?
|
||||
</li>
|
||||
<li>
|
||||
源代码的组织结构是不是一致?
|
||||
</li>
|
||||
<li>
|
||||
版权信息的日期有没有变更成最近修改日期?
|
||||
</li>
|
||||
<li>
|
||||
限定词的使用是不是遵循既定的顺序?
|
||||
</li>
|
||||
<li>
|
||||
有没有注释掉的代码?
|
||||
</li>
|
||||
<li>
|
||||
有没有执行不到的代码?
|
||||
</li>
|
||||
<li>
|
||||
有没有可以复用的冗余代码?
|
||||
</li>
|
||||
<li>
|
||||
复杂的表达式能不能拆解成简单的代码块?
|
||||
</li>
|
||||
<li>
|
||||
代码有没有充分的注释?
|
||||
</li>
|
||||
<li>
|
||||
注释是不是准确、必要、清晰?
|
||||
</li>
|
||||
<li>
|
||||
不同类型的注释内容,注释的风格是不是统一?
|
||||
</li>
|
||||
<li>
|
||||
有没有使用废弃的接口?
|
||||
</li>
|
||||
<li>
|
||||
能不能替换掉废弃的接口?
|
||||
</li>
|
||||
<li>
|
||||
不再推荐使用的接口,是否可以今早废弃?
|
||||
</li>
|
||||
<li>
|
||||
继承的方法,有没有使用Override注解?
|
||||
</li>
|
||||
<li>
|
||||
有没有使用异常机制处理正常的业务逻辑?
|
||||
</li>
|
||||
<li>
|
||||
异常类的使用是不是准确?
|
||||
</li>
|
||||
<li>
|
||||
异常的描述是不是清晰?
|
||||
</li>
|
||||
<li>
|
||||
是不是需要转换异常的场景?
|
||||
</li>
|
||||
<li>
|
||||
转换异常场景,是不是需要保留原异常信息?
|
||||
</li>
|
||||
<li>
|
||||
有没有不应该被吞噬的异常?
|
||||
</li>
|
||||
<li>
|
||||
外部接口和内部实现有没有区分隔离?
|
||||
</li>
|
||||
<li>
|
||||
接口规范描述是不是准确、清晰?
|
||||
</li>
|
||||
<li>
|
||||
接口规范有没有描述返回值?
|
||||
</li>
|
||||
<li>
|
||||
接口规范有没有描述运行时异常?
|
||||
</li>
|
||||
<li>
|
||||
接口规范有没有描述检查型异常?
|
||||
</li>
|
||||
<li>
|
||||
接口规范有没有描述指定参数范围?
|
||||
</li>
|
||||
<li>
|
||||
接口规范有没有描述边界条件?
|
||||
</li>
|
||||
<li>
|
||||
接口规范有没有描述极端状况?
|
||||
</li>
|
||||
<li>
|
||||
接口规范的起草或者变更有没有通过审阅?
|
||||
</li>
|
||||
<li>
|
||||
接口规范需不需要标明起始版本号?
|
||||
</li>
|
||||
<li>
|
||||
产品设计是不是方便用户使用?
|
||||
</li>
|
||||
<li>
|
||||
用户指南能不能快速上手?
|
||||
</li>
|
||||
<li>
|
||||
用户指南的示例是不是可操作?
|
||||
</li>
|
||||
<li>
|
||||
用户指南和软件代码是不是保持一致?
|
||||
</li>
|
||||
|
||||
## 小结
|
||||
|
||||
虽然说编码规范不是强制性的标准,但是如果你能尽可能遵守相同的规范,就会让工作更简单、更高效。
|
||||
|
||||
需要特别强调的是,认为写好代码只有一种方法是愚蠢的。虽然编码规范的指导原则是通用的,但是其中的具体细节则依赖于具体的环境,因具体的需求而变化。所以,除了遵循编码规范外,你还要做好随时重审、调整编码规范的准备,保持编码规范的活力,跟得上实际情况的变化。
|
||||
|
||||
希望你根据自己的实际情况,不断完善、丰富上面的清单,使这份清单更直观、更容易遵循,保持长久的活力,让它更适合你自己。
|
||||
|
||||
## 一起来动手
|
||||
|
||||
正像我们讨论到的,不同的场景,检查清单也不一定相同。如果让你列一个你自己实际工作中需要的编码规范检查清单,会是什么样子的? 你可以在我上面的清单上加减检查点,或者新做一个列表。欢迎在留言区公布你的检查清单,我们一起来讨论、学习。
|
||||
|
||||
另外,推荐一本书《清单革命》。清单能够起到的作用,常常被忽视。这本书告诉我们清单这个小东西,能给我们的工作带来多么巨大的帮助。
|
||||
|
||||
如果你觉得这篇文章有所帮助,欢迎点击“请朋友读”,把它分享给你的朋友或者同事。
|
||||
|
||||
|
||||
72
极客时间专栏/代码精进之路/第一模块:代码“规范”篇/16丨代码“规范”篇用户答疑.md
Normal file
72
极客时间专栏/代码精进之路/第一模块:代码“规范”篇/16丨代码“规范”篇用户答疑.md
Normal file
@@ -0,0 +1,72 @@
|
||||
<audio id="audio" title="16丨代码“规范”篇用户答疑" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/82/cf/82d412f26635fb7622b3e1932660cfcf.mp3"></audio>
|
||||
|
||||
更到这一篇的时候,意味着专栏第一模块“代码规范”篇已经更新完毕了。在更新的时候,非常感谢同学的积极踊跃地留言,提出了很多独到的见解,也给专栏增色了许多。
|
||||
|
||||
今天,我来解答一下留言里的一些疑问。其实有很多问题,我们已经在留言区里讨论了。这里,我们就挑选一些还没有解决掉的问题,深入讨论一下。
|
||||
|
||||
@大於一:回归测试其实怎么测? 不懂。
|
||||
|
||||
答:InfoQ有一篇文章《[回归测试策略概览](https://www.infoq.cn/article/regression-testing-strategies)》我觉得写得不错。你可以找来看看。
|
||||
|
||||
我想要补充的一点是,要想发挥回归测试的最大作用,要把回归测试自动化。只需要简单的操作,就可以启动回归测试。比如使用“make test”命令行,或者其他集成工具的触发配置。这样,我们做的每一个更改,哪怕只是修改了一行代码,都可以跑一遍回归测试。
|
||||
|
||||
@一一:高质量的代码,依赖于高质量的流水线,那么问题来了,如何建立中小企业的高质量的代码流水线呢?
|
||||
|
||||
答:回答这个问题之前,我们先看看一些公开的数据。
|
||||
|
||||
Java SE的安全,是由OpenJDK的Security组负责的,评审一般通过Security-Dev@Openjdk.java.net的邮件列表进行。根据OpenJDK的登记数据,Security组总共有过13人,其中包括离职的,退休的。现存的团队,也就八九个人的样子。
|
||||
|
||||
这八九个人要做多少东西呢?下面是一个不完全的简要的列表:
|
||||
|
||||
1. Java平台安全(platform security, Java language)
|
||||
1. 密码学 (Cryptography,JCA)
|
||||
1. 认证和授权(Authentication and Access Control,JAAS)
|
||||
1. 安全通信(Secure Communications,JSSE/JGSS/SASL)
|
||||
1. 公开密钥基础设施(Public Key Infrastructure (PKI))
|
||||
|
||||
其中任何一个模块,如果没有多年的经验积累,都很难做好。如果你观察Security-Dev邮件列表,就会注意到每一个模块的话题,参与活跃的也就两三人。这就是说,如果把一个模块看作一个虚拟的团队,这个团队也就两三人而已。
|
||||
|
||||
我想通过这个例子告诉你,大公司的研发团队,也可以是一个小团队;团队里的成员,也不是什么都懂的。
|
||||
|
||||
这样的团队和中小企业的研发团队有什么区别呢?我觉得数量上的区别可能不是很大,两三人的团队,八九人的团队,中小企业的团队规模大抵也是这样的,甚至更大。质量上的差别呢?我觉得这没有可比性,你的团队不一定需要密码学的专家。能够满足产品目标的团队就是一个质量好的团队。
|
||||
|
||||
我们来看看我们在专栏里提到的流水线,有哪一道关卡是中小企业做不了的?其实都做得了,但是没有养成这么做事的习惯。
|
||||
|
||||
首先,程序员是最重要的关卡,决定着整个流水线的运转效率。专栏里有一篇文章,我们讨论了优秀的程序员是什么样的。我们要做的就是,让自己成为这样的程序员,找到这样的程序员做同事,帮助同事成为这样的程序员。建立这样的氛围确实不容易。比如,我们讨论的编写代码允许犯错误,允许反复地犯错误,这一点很多中小企业很难做到。杀一个程序员祭天,简单粗暴,还能发泄愤怒,大家都喜欢这样的方式。有时候,要求程序员有责任心,有主人翁意识,要知错就改,就是一种看起来正确无比,其实没有什么意义的做法。
|
||||
|
||||
在编译器这道关中,其实我们大都使用相同的编译器。区别可能在于我们对于编译器警告的处理态度。我们可以忽视编译器警告,也可以认真分析每一个编译器警告,甚至消除每一个编译器警告。这样做,谁不会花费很多时间呢?刚开始,一个程序员写的代码,可能有很多警告,然后他试图弄清楚这些警告,消除掉这些警告。通过这个过程,他成为一个更好的程序员,下一次写代码,就不会出现这么多警告了。随着他越来越清楚警告的来源和解决办法,他的程序越来越好,花的时间越来越少。如果他把所有的警告都忽视掉,无论多长时间,他都掌握不了解决这些警告的方法,无法保证代码质量。我们小时候,算1加2等于几,都要掰掰手指头。长大后,这样的计算,根本都不需要思考。每个程序员,都有掰着手指头学习的阶段。这一点,我觉得大企业和小企业,没有太大的区别。
|
||||
|
||||
回归测试这道关,其实就是把研发流程中的测试,规范化出来。我们写程序,都要测试,都要有测试代码。把测试代码独立出来,形成测试案例,然后每次代码变更时,把所有测试案例都跑一遍,就是回归测试了。如果回归测试通不过,就说明代码变更可能有潜在的问题,需要修改。这里面的难点,就是要坚持写测试代码。这些代码,测试人员要写,研发人员也要写。如果写测试代码是一个硬条件的话,一个公司就不能寄希望于程序员的责任心这种幻象。更有效的做法是,如果一个变更没有测试代码,就不允许提交代码。如果允许意外,程序员必须清楚地解释为什么没有测试代码。比起写个测试代码,解释起来大都是很费劲的。这个小制度的安排,就可以帮助我们养成写测试代码的好习惯。习惯了之后,我们很快就会发现,写测试代码,使用回归测试,其实是帮助我们节省时间、减少错误的。这时候,我们一般就会喜欢上写测试代码了。这一点,我觉得大企业和小企业,没有太大的区别。
|
||||
|
||||
代码评审这道关,其实是误解最深的一道关。第一个误解是,公司没有牛人,没办法评审。其实,评审做重要的,就是多几双眼睛看着,防范我们个人难以避免的错误。不是说做评审的,就一定要比被评审的专业、见识多。即便是刚毕业的大学生,也能够看到我们自己无法完全克服的错误,比如说我们在文章里提到的"goto fail"错误。在OpenJDK社区,谁都可以评审,谁都可以发表不同的意见,提出改进的建议。如果评审依赖牛人,牛人就是最大的瓶颈。第二个误解是,代码评审太浪费时间,耽误功夫。代码评审,确实需要付出时间。可是这只是表象,代码评审可以减少错误,提高代码质量,减少代码返工,帮助积累经验,这些都是节省时间的。如果我们看的角度稍微大一点,就会有不同的结论。这一关的难点,就是要坚持代码评审。这同样不能依赖于我们脆弱的责任心和主人翁精神,而要依靠小制度的安排。比如没有经过评审的代码,就不能提交代码。这一点,我觉得大企业和小企业,没有太大的区别。
|
||||
|
||||
代码分析这道关,其实相对来说,比较简单。找到相关的工具,坚持定期检测,检查分析结果就行了。没什么玄妙的东西。这一点,我觉得大企业和小企业,也没有太大的区别。
|
||||
|
||||
我们该怎么把这个流水线搭建起来呢?我认为最重要的就是要启动代码评审这个环节。这个环节启动了,其他的环节,顺势就建立起来了。这个环节没有,其他环节也不能很好地执行。
|
||||
|
||||
1. 使用Git或者Mercurial这样成熟的版本控制工具,以及像Bugzilla,Phabricator,Jira这样的bug管理工具。
|
||||
1. 找一个工具,可以生成可视化的代码变更页面。比如OpenJDK的webrev,或者像 Phabricator这样的集成工具。
|
||||
1. 找到一个集中的地方,可以让所有人都看到代码变更页面,都可以方便地提意见。比如,OpenJDK使用cr.openjdk.java.net展示,使用邮件列表讨论。GitHub和Phabricator这样的集成工具也有类似展示窗口。
|
||||
1. 制定小制度,没有经过评审的代码,不能提交。OpenJDK的提交,是用过检查Reviewed-by这样的提交描述字段,来自动判断的。Phabricator这样的集成工具,也有类似的强制设置。
|
||||
1. 制定代码评审的通过准则,比如不规范的代码,没有通过测试的代码,以及没有测试代码的变更,不能提交。如果允许例外,提交者要解释清楚。
|
||||
1. 把测试归拢起来,找一个自动化的工具,可以执行回顾测试。比如说使用“make test”命令行,就可以执行所有的回归测试。类似Phabricator这样的集成工具,也有类似的测试执行功能。
|
||||
1. 定期执行代码分析,并且把分析结果记录下来,检查、分析、改进。这个分析工具可以是SpotBugs,或者其他的商业软件。
|
||||
1. 把需求分析、架构设计、接口设计,也当作bug管理,纳入到评审的过程中来。
|
||||
1. 改进激励标准。程序员的评价标准不要狭隘在编写了多少行代码上,还要看看参与了多少评审,发现了多少问题。
|
||||
1. 鼓励内部推广软件开发经验,比如说什么样的插件可以编写规范的代码,什么样的代码容易出现安全问题,什么样的代码效率比较高。有了成熟的经验后,就总结下来,推广出去,形成团队的重要共识和财富。
|
||||
|
||||
这些工具用起来,各个流程衔接起来,就是一个可以运转的流水线了。随着大家越来越习惯这样的工作方式,流水线的效率就会越来越高。而且流水线本身就是一个好老师,可以促进交流,加快每个参与者的成长。当然,从无到有,去适应这个流水线需要一段时间。
|
||||
|
||||
如果对比大公司和小公司,也许有没有现成的流程,算是区别吧。由于丰富的开源软件以及云服务,工具本身的区别,其实很小了。
|
||||
|
||||
拎着一挺AK47去战斗,总不如赤手空拳、手抓子弹来得陶醉,更别提使用飞机大炮了。我们尽可以耻笑电视里的画面,“拿着手枪,算什么英雄!” 但是轮到我们自己去战斗时,最好有飞机大炮航空母舰。
|
||||
|
||||
@轻歌赋:想问问老师,如何在身边没有其他评审的情况下,提供一些自己检查代码逻辑bug的方法呢?而且对业务分析不熟悉,经常会出现建表少了某个字段的情况,请问老师有没有什么相对系统化的设计方面的知识可以学习呢?
|
||||
|
||||
答:对于业务分析,你可以参考下使用UML或者思维导图这样的工具。画画图,有助于我们加深对业务的理解,找出业务的逻辑甚至问题。我自己做需求分析的入门书籍,是《软件工程》(Roger S. Pressman或者Ian Sommerville的,我忘记自己当初学的是那一本了)和《软件需求》(Karl E. Wiegers)。
|
||||
|
||||
我们应该都过了面向对象设计的门槛了,有三本书,可以让我们接触优秀的设计理念和最佳时间。一本是《Unix程序设计艺术》(Eric S. Raymond)。另一本是《设计模式》(Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides)。学设计模式,千万不要固化了思维,所以我建议一定要仔细阅读每一个设计模式的“意图”“动机”“适用性”这几方面的内容。另外一本书,就是《Effective Java》(Joshua Bloch),Java程序员应该人手一本,像是使用字典一样使用这本书,先看一遍,然后时刻备查。
|
||||
|
||||
以上就是答疑篇的内容。从下一篇文章起,我们就要开始“代码经济篇”的学习了。如果你觉得这篇文章对你有所帮助,欢迎点击“请朋友读”,把这篇文章分享给你的朋友或者同事。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user