按月彙整:六月 2012

收藏的幾種版本《聖經》

  上次和友人小田聊天,伊說案頭擺有《聖經》,我就想起來自己也收藏有幾本。

  今天拿出來拍照一下,發現袖珍版的那本找不見了,可能是忘在深圳了吧,這次回去找一下。原來我也是把這本擺在床頭的。

  剩下的三種,左上黑色封面的是研用版,剛打開看了一下,特別適合比對閱讀。有詳細的交叉引用信息和各種索引表。右上是南美用的《牧靈聖經》,書攤淘來的。以上兩本都是簡體中文。下方是King James版本的英文聖經,買來收藏。

  比較可惜的是,我一直想收藏一本繁體字的和一本文言文的實體書。網上能找到翻拍的照片,不過最好還是收藏實體書。哪位朋友知道如何獲取,請告訴我哈。尤其是文言文的,也就是文理和合版,據說裡面的詞句很優美。剛在維基文庫上找到了鏈接,我可以先讀讀,嘿嘿。《聖經 (文理和合)》

廣告

《少年同盟》動畫第1季

  今天看完了友人推薦的動畫《少年同盟》第1季共13集。很輕鬆、閒適的情節,看了心裡特別舒坦,回想起很多曾經的趣事。人生就是要保持少年那樣的心態,一直純真。千萬別被各種精巧包裝過的僞成功學迷住了心竅。

  另外就是……嗯,不能從了壞人。

  片尾曲是澤井美空唱的「なきむし。」(膽小鬼)非常好聽,強烈推薦!

書生的小日子,就是這樣!

  昨晚去書店拿書友卡買了幾本書。《唐前志怪小說輯釋》封面很美,可惜裏面紙張稍硬。《世說新語校箋》一套四本,很有收藏價值。《稗家粹編》是明代人輯錄的小說集,值得一讀。《菊與刀》原來買過口袋書,這次看到商務版印刷很清爽,又便宜,於是買來收藏,準備讀二週目。

  看技術類書的時候,突然發現《Head First Python》一書的封面很好。小正太微微笑。對,以後我也要這個打扮,偶爾賣個小萌什麼的也不錯。

  繞到旁邊那個書架,發現很久前看到過這個系列的書還在呢,封面非常有喜感。看了之後心情突然變得很安定、很溫柔。

  嘛,書生的小日子,就是這樣!

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

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

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

代碼質量隨想錄(五)注得多不如注得巧

  寫代碼也流行注水了麼?不是不是,我說的是註釋。其實註釋這個東西,歷史久遠。我們可以寬泛一點兒說,《春秋》就是要配上左傳的註解,才能興發其“微言大義”嘛!註釋有很多種,如果按照註釋者與原文作者是不是同一個人來分,可以劃分成自注和他注。在程序員這個行當內,一般來說,還是自注多一些,自己寫代碼,自己加注。有的時候進行代碼審查或者復用遺留代碼時,纔可能會有必要對他人寫的代碼加註。

  從代碼質量的角度看,註釋寫得應不應該,寫得好不好,應該從它是否有助於加深代碼讀者及代碼使用者對程序的理解這一標準來判斷。按照《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_tw/

SKIP BEAT 下一站的天后與巨星

  感謝友人向我提供了這麼好看的一部動漫。說實在的,和很多動漫呀、電影呀、遊戲呀一樣,我至今都沒弄清“Skip Beat”和“下一站巨星”、“華麗的挑戰”這兩個譯名有什麼關係。

  其實,看到這個劇情,我想到了很多很多。簡單說來,如果在一個正常的環境中,可以通過個人的努力和真本事來完成自我的發展與提升,那麼整個大環境就會顯得和諧、健康。從小時候看衛視中文臺(現在鳳凰衛視的前身)開始,到後來的很多日本動漫,一直以來,我都覺得很多社會作品在向觀者傳達正面價值觀,希望這種潛移默化的影響能夠徹底改變一代人的思維、價值觀和待人處事的邏輯,讓整個環境慢慢地向正常渠道迴歸。能想到這麼多,我覺得看看動漫什麼的,意義大着呢。

  就是這主人公的名字起的陡,叫最上恭子,不是讓我記成最上義光,就是記成深田恭子,哈哈!

  看到最後一集時突然有點小感動,覺得什麼時候才能活得像劇中人物那樣簡單而純粹,另外學會了一個新詞,叫“戀愛白癡”,我妹說這是個舊詞,她不知道的是,我從舊詞中聽出了新意。

  好了,不管怎樣,頑張吧!

代码质量随想录(四)排版,不只是为了漂亮

  写了前三篇之后,发现比我预想的效果要好。关注代码质量的朋友还蛮多的,而且很多意见和建议也很有益,指出了我文章中的一些问题。

  我这种家庭妇男型的自由职业者来说,在平常写代码的时候可以多停下来,思考一些代码质量与软件设计方面的问题。当然啦,由于具体的工作环境、关注领域、自身阅历等原因,小翔在文中提出的许多观点难免书生之见,请诸位多包涵。

  针对排版这个问题,不同的公司、团队都有自己的一套方案,有时网络上也能下载到很多大型的权威代码规范,其中亦含有程序排版相关的规则,我也经常与众友人一起讨论某个项目所用的排版约定。在看到《The Art of Readable Code》一书中有关此话题的章节时,我的感觉是,很难总结出一套万用的“宇宙排版律”来,多半要根据自身环境、团队和项目的特点来拟定,所给出的建议仅仅是参考,并不能强行照搬。

1.功能相似的代码,版式也应相似

public class PerformanceTester {
  public static final TcpConnectionSimulator wifi = 
    new TcpConnectionSimulator(
      500   /* 以Kbps计量的吞吐量 */, 
      80    /* 以毫秒计的网络延迟 */, 
      200   /* 包抖动 */, 
      1     /* 丢包百分比 */);

  public static final TcpConnectionSimulator t3Fiber =
    new TcpConnectionSimulator(
      45000 /* 以Kbps计量的吞吐量 */, 
      10    /* 以毫秒计的网络延迟 */, 
      0     /* 包抖动 */, 
      0     /* 丢包百分比 */);

