现在的位置: 首页 > 综合 > 正文

代码质量随想录(五):注得多不如注得巧

2012年11月11日 ⁄ 综合 ⁄ 共 4862字 ⁄ 字号 评论关闭

写代码也流行注水了么?不是不是,我说的是注释。其实注释这个东西,历史久远。我们可以宽泛一点儿说,《春秋》就是要配上左传的注解,才能兴发其“微言大义”嘛!注释有很多种,如果按照注释者与原文作者是不是同一个人来分,可以划分成自注和他注。在程序员这个行当内,一般来说,还是自注多一些,自己写代码,自己加注。有的时候进行代码审查或者复用遗留代码时,才可能会有必要对他人写的代码加注。

  从代码质量的角度看,注释写得应不应该,写得好不好,应该从它是否有助于加深代码读者及代码使用者对程序的理解这一标准来判断。按照《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<float> data;
  ...
  void Clear() {
    vector<float>().swap(data);
  }
};

  如果谁突然闯进来看到上面的代码,肯定第一个就要问:为什么不直接调用data.clean()函数呢?与其让读者陷入猜测与不解之中,咱们不如直接用注释把隐晦的细节说明白了:

// 在vector对象上进行强制内存回收,参见“STL容器的swap技巧”(STL swap trick)
vector<float>().swap(data);

抱歉!评论已关闭.