代码质量随想录(五)注得多不如注得巧
写代码也流行注水了么?不是不是,我说的是注释。其实注释这个东西,历史久远。我们可以宽泛一点儿说,《春秋》就是要配上左传的注解,才能兴发其“微言大义”嘛!注释有很多种,如果按照注释者与原文作者是不是同一个人来分,可以划分成自注和他注。在程序员这个行当内,一般来说,还是自注多一些,自己写代码,自己加注。有的时候进行代码审查或者复用遗留代码时,才可能会有必要对他人写的代码加注。
从代码质量的角度看,注释写得应不应该,写得好不好,应该从它是否有助于加深代码读者及代码使用者对程序的理解这一标准来判断。按照《The Art of Readable Code》作者的说法,注释的目标,就是让读者尽量明白代码作者的编程意图。
那么,具体到代码书写层面,究竟怎么注释才算好呢?这个问题得展开来谈。这一篇文章先谈谈注释的时机问题,下一篇再来研究注释的内容。
1.显而易见的代码别注释
写注释经常会遭遇两种极端态度,一种是绝对不写注释,一种是写废话连篇的注释。对于持第一种态度的人,小翔希望看完讲注释的这两篇文章之后,能够适当转变一下态度,稍稍缓释惜墨如金的执念,多为大家带来一些精彩的注释。有很多理由都会被拿来为不写注释做辩护,这在后文会一一讲到,我在这里主要是想先说说口水型注释的害处。从我个人的工作经历来看,不写注释的人一旦能够理性地认识到注释的好处,那么他们很有可能养成在编码的同时自发地为代码精准加注的好习惯,然而没话找话型的程序员,则很难写出优雅简洁的注释来,对这些人来说,先要消解注释泡沫才行。
比如,代码本身就含有的题中之义就不宜再以注释的形式重复了。
- // Account类的定义。
- class Account {
- // 构造器
- public Account(){...}
- // 将profit字段设定为新指定的值
- public void setProfit(double profit){...}
- // 获取本Account对象的profit字段值
- public double getProfit(){...}
- }
以上几行注释的内容完全是在重述代码,意义不大。
2.注释要尽量阐发被注标识符无法容纳的意思,比如操作的同步性、工作流程、参数的范围、返回值、异常等有价值的信息
形成上例这种情况,也许还有一个原因,那就是有些公司或者团队会对注释形成一种强制要求,比如在Java语言中要求公有和保护级别的API必须写Javadoc。这种规范是好的,不过要定出具体细则来,比如类的总结部分怎么写,构建子怎么写注释,简单的setter/getter方法怎么写注释。
针对上述这些问题,我觉得在制定开发团队的注释规范时,要明确指出:注释应该尽量阐明被注标识符无法容纳的义涵。例如,针对本类字段的简单存取方法,如果其中有特殊之处,比如setter方法参数的取值范围、参数非法时是否会造成异常、设置的新值是否立刻生效等等问题,那么这些情况就应当明确标注。例如:
- /**
- * 将profit字段设定为新指定的值。设置动作有可能不会立即生效,要根据该账户对象的修改
- * 策略所允许的单位时段内最大修改次数来定。如果修改策略是“延时生效”,则超过修改次数
- * 限制的修改动作会在下个时间段生效.
- * @param profit 新的收益率,必须在[0.0d, 1.0d]之间
- * @throws IllegalArgumentException 如果收益率不在合法区间内
- * @throws IllegalOperationException 如果本次设置已在修改策略容许次数之外,
- * 且修改策略是“立即生效”
- */
- public void setProfit(double profit){...}
虽然有点儿啰嗦(我写注释的毛病,哈哈),不过比起上例来说,毕竟还是带来了一些新内容。而且一旦通过注释把这些隐晦的东西挑明了,那么还可以由此引发新的讨论,以促进团队成员对代码的理解,进而触发重构。比如大家可以尽情吐槽:这个方法名怎么能简简单单地叫成setProfit呢?这样怎么能体现出它还受制于“账户修改策略”这个事实?参数怎么能叫成profit?为什么不写成profitBetweenZeroAndOne?如果设置无法立刻生效的话,那为什么不提供通知机制?不然客户代码怎么知道什么时候才能设置生效?等等等等……这些质疑未必各个都有道理,不过可以由此让我们重新审视该方法,甚至是整个类,看看它设计得是不是有问题,对下游开发者是否友好。
再看getProfit方法,可就有点儿尴尬了,因为不管怎么写注释,貌似都很无力。这时咱们就可以很有自信地无视它了。不过使用Eclipse的开发者可能会遇到一些小障碍,比如在设定里面设置好了强制要求所有protected、public的API都要写Javadoc注释,那么略去这种getProfit方法不注,可能会有警告或者错误。这种小麻烦,恐怕就需要一些变通办法了,大家如果有好办法,也请告诉我。
如果代码读者和下游开发者有必要适当地瞭解工作流程和返回值详情,那么这些信息就要注释,比如:
- // 在子树中寻找某个深度范围内,具有给定名称的节点。
- public Node findNodeInSubtree(Node subtree, string name, int depth){...}
就应该改为:
- // 找寻具有指定名称的节点,找不到则返回null。
- // 如果深度值小于等于0,则整个子树都将被查找。
- // 如果大于0,则只在N级深度范围内查找。
- public Node findNodeInSubtree(Node subtree, string name, int depth){...}
3.如果编程意图不够明显,则可以适当地加些注释。此种情况的根本解决办法还是通过重构来理顺复杂的代码,使之清晰、直观
- # 移除第二个'*'字符及其后内容
- name = '*'.join(line.split('*')[:2])
ARC作者可能认为以上这句大家看到之后第一眼有点搞不清楚状况,所以建议加上那行注释。小翔倒是觉得,不妨对上面的代码进行重构,将“切割、数组切片、拼合”这个大操作拆解成三个小操作,并且封装起来,这样更符合迪米特原则(又叫得墨忒耳定律、最少知识原则),而且看上去代码会更加清晰,不需加注即可明白。
- String name=truncateFromDelimiter(line,'*',2);
- ...
- private String truncateFromDelimiter(String input, char delimiter,
- int groupIndexToDropFrom){...}
4.再好的注释也无法彻底掩饰坏名称
- // 确保回覆对象的内容符合请求对象中关于条目数量、总字节数等规格的限定。
- public void cleanReply(Request request, Reply reply){...}
以上注释中的“确保”(Enforce)、“限定”(Limit)等词应该直接纳入方法名称中。不妨改成:
- // 经请求对象所限定的规格包括“条目数量”、“总字节数”等指标。
- public void enforceLimitsFromRequest(Request request, Reply reply){...}
这样不仅注释内容变简单了,而且方法名称所表达的意思也比原来精确许多,让人更易理解。关于这一点,我在做项目时体会特别深刻,千万不要试图用注释去粉饰糟糕的名字,而应该直接修正不当的命名。
- // 释放主键所指向的注册表操作句柄。该方法并不修改实际的注册表内容。
- public void deleteRegistry(RegistryKey key);
既然“并不修改实际的注册表内容”,那么名称中delete何谓?用注释无法掩饰这个矛盾。莫如去掉注释,直书其意,这样不需要注释大家也能从方法名称中准确判断出该操作的效果仅仅是释放句柄:
- public void releaseRegistryHandle(RegistryKey key);
5.能够对代码读者起到警示、启发或备忘作用的注释值得去写
有时需要警告同组开发者,不要进行仓促的优化:
- // 在处理该数据时,使用二叉树比哈希表快40%,计算哈希码的开销比进行左右比较的开销要大。
有时则要避免开发者在无关紧要的问题上浪费时间:
- // 这种试探法可能会漏掉一些词语,不过不影响使用,100%解决这个问题很难。
有时陈述将来可改观之处:
- // 这个类很乱,也许应该创建一个ResourceNode子类来下移一部分代码。
- // TODO:应该使用更快的算法
有时要陈述不完备的功能:
- // TODO: 除了JPEG之外,还得处理其他格式。
上述最后两种情况要特别注意,也就是在注释待改进或者功能不完备的代码时,强烈建议使用特殊的前导标识符来标明注释行。这样可以藉助文本统计或者IDE提供的待办任务视图来立刻检索到项目中存在的隐患,促进开发者之间对代码现状的理解,以便发现问题及时沟通。这种注释其实扮演了“待办任务”或“待办事项”的角色。咱们业内通用的标注法按照紧急程度从低到高排列如下,新入行的小朋友们可以学习一下:
- // TODO: 可改观或不完备的功能。
- // HACK: 用来应急的杂技代码,稍后必须纠正。
- // FIXME: 代码有错,需要修正。
- // XXX: 代码大误,即行修正!
6.关乎代码逻辑的常量,如其名称不足以描述其包含的重要信息,则必须加注
必须具备某种特性,方能使程序正常运转的常量应该加注,例如:
- /** 只要不小于处理器数量的2倍就好. */
- public static final int NUM_THREADS = 8;
翔按:ARC作者在说明此种情况应当加注时,举了上面这个例子。其实,这里不妨补以// TODO: 提示信息,因为这种“不小于处理器数量的2倍”的特性可能会随着运行环境的改变而无法满足。仅凭这个注释,程序员未必能在出问题时第一时间就定位到该常量。大家可以在遇到这种情况时,补以提示性注释,例如“// TODO: 在后续版本改进过程中,应使用系统硬件信息来初始化此常量值,不宜手工指定”。
随意选取数值的限定常量亦应加注,以便后续版本要对其进行可定制的功能扩展时参考(注意TODO后面的话):
- // TODO: 如果将来要由客户自行指定订阅点上限,则可把此值改为变量。
- /** 最大的RSS订阅点数量。这么多订阅点足以应对客户当前的需求了. */
- public static final int MAX_RSS_SUBSCRIPTIONS = 1000;
精心调优后的常量应加注,避免误调:
- // 使用0.72作为质量参数,可以在画质与占用空间之间取得良好平衡。
- public static final double IMAGE_QUALITY = 0.72d;
其实这一条原则的三个小分支,都与上一条所述的“能够对代码读者起到警示、启发或备忘作用的注释值得去写”这一原则有重复。之所以要单列出来,是因为常量的设置尤为微妙,经常会暗含无法用标识符全面涵盖的细微特征,应当适时地辅以注释。
7.提高注释质量所奉行的原则之一与提高代码质量的大原则一致:用局外人的视点来审读代码
这一点,我在日常编码中曾一再对身边同事强调,此时不妨再啰嗦几句。那就是要从当前代码中跳出来,“冷眼看程序,热心挑毛病”。
大部分人不甚明瞭的微妙语言细节应该加注,例如:
- struct Recorder {
- vector data;
- ...
- void Clear() {
- vector().swap(data);
- }
- };
如果谁突然闯进来看到上面的代码,肯定第一个就要问:为什么不直接调用data.clean()函数呢?与其让读者陷入猜测与不解之中,咱们不如直接用注释把隐晦的细节说明白了:
- // 在vector对象上进行强制内存回收,参见“STL容器的swap技巧”(STL swap trick)
- vector().swap(data);
好久没做C++的项目了,刚Google了一下,这个技巧问的人还蛮多,我想起当时Scott Meyers在《Effective STL》一书里面讲过,Stack Overflow上面有人说是条目17,大家可以去复习一下。我觉得,如果真是像本例这种情况,某段代码使用了一个不成文的高端技巧或者某权威著作中深入讲述的代码惯用法,那么不如在注释中直接给出明确的参考源,例如“参阅网址:……;参考书目或文章:……”。
可能会导致客户代码出状况的API要加注。例如:
- // 调用外部程序投递邮件(有可能耗时长达1分钟,若届时还未完成,则算超时)
- public void sendEmail(String to, String subject, String body){...}
- // 算法时间复杂度是O(标签数量*平均标签深度),若输入数据含有大量嵌套错误,可能相当耗时。
- public void fixBrokenHtml(String html){...}
类之间的互动、整个系统数据流、程序的入口点等宏观信息应该加注。讲到这个问题时,ARC的作者让我们假想一下,如果某个程序狼(或者程序娘,原文按照英语惯例,写的是her)突然闯入团队里面,你怎么以代码的方式向他解释整个项目的架构,使他尽速融入开发过程中呢?这个时候就必须有一些全局性的注释了,通过阅读这些注释,新人就可以迅速把握住整个项目的大方向、大节奏。例如:
- // 在业务逻辑与数据库层之间的粘合代码,应用程序不直接使用它。
- // 该类内部逻辑稍显复杂,不过仅仅扮演智能缓存池的角色。它并不依赖于系统的其他部分。
在Java项目中,我们通常以包注释或类概览的Javadoc形式来提供宏观注释。
- /**
- * 为便于访问与文件操作有关的功能而提供的工具类。
- * 其内部会处理与操作权限等事项相关的细节问题。
- */
- public class FileMiscellaneousUtility{...}
8.以注释将长段代码分为小段,使读者快速掌握程序流程
在上一篇文章中举过一个类似的例子,那次是编写一个社交软件中的潜在友人推荐功能。那个例子其实只有8行有效代码。所以只需分段,不用注释,读者就可以清晰地理解它。然而有的时候,如果某方法内部包含数十甚至上百行代码,而因为效率或复杂度等原因无法立刻进行代码整理的话,那么可以先写一些注释来厘清程序流程,这样也便于后续的维护。例如:
- public void generateUserReport{
- // 获取配给该用户的锁
- ...
- // 从数据源读入用户信息
- ...
- // 将信息写入文件
- ...
- // 释放用户锁
- ...
- }
本来上述方法的四段应该分别被重构提取到四个不同的小方法之内,不过如果由于内部逻辑过于复杂,提取小方法的时候需要提取过多的参数以配合程序流程,那么在短期内无法进行有效重构的情况下,方法内部的适当注释可以起到“起、承、转、合”之目的,也可以为稍后进行重构的人厘清思路。
嗯,这一篇讲的心得有点多,可以小小总结一下。有一种传统的说法,那就是“只注释写代码的原因(why),不要注释代码具体内容(what)以及代码的算法(how)”。不过看了上述这些例子之后,我想大家应该明白,有些时候,代码的具体细节以及算法等内容,如果与代码的理解紧密相关,那么就应该毫不吝惜地注释。
巧妙的注释,好就好在它能促进代码理解这一点上。不仅能让读者快速抓住代码的意图,而且还能为将来潜在的重构打开思路,同时还利于项目的维护,再有就是方便下游开发者进行二次开发。相反,对代码理解毫无益处的注释,就显得笨拙、累赘,应该删去。所以嘛,我想大家可以稍微修正一下上述说法了:只要有助于代码的理解,“做什么、为什么做、怎么做“这几方面都应加注。
最后说一个小问题,那就是“注释恐惧症”。本文开头说道,有些人不愿意写注释,原因有很多种。其中有一种就是注释恐惧症,一旦形成这个习惯,同时又没有督促因素的话,则很难改正。此时如果通过团队注释规范强迫开发者去写注释的话,那么在没有养成良好注释习惯的情况下,就很可能会立刻走入另一个极端,为了应付差事而写出毫无意义甚至刻意掩盖代码隐患的注释来。对于如何克服注释恐惧症的问题,ARC的作者说了一个方法,我转述给大家听听。他们二位建议,将自己的第一感觉以“原生态”的方式写出来,例如:
- // 额滴神啊,如果列表中有重复元素的话,这家伙就玩儿不转了。
- // (其实,ARC这本书的原文是这样的:)
- // Oh crap, this stuff will get tricky
- // if there are ever duplicates in this list.
上面这种话我估计人人都会写吧。好,写完了之后,用具体的、精确的词语代替模糊的、情绪化的描述。
- “额滴神啊”这几个字,其实是想说“这里有必须要注意的状况发生”。
- “这家伙”其实指的是“处理输入数据的代码”。
- “玩儿不转了”意思是“这种情况下的算法很难实现”。
所以,上述注释经过美化之后,就变成了:
- // 注意:这段代码并不能处理含有重复元素的列表,因为那种情况下的算法太难实现了。
- // (ARC的原文是:)
- // Careful: this code doesn't handle duplicates in the list
- // (because that's hard to do)
不知道上面这个顽皮搞笑的过程能不能克服注释恐惧症,如果不能的话,大家也可以跟帖想想办法。
这段时间一直没有写文章,一来由于工作繁忙,二来是晚上想贪玩看看比赛,三嘛,你别说,还真有可能是写作恐惧症呢!其实这更像是写作倦怠症。好了,不管怎么说,这次写开了,就不倦怠了。这一篇讲的是注释的时机问题,也就是什么时候应该注释,什么时候不该注释,下一篇来讲讲内容问题,也就是说,如果要写注释的话,怎么写才算好。
爱飞翔
2012年6月16日至17日
本文使用Creative Commons BY-NC-ND 3.0协议(创作共用 自由转载-保持署名-非商业使用-禁止衍生)发布。
原文网址:http://agilemobidev.net/eastarlee/code-quality/think_in_code_quality_5_judicious_comments_zh_cn/
转载于:https://blog.51cto.com/eastarlee/901168
代码质量随想录(五)注得多不如注得巧相关推荐
- 代码质量随想录(四):排版,不只是为了漂亮
代码质量随想录(四):排版,不只是为了漂亮 作者: 爱飞翔 发布时间: 2012-06-15 21:18 阅读: 1433 次 原文链接 全屏阅读 [收藏] 写了前三篇(一.二.三)之后 ...
- 代码质量随想录(四)排版,不只是为了漂亮
写了前三篇之后,发现比我预想的效果要好.关注代码质量的朋友还蛮多的,而且很多意见和建议也很有益,指出了我文章中的一些问题. 我这种家庭妇男型的自由职业者来说,在平常写代码的时候可以多停下来,思考一些代 ...
- 代码质量检测-SonarQube
文章目录 前言 一.SonarQube是什么? 二.SonarQube安装步骤 1.docker安装 2.docker-compose安装 3. 访问SonarQube 4. 配置SonarQube ...
- 五个有用的工具帮助您提高代码质量
前言 对于开发人员而言,代码质量一直是一个非常重要的话题.高质量的代码不仅可以提高应用程序的性能,还可以减少代码错误和维护成本.然而,如何确保代码质量呢?下面介绍五个有用的工具,可以帮助您提高代码质量 ...
- java双色球机选五注代码
java双色球机选五注代码 从小到大没买过彩票,准备尝试下人生第一次. 看了下双色球规则,自己写了个小代码. 主体代码如下: for (int i=0;i<5;i++) { Set set = ...
- 代码质量与规范,那些年你欠下的技术债
提到"质量"二字时,我们的第一反应往往是"有多少BUG?""性能好不好?"这样的问题.我们对软件产品或服务的质量定义看其能不能满足用户的需求 ...
- linux搭建SonarQube代码质量平台_Oracle 最新详细版本
文章目录 一.最低配置要求 1. JDK版本要求 2. 数据库版本要求 3. 支持浏览器版本 二.软件下载安装 2.1. 软件列表总览 2.2. jdk11下载 2.3. sonarqube下载 2. ...
- 代码质量度量标准_Google研发度量改进实践
Google改进过程: 本文案例源自:<Measuring Engineering Productivity> 作者:Ciera Jaspen,Google 前言 随着敏捷开发.DevOp ...
- ISO/IEC 5055:软件代码质量的标尺
本文分享自华为云社区<自动源代码质量度量(ISO/IEC 5055)>,原文作者:Uncle_Tom . ISO 5055是首个直接从软件内部结构方面衡量软件质量(如安全性和可靠性)的IS ...
最新文章
- 分布式存储 Ceph 介绍及原理架构分享--云平台技术栈系列01
- 制药行业SAP项目里的那些LIMS系统
- SpringBoot从介绍到各个框架的整合
- 函数不可访问_关于可重入函数
- 【译】Lesson 1: 一个三角形和一个方块
- 美女面试官问我Python如何优雅的创建临时文件,我的回答....
- DLL程序组件Microsoft Reporting Services Barcode Custom Report Item
- 【资源】PyTorch版《动手学深度学习》开源了,最美DL书遇上最赞DL框架
- 计算机网络实验视频word,(完整word版)《计算机网络与通信》实验.docx
- c语言ctype函数指针,ctype调用DLL中的函数问题已经成功解决
- Webmagic爬虫框架
- 智能计算机与应用是核心期刊吗,人工智能的核心期刊都有哪些
- android gallary demo
- ubuntu设置共享文件夹
- Delphi基本数据类型(内置数据类型)
- c语言表达式优先级()
- D3D12渲染技术之光栅化与管道
- 软件体系结构-04-CSBS体系结构
- oracle中prad函数_R中用GDCRNATools包下载TCGA数据
- 2021-07-20accelerated c++第2章