  public static final TcpConnectionSimulator cell = 
    new TcpConnectionSimulator(
      100   /* 以Kbps计量的吞吐量 */, 
      400   /* 以毫秒计的网络延迟 */, 
      250   /* 包抖动 */, 
      5     /* 丢包百分比 */);
}

  上面这个例子是ARC书中所举的,我认为很恰当。该类的三个静态字段功能类似,都指代某种环境下的网络模拟器,所以排版也应该相似。每行都只写一个实参,而且后面用行内注释的形式解释该实参的意思。在垂直方向上的对齐做得也很好:字段申明前面空2格,实例化语句前面空4格,各实参前面空6格(以上数字非实指,仅是举例而已)。这样要修改某个参数,很快就能定位到它,而且以后如果增加类似的字段,如badWIFI,也可以比照这个格式来,便于维护。

  由以上范例还可引出一个问题,那就是在实例化或方法调用中,经常会遇到一些孤立的魔法数字(magic number),如果确有必要为它起名,那么不妨执行一个小的重构,以常量来代替它。反之,如果是大段的硬数值,则不一定非要为每个值都起一个名字,例如:

TcpConnectionSimulator wifi = 
  new TcpConnectionSimulator(
    WIFI_KBPS_THROUGHPUT,
    WIFI_LATENCY,
    WIFI_JITTER,
    WIFI_PACKET_LOSS_PERCENT);

  这样反而显得累赘。不妨像上例那样采用行内注释的办法来解释这些硬值的意思。

  承上,ARC的作者又推导出一条建议,就是将相似的方法调用参数注释提取到一处,例如:

public class PerformanceTester {
  // TcpConnectionSimulator(throughput, latency, jitter, packet_loss)
  //                        [Kbps]      [ms]     [ms]    [percent]

  public static final TcpConnectionSimulator wifi = 
    new TcpConnectionSimulator(500,   80,  200, 1);

  public static final TcpConnectionSimulator t3Fiber =
    new TcpConnectionSimulator(45000, 10,  0,   0);

  public static final TcpConnectionSimulator cell = 
    new TcpConnectionSimulator(100,   400, 250, 5);
}

  说实在的,以前在工作中还没太重视这个问题,一来是觉得我在写Javadoc时一贯非常完备,出现这种情况时只需靠鼠标悬停就可知道某个方法或构造器的具体信息了;二来嘛,也是想着如果使用大量数值的调用代码多到无法管控,我可能会祭出配置文件这个大旗来,将它们全部纳入配置中了事。所以关于以上例子中谈到的这些问题,我觉得还是根据大家的具体实践来理解为好,不要机械地寻求一致。

2.将大量相似的嵌套式、接续式调用逻辑整合到共用方法之中,即利于排版,又可凸显重要数据

  在测试用例等代码中,经常会出现类似下面这种状况:

// 某受测类中:
// 将类似"Doug Adams"这样的不完整称呼进行补全,扩展为"Mr. Douglas Adams"的形式。
// 如若不能(查不到数据或无法补完),则于error参数中填充错误信息并返回空串。
// 此方法会置空错误信息接收参数。
public String expandToFullName(DatabaseConnection   conn, 
                               String               partialName,
                               ErrorMessageReceiver error){...}

// 某测试方法中:
  DatabaseConnection connection=...;
  ErrorMessageReceiver error=...;
  assertEquals(expandToFullName(connection,"Doug Adams" ,error) , 
               "Mr. Douglas Adams");
  assertEquals(error.getMessage() , "");
  assertEquals(expandToFullName(connection,"Jake Brown" ,error) , 
               "Mr. Jacob Brown III");
  assertEquals(error.getMessage() , "");
  assertEquals(expandToFullName(connection,"No Such Guy“,error) , 
               "");
  assertEquals(error.getMessage() , "no match found");
  assertEquals(expandToFullName(connection,"John“,       error) , 
               "");
  assertEquals(error.getMessage() , "more than one result");

  这符合上面所说的“量大”、“形似”、“嵌套”等特征,而且诸如输入字串、预期结果、预期错误消息等重要的数据,被埋没于connection、error、getMessage()等技术细节之中。所以可以借由美化版式之机进行重构:

  checkPartialToFull("Doug Adams" , "Mr. Douglas Adams" , "");
  checkPartialToFull("Jake Brown" , "Mr. Jake Brown III", "");
  checkPartialToFull("No Such Guy", ""                  , "no match found");
  checkPartialToFull("John"       , ""                  , "more than one result");

private void checkPartialToFull(String partialName, 
                                String expectedFullName,
                                String expectedErrorMessage) {
  // connection已被提取为测试固件类的成员变量
  ErrorMessageReceiver error=...;
  String actualFullName = expandToFullName(connection, partialName, error);
  assertEquals(expectedErrorMessage, error.getMessage());
  assertEquals(expectedFullName    , actualFullName);
}

  如此一来一举三得:既消除了重复代码,同时美化了版式,凸显了输入字串、预期结果、预期错误消息等重要数据,顺带着还方便了后续测试数据的维护。这种藉由版式整理带来的重构,我看可以有!

3.明智地使用纵向对齐来减少拼写错误、厘清大量同组数据。

  我觉得这一条和第1条有重复,其实也属于类似功能的代码应具类似版式之意,不过既然ARC作者将它单列,我想可能是为了强调纵向对齐的好处吧。

// 将POST参数中的属性分别提取至各个局部变量中
ServletRequest request=...;

String details  = request.getParameter("details");
String location = request.getParameter("location");
String hone     = request.getParameter("phon");
String email    = request.getParameter("email");
String url      = request.getParameter("url");

  经由纵向对齐,很容易看出第三个局部变量这行的错误:将变量名“phone”误写为“hone”,参数名的“phone”则错成了”phon“。

  另外,在进行结构体数据、数组成员等这种同组数据排列时,也可以充分利用版式来厘清每个元素的意义。ARC的作者就大赞wget这个命令行工具在指定参数结构体时,代码排列地很工整。

// 非原文,小翔以Java形式改写
Object[][] commands = {
  //参数名         , 默认值           , 类型
  { "timeout",      null,             TIMEOUT    },
  { "timestamping", defOpt.timestamp, BOOLEAN    },
  { "tries",        defOpt.tryCount,  NUMBER     },
  { "useproxy",     defOpt.useProxy,  BOOLEAN    },
  { "useragent",    null,             USER_AGENT }
};

  这一条建议如果与第1条合并起来说,那就是:任务相似的代码块应该具有相似的轮廓(ARC的作者叫它silhouette),如行数、缩进、纵向对齐等。

4.使用适当空行与注释,将代码按功能分段

  有时候经常在考虑代码与散文或诗的联系,如果从隐喻(metaphor)的观点来看,的确有相似性:都是信息的载体,都可以用一定的段落来整合文意。要说区别嘛,前者服务于软件需求,后者服务于社会关系。前者为了向更低阶的执行机制去接合,所以更加注重语法格式。我可不是第一个进行这种思维比拟的人,记得台湾的技术畅销书作者侯捷先生(侯俊杰)就曾写过一本《左手程序右手诗》的书。

class FrontendServer {
public:
  FrontendServer();
  void ViewProfile(HttpRequest* request);
  void OpenDatabase(string location, string user);
  void SaveProfile(HttpRequest* request);
  string ExtractQueryParam(HttpRequest* request, string param);
  void ReplyOK(HttpRequest* request, string html);
  void FindFriends(HttpRequest* request);
  void ReplyNotFound(HttpRequest* request, string error);
  void CloseDatabase(string location);
  ~FrontendServer();
};

  上面的代码挺蜗居的,如果加上适当的空行与说明,就显得清晰多了。

class FrontendServer {
public:
  FrontendServer();
  ~FrontendServer();

  // 与用户配置相关的处理函数
  void ViewProfile(HttpRequest* request);
  void SaveProfile(HttpRequest* request);
  void FindFriends(HttpRequest* request);

  // 回覆及应答工具函数
  string ExtractQueryParam(HttpRequest* request, string param);
  void   ReplyOK(HttpRequest* request, string html);
  void   ReplyNotFound(HttpRequest* request, string error);

  // 数据库操作工具函数
  void OpenDatabase(string location, string user);
  void CloseDatabase(string location);
};

  上述类将声明区按照构建子/析构子、社交功能函数、工具函数这个标准划分为的三大思维区段,工具函数区又按题材划分为消息操作与数据库操作两小段。这样一来,以后再要维护这份声明代码就会很清爽了。同理,如果声明一个集合类的接口,也应该按照“增、删、改、查”等概念来将API划分为若干小组,以便帮助代码阅读者理顺思路。

  就算是在流水式的业务代码中,也可以用段落来衬托出逻辑的“起、承、转、合”。

// 导入用户电子邮件账户中联系人,同本产品中已有的联系人相比对。
// 然后展示正在使用本产品但未与用户建立朋友关系的联络人列表。
public ListDataModel suggestNewFriends(User user,Password emailPassword){
  SocialCircle friends = user.friends();
  Emails friendEmails = friend.dumpAllEmails();
  Contacts contacts = importContacts(user.email, emailPassword);
  Emails contactEmails = contacts.extractAllEmails();
  Emails productUserEmails = UserDataCenter.selectEmails(contactEmails);
  Emails suggestedFriends = productUserEmails.subtract(friendEmails);
  ListDataModel displayModel = new ListDataModel(user,friends,suggestedFriends);
  return displayModel;
}

  上面的代码给人的压迫感很强列,没有思维喘息的机会。不如把注释拆解,按其逻辑将代码分成小段,为每一段冠以简短标题。

public ListDataModel suggestNewFriends(User user,Password emailPassword){
  // 取得当前用户全部朋友的邮件地址
  SocialCircle friends = user.friends();
  Emails friendEmails  = friend.dumpAllEmails();

  // 引入当前用户电子邮件账户中的联系人
  Contacts contacts    = importContacts(user.email, emailPassword);
  Emails contactEmails = contacts.extractAllEmails();

  // 找出正在使用本产品但尚未与本用户建立朋友关系的联系人
  Emails productUserEmails = UserDataCenter.selectEmails(contactEmails);
  Emails suggestedFriends  = productUserEmails.subtract(friendEmails);

  // 返回待显示列表的数据模型
  ListDataModel displayModel = new ListDataModel(user,friends,suggestedFriends);
  return displayModel;
}

  欣赏一下上面这段代码吧,每小段以一句概括性的注释引领,然后是两句实现代码,排列得非常整齐,代码的阅读者根据此的版式,很容易就能抓住代码的思维走向:“2(获取朋友列表)-2(获取联系人邮箱)-2(找出潜在友人)-2(返回数据模型)”。怎么样,是不是有点儿“起、承、转、合”的意思了?照这样写下去,可以山寨一个小的Google+了吧?我这个SocialCircle类比谷加的还厉害,它那个只能是同级别的平行关系,我这个还能像组合体模式那样,互相嵌套呢!(大误)

  由上例可见,适当地进行代码分段并通过注释来充当代码段的概括语,有助于梳理代码阅读者的思路,也有助于代码修改、维护和后续查错。比如想做一个“向未使用本社交网站的电邮联络人发送邀请”的功能,扫一眼上述这段清晰排版的代码,大家立刻就能看出,只需要写好测试用例,复制一份suggestNewFriends的代码,把selectEmails改成excludeEmails,就能找到这些潜在的被邀请人了。给新的方法起个名字,叫inviteContacts,删去多余的程序,然后通过重构提取一下共用代码,再确保测试无误,就可以收工了。思路顺了,编码的过程自然也就更加流畅了。

  好了,小小总结一下吧。其实代码排版这种略带个人化的东西,不仅仅是让代码看起来更漂亮,其根本目的还是着眼于代码的可读性,要有助于代码的理解、维护、纠错。具体到执行层面,除了可以参考上述4条建议外,还要注意两方面的问题。

  第一个问题,ARC的作者也提到了,那就是很多朋友对代码排版有排斥心理,不愿意认真排版。有一部分原因是怕浪费时间,还有就是担心代码管理系统会将排版后的代码与排版之前的代码判定为两份截然不同的程序,在版本比对时导致满屏的diff,非常难看。其实,在现有的成熟IDE之中(抑或各位Geek们惯用的文本编辑器之中)已经有非常完备的功能来支援代码版式的调整了。比如Eclipse、Netbeans等开发环境,都可以把版式定义文件导出为xml等数据格式,到了陌生的环境时,只需导入即可。而且代码排版一旦确定,就可以一次性地更改所有项目源码的版式然后提交,这样就可以避免在版本比对时显示过多的修改提示了。

  第二个问题就是应该在必要的范围内保持代码排版的一致性。虽然我刚也说了,代码排版没有绝对的真理,不过,它却应该有一个相对的底线。在公司与公司之间、团队与团队之间,的确没有必要强行要求一致的版式。例如我们不宜妄自菲薄,说I记或G社的代码排得如何如何漂亮,同时也不能过分地自高自大,说自己团队的版式是天下最美观、最养眼的。但是,如果具体到某个项目,尤其是中小型项目里面,那么就要想方设法达成一致的版式规范了,否则将会给代码的阅读、理解与维护造成不必要的障碍。为此,项目组的成员应该富有的妥协精神,在坚持个人风格这个问题上稍作让步,以求达成大家对代码版式的共识。比如,小翔在个人项目或由我带队的项目中,通常使用以下版式:

public class MyArrayList extends MyAbstractList implements MyCollection{
  // 静态部分在前:
  // 静态内部类型区。同区成员按存取级别排序,高者在前。
  /** 
   * 列表容量参数。
   */
  public static class CapacityOptions{
    /** 初始容量。 */
    private final int initialElementCount;
    /** 扩容时新增的容量与扩容前容量之比。 */
    private final int expandRatio;
  }
  ...

  // 静态初始化块与静态字段区。
  private static final Map commonCapacityOptions=...;
  ...

  static{
    commonCapacityOptions.put("normal" , new CapacityOptions(12,1));
    ...;
  }
  ...

  // 静态方法区。
  /**
   * 从既有数组中构建列表。
   * @param elements 用以构建的数组,不能为null。
   * @return 构建好的列表
   */
  public static List create(Object[] elements){
    ...;
  }
  ...

  // 动态部分在后:
  // 动态内部类型区。
  public class MyIterator{
    public Object next(){
      ...;
    }
  }

  // 动态初始化块与实例成员变量区。
  {
    ...;
  }

  private int count;
  ...

  // 构造器区。
  public MyList(){
    ...;
  }
  ...

  // 实例方法区。
  // 先写本类方法。
  void expand(){
    ...;
  }

  // 其次,从直接超类开始,层层追溯至Object,将各层级上的覆写方法列出。
  @Override
  public boolean add(Object e){
    ...;
  }

  // 然后按由进至远的顺序,实现接口中的方法。
  // 同等层级的接口,按其出现在implements、extends子句中的先后顺序,依次实现其方法。
  @Override
  public void clear(){
    ...;
  }

  // 最后覆写Object类中的方法。
  @Override
  public String toString(){
    ...;
  }

  //准析构方法区。
  protected void finalize() throws Throwable{
    ...;
  }
}

  上述这个“3+5式分段法”(静态部分:静态内部类型、静态初始化块与字段、静态方法;动态部分:动态内部类性、动态初始化块与实例成员变量、构造器、实例方法、准析构方法),小翔在六年多的工作中一直用着,我觉得它对于代码阅读来说,还算满流畅的,在此也分享给大家。不过,如果现有项目大多数成员要求将左花括号放于新行之首,并要求动态部分出现在静态部分的前边,那么我就会在这个项目中按照大家喜欢的格式来办(并不是随意放弃自己认为合理的版式,而是在某个项目的具体语境下为了求得共识而妥协),同理,类似新行符是n、r还是nr,空白符是空格还是制表符之类问题,我觉得只要大家认为合适,就没有必要过分争执,定出一个项目内部易于统一管理的规范就好。

  再多说一句吧,有同学可能会问,既然Eclipse等IDE中已经可以通过类结构导览视图来显示类代码中的各种成员,那么为何还要如此在乎代码版式呢?因为不管具体代码怎么排列,视图中都可以调整显示顺序呀。对于这个问题,我想有时我们不仅仅要通过导览视图来看宏观结构,还需要进行微观的具体代码审读与维护,所以微观代码的排列终究还是为了易读。当然啦,排列方法可以商量,比如你可以说不必按照“静态、动态”那样分,而是按照“内部类、变量、方法”这样来分。

  从下午开始写,断断续续到了深夜,微笑地浏览了一遍之后,顿时觉得这一篇文章讲的话题有点儿文艺了。嗯,接下来,将和大家聊聊代码注释。

爱飞翔

2012年6月6日至7日

本文使用Creative Commons BY-NC-ND 3.0协议(创作共用 自由转载-保持署名-非商业使用-禁止衍生)发布。

原文网址:http://agilemobidev.net/eastarlee/code-quality/think_in_code_quality_4_layout_zh_cn/

代碼質量隨想錄(四)排版,不只是爲了漂亮

  寫了前三篇之後,發現比我預想的效果要好。關注代碼質量的朋友還蠻多的,而且很多意見和建議也很有益,指出了我文章中的一些問題。

  我這種家庭婦男型的自由職業者來說,在平常寫代碼的時候可以多停下來,思考一些代碼質量與軟件設計方面的問題。當然啦,由於具體的工作環境、關注領域、自身閱歷等原因,小翔在文中提出的許多觀點難免書生之見,請諸位多包涵。

  針對排版這個問題,不同的公司、團隊都有自己的一套方案,有時網絡上也能下載到很多大型的權威代碼規範,其中亦含有程序排版相關的規則,我也經常與衆友人一起討論某個項目所用的排版約定。在看到《The Art of Readable Code》一書中有關此話題的章節時,我的感覺是,很難總結出一套萬用的“宇宙排版律”來,多半要根據自身環境、團隊和項目的特點來擬定,所給出的建議僅僅是參考,並不能強行照搬。

1.功能相似的代碼,版式也應相似

public class PerformanceTester {
  public static final TcpConnectionSimulator wifi = 
    new TcpConnectionSimulator(
      500   /* 以Kbps計量的吞吐量 */, 
      80    /* 以毫秒計的網絡延遲 */, 
      200   /* 包抖動 */, 
      1     /* 丟包百分比 */);

  public static final TcpConnectionSimulator t3Fiber =
    new TcpConnectionSimulator(
      45000 /* 以Kbps計量的吞吐量 */, 
      10    /* 以毫秒計的網絡延遲 */, 
      0     /* 包抖動 */, 
      0     /* 丟包百分比 */);

  public static final TcpConnectionSimulator cell = 
    new TcpConnectionSimulator(
      100   /* 以Kbps計量的吞吐量 */, 
      400   /* 以毫秒計的網絡延遲 */, 
      250   /* 包抖動 */, 
      5     /* 丟包百分比 */);
}

  上面這個例子是ARC書中所舉的,我認爲很恰當。該類的三個靜態字段功能類似,都指代某種環境下的網絡模擬器,所以排版也應該相似。每行都只寫一個實參,而且後面用行內註釋的形式解釋該實參的意思。在垂直方向上的對齊做得也很好:字段申明前面空2格,實例化語句前面空4格,各實參前面空6格(以上數字非實指,僅是舉例而已)。這樣要修改某個參數,很快就能定位到它,而且以後如果增加類似的字段,如badWIFI,也可以比照這個格式來,便於維護。

  由以上範例還可引出一個問題,那就是在實例化或方法調用中,經常會遇到一些孤立的魔法數字(magic number),如果確有必要爲它起名,那麼不妨執行一個小的重構,以常量來代替它。反之,如果是大段的硬数值,則不一定非要爲每個值都起一個名字,例如:

TcpConnectionSimulator wifi = 
  new TcpConnectionSimulator(
    WIFI_KBPS_THROUGHPUT,
    WIFI_LATENCY,
    WIFI_JITTER,
    WIFI_PACKET_LOSS_PERCENT);

  這樣反而顯得累贅。不妨像上例那樣採用行內註釋的辦法來解釋這些硬值的意思。

  承上,ARC的作者又推導出一條建議,就是將相似的方法調用參數註釋提取到一處,例如:

public class PerformanceTester {
  // TcpConnectionSimulator(throughput, latency, jitter, packet_loss)
  //                        [Kbps]      [ms]     [ms]    [percent]

  public static final TcpConnectionSimulator wifi = 
    new TcpConnectionSimulator(500,   80,  200, 1);

  public static final TcpConnectionSimulator t3Fiber =
    new TcpConnectionSimulator(45000, 10,  0,   0);

  public static final TcpConnectionSimulator cell = 
    new TcpConnectionSimulator(100,   400, 250, 5);
}

  說實在的,以前在工作中還沒太重視這個問題,一來是覺得我在寫Javadoc時一貫非常完備,出現這種情況時只需靠鼠標懸停就可知道某個方法或構造器的具體信息了;二來嘛,也是想着如果使用大量數值的調用代碼多到無法管控,我可能會祭出配置文件這個大旗來,將它們全部納入配置中了事。所以關於以上例子中談到的這些問題,我覺得還是根據大家的具體實踐來理解爲好,不要機械地尋求一致。

2.將大量相似的嵌套式、接續式調用邏輯整合到共用方法之中,即利於排版,又可凸顯重要數據

  在測試用例等代碼中,經常會出現類似下面這種狀況:

// 某受測類中:
// 將類似"Doug Adams"這樣的不完整稱呼進行補全,擴展爲"Mr. Douglas Adams"的形式。
// 如若不能(查不到數據或無法補完),則於error參數中填充錯誤信息並返回空串。
// 此方法會置空錯誤信息接收參數。
public String expandToFullName(DatabaseConnection   conn, 
                               String               partialName,
                               ErrorMessageReceiver error){...}

// 某測試方法中:
  DatabaseConnection connection=...;
  ErrorMessageReceiver error=...;
  assertEquals(expandToFullName(connection,"Doug Adams" ,error) , 
               "Mr. Douglas Adams");
  assertEquals(error.getMessage() , "");
  assertEquals(expandToFullName(connection,"Jake Brown" ,error) , 
               "Mr. Jacob Brown III");
  assertEquals(error.getMessage() , "");
  assertEquals(expandToFullName(connection,"No Such Guy“,error) , 
               "");
  assertEquals(error.getMessage() , "no match found");
  assertEquals(expandToFullName(connection,"John“,       error) , 
               "");
  assertEquals(error.getMessage() , "more than one result");

  這符合上面所說的“量大”、“形似”、“嵌套”等特徵,而且諸如輸入字串、預期結果、預期錯誤消息等重要的數據,被埋沒於connection、error、getMessage()等技術細節之中。所以可以借由美化版式之機進行重構:

  checkPartialToFull("Doug Adams" , "Mr. Douglas Adams" , "");
  checkPartialToFull("Jake Brown" , "Mr. Jake Brown III", "");
  checkPartialToFull("No Such Guy", ""                  , "no match found");
  checkPartialToFull("John"       , ""                  , "more than one result");

private void checkPartialToFull(String partialName, 
                                String expectedFullName,
                                String expectedErrorMessage) {
  // connection已被提取爲測試固件類的成員變量
  ErrorMessageReceiver error=...;
  String actualFullName = expandToFullName(connection, partialName, error);
  assertEquals(expectedErrorMessage, error.getMessage());
  assertEquals(expectedFullName    , actualFullName);
}

  如此一來一舉三得:既消除了重複代碼,同時美化了版式,凸顯了輸入字串、預期結果、預期錯誤消息等重要數據,順帶着還方便了後續測試數據的維護。這種藉由版式整理帶來的重構,我看可以有!

3.明智地使用縱向對齊來減少拼寫錯誤、釐清大量同組數據。

  我覺得這一條和第1條有重複,其實也屬於類似功能的代碼應具類似版式之意,不過既然ARC作者將它單列,我想可能是爲了強調縱向對齊的好處吧。

// 將POST參數中的屬性分別提取至各個局部變量中
ServletRequest request=...;

String details  = request.getParameter("details");
String location = request.getParameter("location");
String hone     = request.getParameter("phon");
String email    = request.getParameter("email");
String url      = request.getParameter("url");

  經由縱向對齊,很容易看出第三個局部變量這行的錯誤:將變量名“phone”誤寫爲“hone”,參數名的“phone”則錯成了”phon“。

  另外,在進行結構體數據、數組成員等這種同組數據排列時,也可以充分利用版式來釐清每個元素的意義。ARC的作者就大讚wget這個命令行工具在指定參數結構體時,代碼排列地很工整。

// 非原文,小翔以Java形式改寫
Object[][] commands = {
  //參數名         , 默認值           , 類型
  { "timeout",      null,             TIMEOUT    },
  { "timestamping", defOpt.timestamp, BOOLEAN    },
  { "tries",        defOpt.tryCount,  NUMBER     },
  { "useproxy",     defOpt.useProxy,  BOOLEAN    },
  { "useragent",    null,             USER_AGENT }
};

  這一條建議如果與第1條合並起來說,那就是:任務相似的代碼塊應該具有相似的輪廓(ARC的作者叫它silhouette),如行數、縮進、縱向對齊等。

4.使用適當空行與註釋,將代碼按功能分段

  有時候經常在考慮代碼與散文或詩的聯繫,如果從隱喻(metaphor)的觀點來看,的確有相似性:都是信息的載體,都可以用一定的段落來整合文意。要說區別嘛,前者服務於軟件需求,後者服務於社會關係。前者爲了向更低階的執行機制去接合,所以更加注重語法格式。我可不是第一個進行這種思維比擬的人,記得臺灣的技術暢銷書作者侯捷先生(侯俊傑)就曾寫過一本《左手程序右手詩》的書。

class FrontendServer {
public:
  FrontendServer();
  void ViewProfile(HttpRequest* request);
  void OpenDatabase(string location, string user);
  void SaveProfile(HttpRequest* request);
  string ExtractQueryParam(HttpRequest* request, string param);
  void ReplyOK(HttpRequest* request, string html);
  void FindFriends(HttpRequest* request);
  void ReplyNotFound(HttpRequest* request, string error);
  void CloseDatabase(string location);
  ~FrontendServer();
};

  上面的代碼挺蝸居的,如果加上適當的空行與說明,就顯得清晰多了。

class FrontendServer {
public:
  FrontendServer();
  ~FrontendServer();

  // 與用戶配置相關的處理函數
  void ViewProfile(HttpRequest* request);
  void SaveProfile(HttpRequest* request);
  void FindFriends(HttpRequest* request);

  // 回覆及應答工具函數
  string ExtractQueryParam(HttpRequest* request, string param);
  void   ReplyOK(HttpRequest* request, string html);
  void   ReplyNotFound(HttpRequest* request, string error);

  // 數據庫操作工具函數
  void OpenDatabase(string location, string user);
  void CloseDatabase(string location);
};

  上述類將聲明區按照構建子/析構子、社交功能函數、工具函數這個標準劃分爲的三大思維區段,工具函數區又按題材劃分爲消息操作與數據庫操作兩小段。這樣一來,以後再要維護這份聲明代碼就會很清爽了。同理,如果聲明一個集合類的接口,也應該按照“增、刪、改、查”等概念來將API劃分爲若干小組,以便幫助代碼閱讀者理順思路。

  就算是在流水式的業務代碼中,也可以用段落來襯托出邏輯的“起、承、轉、合”。

// 導入用戶電子郵件賬戶中聯繫人,同本產品中已有的聯繫人相比對。
// 然後展示正在使用本產品但未與用戶建立朋友關係的聯絡人列表。
public ListDataModel suggestNewFriends(User user,Password emailPassword){
  SocialCircle friends = user.friends();
  Emails friendEmails = friend.dumpAllEmails();
  Contacts contacts = importContacts(user.email, emailPassword);
  Emails contactEmails = contacts.extractAllEmails();
  Emails productUserEmails = UserDataCenter.selectEmails(contactEmails);
  Emails suggestedFriends = productUserEmails.subtract(friendEmails);
  ListDataModel displayModel = new ListDataModel(user,friends,suggestedFriends);
  return displayModel;
}

  上面的代碼給人的壓迫感很強列,沒有思維喘息的機會。不如把註釋拆解,按其邏輯將代碼分成小段,爲每一段冠以簡短標題。

public ListDataModel suggestNewFriends(User user,Password emailPassword){
  // 取得當前用戶全部朋友的郵件地址
  SocialCircle friends = user.friends();
  Emails friendEmails  = friend.dumpAllEmails();

  // 引入當前用戶電子郵件賬戶中的聯繫人
  Contacts contacts    = importContacts(user.email, emailPassword);
  Emails contactEmails = contacts.extractAllEmails();

  // 找出正在使用本產品但尚未與本用戶建立朋友關係的聯繫人
  Emails productUserEmails = UserDataCenter.selectEmails(contactEmails);
  Emails suggestedFriends  = productUserEmails.subtract(friendEmails);

  // 返回待顯示列表的數據模型
  ListDataModel displayModel = new ListDataModel(user,friends,suggestedFriends);
  return displayModel;
}

  欣賞一下上面這段代碼吧,每小段以一句概括性的註釋引領,然後是兩句實現代碼,排列得非常整齊,代碼的閱讀者根據此的版式,很容易就能抓住代碼的思維走向:“2(獲取朋友列表)-2(獲取聯繫人郵箱)-2(找出潛在友人)-2(返回數據模型)”。怎麼樣,是不是有點兒“起、承、轉、合”的意思了?照這樣寫下去,可以山寨一個小的Google+了吧?我這個SocialCircle類比谷加的還厲害,它那個只能是同級別的平行關係,我這個還能像組合體模式那樣,互相嵌套呢!(大誤)

  由上例可見,適當地進行代碼分段並通過註釋來充當代碼段的概括語,有助於梳理代碼閱讀者的思路,也有助於代碼修改、維護和後續查錯。比如想做一個“向未使用本社交網站的電郵聯絡人發送邀請”的功能,掃一眼上述這段清晰排版的代碼,大家立刻就能看出,只需要寫好測試用例,複製一份suggestNewFriends的代碼,把selectEmails改成excludeEmails,就能找到這些潛在的被邀請人了。給新的方法起個名字,叫inviteContacts,刪去多餘的程序,然後通過重構提取一下共用代碼,再確保測試無誤,就可以收工了。思路順了,編碼的過程自然也就更加流暢了。

  好了,小小總結一下吧。其實代碼排版這種略帶個人化的東西,不僅僅是讓代碼看起來更漂亮,其根本目的還是着眼於代碼的可讀性,要有助於代碼的理解、維護、糾錯。具體到執行層面,除了可以參考上述4條建議外,還要注意兩方面的問題。

  第一個問題,ARC的作者也提到了,那就是很多朋友對代碼排版有排斥心理,不願意認真排版。有一部分原因是怕浪費時間,還有就是擔心代碼管理系統會將排版後的代碼與排版之前的代碼判定爲兩份截然不同的程序,在版本比對時導致滿屏的diff,非常難看。其實,在現有的成熟IDE之中(抑或各位Geek們慣用的文本編輯器之中)已經有非常完備的功能來支援代碼版式的調整了。比如Eclipse、Netbeans等開發環境,都可以把版式定義文件導出爲xml等數據格式,到了陌生的環境時,只需導入即可。而且代碼排版一旦確定,就可以一次性地更改所有項目源碼的版式然後提交,這樣就可以避免在版本比對時顯示過多的修改提示了。

  第二個問題就是應該在必要的範圍內保持代碼排版的一致性。雖然我剛也說了,代碼排版沒有絕對的真理,不過,它卻應該有一個相對的底線。在公司與公司之間、團隊與團隊之間,的確沒有必要強行要求一致的版式。例如我們不宜妄自菲薄,說I記或G社的代碼排得如何如何漂亮,同時也不能過分地自高自大,說自己團隊的版式是天下最美觀、最養眼的。但是,如果具體到某個項目,尤其是中小型項目裏面,那麼就要想方設法達成一致的版式規範了,否則將會給代碼的閱讀、理解與維護造成不必要的障礙。爲此,項目組的成員應該富有的妥協精神,在堅持個人風格這個問題上稍作讓步,以求達成大家對代碼版式的共識。比如,小翔在個人項目或由我帶隊的項目中,通常使用以下版式:

public class MyArrayList extends MyAbstractList implements MyCollection{
  // 靜態部分在前:
  // 靜態內部類型區。同區成員按存取級別排序,高者在前。
  /** 
   * 列表容量參數。
   */
  public static class CapacityOptions{
    /** 初始容量。 */
    private final int initialElementCount;
    /** 擴容時新增的容量與擴容前容量之比。 */
    private final int expandRatio;
  }
  ...

  // 靜態初始化塊與靜態字段區。
  private static final Map commonCapacityOptions=...;
  ...

  static{
    commonCapacityOptions.put("normal" , new CapacityOptions(12,1));
    ...;
  }
  ...

  // 靜態方法區。
  /**
   * 從既有數組中構建列表。
   * @param elements 用以構建的數組,不能爲null。
   * @return 構建好的列表
   */
  public static List create(Object[] elements){
    ...;
  }
  ...

  // 動態部分在後:
  // 動態內部類型區。
  public class MyIterator{
    public Object next(){
      ...;
    }
  }

  // 動態初始化塊與實例成員變量區。
  {
    ...;
  }

  private int count;
  ...

  // 構造器區。
  public MyList(){
    ...;
  }
  ...

  // 實例方法區。
  // 先寫本類方法。
  void expand(){
    ...;
  }

  // 其次,從直接超類開始,層層追溯至Object,將各層級上的覆寫方法列出。
  @Override
  public boolean add(Object e){
    ...;
  }

  // 然後按由進至遠的順序,實現接口中的方法。
  // 同等層級的接口,按其出現在implements、extends子句中的先後順序,依次實現其方法。
  @Override
  public void clear(){
    ...;
  }

  // 最後覆寫Object類中的方法。
  @Override
  public String toString(){
    ...;
  }

  //準析構方法區。
  protected void finalize() throws Throwable{
    ...;
  }
}

  上述這個“3+5式分段法”(靜態部分:靜態內部類型、靜態初始化塊與字段、靜態方法;動態部分:動態內部類性、動態初始化塊與實例成員變量、構造器、實例方法、準析構方法),小翔在六年多的工作中一直用着,我覺得它對於代碼閱讀來說,還算滿流暢的,在此也分享給大家。不過,如果現有項目大多數成員要求將左花括號放於新行之首,並要求動態部分出現在靜態部分的前邊,那麼我就會在這個項目中按照大家喜歡的格式來辦(並不是隨意放棄自己認爲合理的版式,而是在某個項目的具體語境下爲了求得共識而妥協),同理,類似新行符是n、r還是nr,空白符是空格還是製表符之類問題,我覺得只要大家認爲合適,就沒有必要過分爭執,定出一個項目內部易於統一管理的規範就好。

  再多說一句吧,有同學可能會問,既然Eclipse等IDE中已經可以通過類結構導覽視圖來顯示類代碼中的各種成員,那麼爲何還要如此在乎代碼版式呢?因爲不管具體代碼怎麼排列,視圖中都可以調整顯示順序呀。對於這個問題,我想有時我們不僅僅要通過導覽視圖來看宏觀結構,還需要進行微觀的具體代碼審讀與維護,所以微觀代碼的排列終究還是爲了易讀。當然啦,排列方法可以商量,比如你可以說不必按照“靜態、動態”那樣分,而是按照“內部類、變量、方法”這樣來分。

  從下午開始寫,斷斷續續到了深夜,微笑地瀏覽了一遍之後,頓時覺得這一篇文章講的話題有點兒文藝了。嗯,接下來,將和大家聊聊代碼註釋。

愛飛翔

2012年6月6日至7日

本文使用Creative Commons BY-NC-ND 3.0協議(創作共用 自由轉載-保持署名-非商業使用-禁止衍生)發佈。

原文網址:http://agilemobidev.net/eastarlee/code-quality/think_in_code_quality_4_layout_zh_tw/

代码质量随想录(三)名字好,误会少

  写完前两篇之后,有点小倦怠,因为一方面要整理读书笔记,一方面还要结合自己的思路加以重新表述,颇费周张。不过前两日看到有小朋友过来赞我的文章,说对实际代码有所帮助,还是满欣慰的,本系列随想录的目的之一,就是要营造一个努力改良代码质量的思维环境。

  要想让标识符的名称更易理解,就应该多考虑考虑此名称是否会被误读。

  先看两个很容易误读的例子。

Object[] results = Database.getAllObjects().filter("year <= 2011");

  到底是要选出year小于等于2011的那部分对象,还是选出year大于2011的那部分呢?filter到底是排除(exclude),还是遴选(select)呢?我自己在日常编码中也爱用filter,多半由于习惯。现在自己思量,是得改正了。
再看

public String clip(String text,int length); //裁掉文本的末尾

  clip方法有歧义:到底是去掉文本后的length个字符,还是从头开始截取最大length个字符呢?比如clip("Java",2);到底是"va"还是"Ja"?如果是前者应该叫removeLast,如果是后者则应叫truncate。而且length也有毛病,到底以什么为单位?字节?字符?还是词语?如果是字符,应该是truncate(String text,int maxCharCount)。

  归纳起来说,以下几种情形应格外注重选取避免误解的名称。

1.以常量表示包含端点的上限或下限时,应分别用MAX与MIN做前缀。

  例如CART_TOO_BIT_LIMIT=10到底是说购物车中最多放10件商品还是11件?抑或是9件?改为MAX_ITEMS_IN_CART = 10则很清楚:最多10件。

2.在表达包含左右端点的区间时,应用first及last。

public void printIntegerInRange(int start,int stop){...}
...
printIntegerInRange(2,6);

  到底打印[2,3,4,5]还是[2,3,4,5,6]?如果是后者,应该是printIntegerInRange(int first,int last)。

3.在表达包含左端点而不含右端点的区间时,应当使用begin与end。

  英文中没有哪个常用词的字面意思能表示“区段内最后一个值的紧下一个值”这个意思,所以使用end只是约定成俗而已,并不精确。例如public void printEventsInRange(String begin,String end),可以使用如下参数来调用:printEventsInRange(“OCT 16 00:00″, “OCT 17 00:00″),这样的话,一般人都能理解右端点("OCT 17 00:00″)不含在范围内。如果用public void printEventsInRange(String first,String last),则是printEventsInRange(“OCT 16 00:00″, “OCT 16 23:59″)。

4.使用判断词来消除boolean变量的歧义。

  为boolean变量起名时一定注意是否有歧义:

bool readPassword = true;

  到底是当前需要读取密码,还是密码已经被读取过了?前者应是needPassword,后者应是userIsAuthenticated。

  使用is、has、can、should等词汇来让boolean变量与方法的意图更加清晰,尤其是在那些不需要申明方法或函数返回类型的编程语言中。例如:spaceLeft()到底是返回剩下的空间大小,还是返回是否有剩余空间?根据是简单获取还是复杂计算,前者应命名为getLeftSpaceInPixel()或calcLeftSpacePx(),分别指示轻量级(get)和重量级(calculate或compute)的两种获取办法;而后者则应是hasSpaceLeft(),只说有没有剩余空间,不谈具体的量。

5.避免在boolean命名中使用否定形式。

  例如:

bool disableSSL = false;

  不如下面这种命名方式清晰:

bool useSSL = true;

6.不要同约定成俗的命名方式相违逆。

  例如getXXX()格式的方法一般有两个隐含意义:1.该操作为轻量级。2.该操作返回所在类的某个成员。

  如下统计算数平均数的方法名称即为不宜:

public class SampleCollector {
  public void add(double sample) { ... }

  public double getMean() {
    ... // 叠加所有采样值并返回“总和/样本数”
  }
...
}

  getMean()并非轻量级操作,且不返回本类某个成员。不如叫它computeMean()更好,compute会引人联想该操作是不是稍为复杂一些,耗时一些。如果非要用getMean做名称的话,那么mean应被纳入缓存机制。例如:

private boolean meanCached;//计算完样本后置为true,样本改动时置为false
...

public double getMean() {
  if(!meanCached){
    ... // 叠加所有采样值
    mean=sampleSum/sampleCount;
    meanCached=true;
  }
  return mean;
}

void ShrinkList(list& list, int max_size) {
  while (list.size() > max_size) {
    FreeNode(list.back());
    list.pop_back();
  }
}

  size()操作的时间复杂度为O(1)应是大多数人的共识,可是恰恰list的size()是时间复杂度为O(n)的操作,这导致整个函数的复杂度变为O(n2)。按理说size()应该叫为countSize() 或countElements(),以体现其重量级运算的特质来,不过,为了和其余容器类相符合,还是叫成size了。所幸新版C++规范强制要求size操作的时间复杂度为O(1)了(ARC书的作者这么说的,我未查证。大家帮忙在C++11规范中查证此事。原有规范只是“建议”它应具有常数时间复杂度,并未强制)。

  小翔以为,如果某个抽象接口定义了一个貌似轻量级的简单操作,如Collection的size(),则子类对象在实现时应该尽量降低时间复杂度。实在不能时甚至可以考虑抛出异常或对客户提出警告。根本的解决办法还是学习C++规范那样,给出一个建议的时间复杂度来。

7.在多个候选名称中取舍时应该仔细质询其可能带来的歧义。

  例如有两份相似的服务器配置参数文件:

config_id: 100
description: "increase font size to 14pt"
traffic_fraction: 5%
...

config_id: 101
description: "increase font size to 13pt"
[其余参数与前一份相同]

  我们现在想通过某个机制复用整套参数,例如这样:

config_id: 101
想要复用的配置文件id: 100
[其余参数与前一份相同]

  那么,这个“想要复用的配置文件id”,应该怎么起名呢?备选关键词有:template、reuse、copy和inherit。

  template很模糊:“template: 100”到底是说自己是一份名叫“100”模板,还是说使用一个名叫“100”的模板作为其基础参数?况且模板这个概念太过抽象,给人感觉需要以具体内容填充它。

  “reuse: 100”到底是说这份参数最多可以使用100次,还是说复用名为“100”的那份配置文件中的参数?

  “copy: 100”是第100份拷贝吗?还是说拷贝自编号为“100”的那套配置?后者不如叫copy_config_from更好。

  “inherit: 100”,inherit这个词,大多数程序员很熟悉,且与日常生活的“财产继承”概念可相比拟,所以引起的误解相对较少。可以扩充为inherit_config_from来更精确地阐明这个意思。

  综上,copy_config_from或inherit_config_from应为最终中选名称。

  总之,好的标识符名称可以尽量消除代码阅读者的误解,提高代码可读性与可维护性,亦能促进业务交流。所以应当仔细考究,尽量选取免于误会的名称,尤其是遇到“filter、length和limit”这些模棱两可的词语时。此外,区间与上下限含不含端点、boolean类型的标识符会不会引起误解、方法名称所隐含的意义是否符合常识,这些问题也应该在起名时反覆考量。

  用了两篇文章才讲完给标识符起名的事情,可见其的确关乎代码质量的提升。下一篇我们谈谈代码的排版问题。

爱飞翔
2012年6月4日

本文使用Creative Commons BY-NC-ND 3.0协议(创作共用 自由转载-保持署名-非商业使用-禁止衍生)发布。
原文网址:http://agilemobidev.net/eastarlee/code-quality/think_in_code_quality_3_good_name_zh_cn/