Clean Code第四章:注释 --阅读与讨论

尼克徐 发布于 2015年10月31日 | 更新于 2015年11月06日
无人欣赏。

alt text

图片来自:Chapter 4 Comments

注释,是一种“必须的恶”。

若编程语言有足够表达力,或我们长于用这些语言来表达意图,就不那么需要注释--也许根本不需要。

注释的恰当用法是弥补我们在用代码表达意图时的失败。

注意,注释毕竟是一种失败,是我们无法找到不用注释就能表达的办法,所以才用注释。

如果发现自己需要写注释,想想看是否有办法用代码来表达。

每次用代码表达,都应该夸奖一下自己。每次写注释,都应该做个鬼脸,感受一下自己在表达能力上的失败。

因为程序员不能坚持维护注释,所以注释可能会撒谎。存在的时间越久,离所描述的代码越远。

例如以下例子,因为中间插入了代码,注释和所要描述的代码距离很远:

MockRequest request;
private final String HTTP_DATE_REGEXP =
"[SMTWF][a-z]{2},s[0-9]{2}s[JFMASOND][a-z]{2}s"+
"[0-9]{4}s[0-9]{2}:[0-9]{2}:[0-9]{2}sGMT"; private Response response;
private FitNesseContext context;
private FileResponder responder;
private Locale saveLocale;
// Example: "Tue, 02 Apr 2003 22:18:49 GMT"

一,注释不能为了美化糟糕代码

写注释常见的动机是糟糕的代码的存在。

我们编写了一个模块,发现它令人困扰乱七八糟,所以就写了注释。

而此时需要做的,反而是要把代码清理干净!

带有少量注释的整洁代码,要比有大量注释的零碎而复杂的代码像样的多。

二,用代码来阐述

程序员总是倾向于认为代码不足以解释行为,但其实只要多想想,就能找到办法来用代码阐述行为。

有时候只是改变函数名就可以。

例如如下例子:

// Check to see if the employee is eligible for full benefits 
if ((employee.flags & HOURLY_FLAG) && (employee.age > 65))

改成:

if (employee.isEligibleForFullBenefits())

是不是一下子就清晰多了,不需要注释了。

三,好注释

有些注释是必须的和需要写的。

当然,真正好的注释是你想出办法不去写注释。

1,法律信息

版权和著作权声明是必须和有理由在每个源文件开头注释处放置的内容。

// Copyright (C) 2003,2004,2005 by Object Mentor, Inc. All rights reserved.
// Released under the terms of the GNU General Public License version 2 or later.

这类注释不应是合同或法典。只要有可能,就指向一份标准许可或其他外部文档,而不要把所有条款放在注释中。

2,提供信息的注释

注释有时是为了提供基本信息:

// format matched kk:mm:ss EEE, MMM dd, yyyy 
Pattern timeMatcher = Pattern.compile("d*:d*:d* w*, w* d*, d*");

3,对意图的注释

有时候,注释不仅提供了信息,还提供了某个决定后的意图:

public void testConcurrentAddWidgets() throws Exception { 
    WidgetBuilder widgetBuilder = new WidgetBuilder(new Class[]{BoldWidget.class});

    String text = "'''bold text'''"; 
    ParentWidget parent = new BoldWidget(new MockWidgetRoot(), "'''bold text'''"); 
    AtomicBoolean failFlag = new AtomicBoolean(); 
    failFlag.set(false);
    //This is our best attempt to get a race condition 
    //by creating large number of threads.
    for (int i = 0; i < 25000; i++) {
        WidgetBuilderThread widgetBuilderThread =
            new WidgetBuilderThread(widgetBuilder, text, parent, failFlag);
        Thread thread = new Thread(widgetBuilderThread);
        thread.start(); 
    }
    assertEquals(false, failFlag.get());

4,阐释

有时候,注释把某些晦涩难明的参数或返回值翻译为某种可读形式。

通常,更好的方法是尽量让参数或返回值自身就足够清楚,但如果参数或返回值是某个标准库的一部分,或者你不能修改的代码,帮助阐释其意义就会有用。

public void testCompareTo() throws Exception {
    WikiPagePath a = PathParser.parse("PageA"); 
    WikiPagePath ab = PathParser.parse("PageA.PageB"); 
    WikiPagePath b = PathParser.parse("PageB"); 
    WikiPagePath aa = PathParser.parse("PageA.PageA"); 
    WikiPagePath bb = PathParser.parse("PageB.PageB"); 
    WikiPagePath ba = PathParser.parse("PageB.PageA");
    assertTrue(a.compareTo(a) == 0); // a == a
    assertTrue(a.compareTo(b) != 0); // a != b
    assertTrue(ab.compareTo(ab) == 0); // ab == ab
    assertTrue(a.compareTo(b) == -1); // a < b
    assertTrue(aa.compareTo(ab) == -1); // aa < ab
    assertTrue(ba.compareTo(bb) == -1); // ba < bb
    assertTrue(b.compareTo(a) == 1); // b > a
    assertTrue(ab.compareTo(aa) == 1); // ab > aa
    assertTrue(bb.compareTo(ba) == 1);// bb > ba
}

5,警示

有时候,用于警告其他程序员会出现某种后果的注释也是有用的。

// Don't run unless you
// have some time to kill.
public void _testWithReallyBigFile() {
    writeLinesToFile(10000000);
    response.setBody(testFile);
    response.readyToSend(this);
    String responseString = output.toString(); 
    assertSubString("Content-Length: 1000000000", responseString); 
    assertTrue(bytesSent > 1000000000);
}

又例如:

public static SimpleDateFormat makeStandardHttpDateFormat() {
    //SimpleDateFormat is not thread safe,
    //so we need to create each instance independently.
    SimpleDateFormat df = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z");
    df.setTimeZone(TimeZone.getTimeZone("GMT"));
    return df;
}

6,TODO注释

有时,有理由用//TODO形式在源代码中放置要做的工作列表。

//TODO-MdM these are not needed
// We expect this to go away when we do the checkout model protected VersionInfo makeVersion() throws Exception {
    return null; 
}

7,放大

注释可以用来放大某种看来不合理的代码的重要性:

String listItemContent = match.group(3).trim();
// the trim is real important. It removes the starting 
// spaces that could cause the item to be recognized
// as another list.
new ListItemWidget(this, listItemContent, this.level + 1); 
return buildList(text.substring(match.end()));

8,公共API中的Javdoc

良好描述的公共API是非常令人满意的。如果在编写公共API,请为它编写良好的Javadoc。

四,坏注释

1, 喃喃自语

有时候,作者太着急或者没花心思,结果注释成了喃喃自语不知所云。

public void loadProperties() {
    try {
        String propertiesPath = propertiesLocation + "/" + PROPERTIES_FILE; 
        FileInputStream propertiesStream = new FileInputStream(propertiesPath); 
        loadedProperties.load(propertiesStream);
    }
    catch(IOException e) {
        // No properties files means all defaults are loaded
    } 
}

2,多余的注释

有的注释纯属多余,读注释花的时间可能比读代码花的时间长。

// Utility method that returns when this.closed is true. Throws an exception 
// if the timeout is reached.
public synchronized void waitForClose(final long timeoutMillis)
    throws Exception {
        if(!closed) {
            wait(timeoutMillis); 
            if(!closed)
                throw new Exception("MockResponseSender could not be closed"); 
    }
}

3, 误导性注释

有时候,注释有误导性,例如前面出现过的这段:

// Utility method that returns when this.closed is true. Throws an exception 
// if the timeout is reached.
public synchronized void waitForClose(final long timeoutMillis)
    throws Exception {
        if(!closed) {
            wait(timeoutMillis); 
            if(!closed)
                throw new Exception("MockResponseSender could not be closed"); 
    }
}

从注释里看,在this.closed时函数会立即返回,但实际上,函数会有一段休眠时间然后再判断。

如果程序员只看了注释,则很容易就期望this.closed为真时立即返回,但代码中就会有没有预期到的延迟。

4,循规式注释

所谓每个函数都要有Javadoc或者每个变量都要有注释的规矩是很愚蠢的,徒然让代码变得散乱。

/** *
* @param title The title of the CD
* @param author The author of the CD
* @param tracks The number of tracks on the CD
* @param durationInMinutes The duration of the CD in minutes */
public void addCD(String title, String author,
    int tracks, int durationInMinutes) {
    CD cd = new CD(); 
    cd.title = title; cd.author = author; 
    cd.tracks = tracks; 
    cd.duration = duration; cdList.add(cd);
}

5,日志式注释

有人会在每次编辑代码时,在模块开始处添加一条注释。

因为我们现在有了源码控制系统(svn,git等)了,这类冗长的记录只会让模块变得凌乱,应当全部删除。

* Changes (from 11-Oct-2001)
* --------------------------
* 11-Oct-2001 : Re-organised the class and moved it to new package com.jrefinery.date (DG);
* 05-Nov-2001 : Added a getDescription() method, and eliminated NotableDate class (DG);
* 12-Nov-2001 : IBD requires setDescription() method, now that NotableDate class is gone (DG); 
*  Changed getPreviousDayOfWeek(), getFollowingDayOfWeek() and getNearestDayOfWeek() to correct bugs (DG);
* 05-Dec-2001 :Fixed bug in SpreadsheetDate class (DG);
* 29-May-2002 :Moved the month constants into a separate interface (MonthConstants) (DG);
* 27-Aug-2002 :Fixed bug in addMonths() method, thanks to N???levka Petr (DG);
* 03-Oct-2002 :Fixed errors reported by Checkstyle (DG);
* 13-Mar-2003 :Implemented Serializable (DG);
* 29-May-2003 :Fixed bug in addMonths method (DG);
* 04-Sep-2003 :Implemented Comparable. Updated the isInRange javadocs (DG);
* 05-Jan-2005 :Fixed bug in addYears() method (1096282) (DG);

6,废话注释

有时候会看到纯然时废话的注释

/**
* Default constructor. */
protected AnnualDateRule() { }
/** The day of the month. */
private int dayOfMonth;
/**
* Returns the day of the month. *
* @return the day of the month. */
public int getDayOfMonth() {
     return dayOfMonth;
}

7,Javadoc废话

Javadoc也可能是废话。下列Javadoc来自某知名开源库,其注释的目的是什么?

/** The name. */ 
private String name;
/** The version. */ 
private String version;
/** The licenceName. */ 
private String licenceName;
/** The version. */ 
private String info;

8,能用函数或变量时就别用注释

例如以下代码和注释:

// does the module from the global list  depend on the
// subsystem we are part of?
if (smodule.getDependSubsystems().contains(subSysMod.getSubSystem()))

可以改为:

ArrayList moduleDependees = smodule.getDependSubsystems(); 
String ourSubSystem = subSysMod.getSubSystem();
if (moduleDependees.contains(ourSubSystem))

9,位置标记

有时,程序员喜欢在源代码中标记某个特别位置,但基本上都无用。

// Actions //////////////////////////////////

10,括号后面的注释

有时程序员会在深度嵌套结构的函数中,在括号后面放置特殊的注释。

但如果你发现想标记右括号,往往要做的是缩短函数(见函数一章)。

public class wc {
public static void main(String[] args) {
        BufferedReader in = new BufferedReader(new InputStreamReader(System.in)); String line;
        int lineCount = 0;
        int charCount = 0;
        int wordCount = 0; 
        try {
          while ((line = in.readLine()) != null) { 
                lineCount++;
                charCount += line.length();
                String words[] = line.split("W"); 
                wordCount += words.length;
            } //while
            System.out.println("wordCount = " + wordCount); 
            System.out.println("lineCount = " + lineCount); 
            System.out.println("charCount = " + charCount);
        } // try
        catch (IOException e) { 
            System.err.println("Error:" + e.getMessage());
        } //catch
    } //main 
}

11,归属与署名

例如:

/* Added by Rick */

这是源代码控制系统做的事情。

如果你写注释在那里,随着一年又一年的过去,注释就会越来越不准确,越来越与原作者没关系。

12,注释掉代码

把代码注释掉,并遗留在那里是很讨厌的行为。而我们有了源代码控制系统,一定不要留下这些遗留物。

InputStreamResponse response = new InputStreamResponse();
response.setBody(formatter.getResultStream(), formatter.getByteCount());
// InputStream resultsStream = formatter.getResultStream();
// StreamReader reader = new StreamReader(resultsStream);
// response.setContent(reader.read(formatter.getByteCount()));

13,注释中有HTML标签

注释中的HTML标签会让注释很难读。

如果需要呈现网页,那也应该由工具生成标签,而不是写在注释里。

14,非本地信息

如果一定要写注释,请确保它描述了离它最近的代码,别给出其他地方代码的信息。

例如以下代码给出了一个默认端口,而该端口不是由此段代码所能控制。

如果设默认端口的代码被修改,此注释就变成了误导。

/**
* Port on which fitnesse would run. Defaults to 8082. *
* @param fitnessePort
*/
public void setFitnessePort(int fitnessePort) {
    this.fitnessePort = fitnessePort; 
}

15,信息过多

别再注释中添加有趣的历史性话题和无关的细节描述,这些只会增加阅读代码的负担。

以下注释除了RFC文档编号外,其他细节对读者完全没必要。

/*
RFC 2045 - Multipurpose Internet Mail Extensions (MIME)
Part One: Format of Internet Message Bodies
section 6.8. Base64 Content-Transfer-Encoding
The encoding process represents 24-bit groups of input bits as output strings of 4 encoded characters. Proceeding from left to right, a 24-bit input group is formed by concatenating 3 8-bit input groups. These 24 bits are then treated as 4 concatenated 6-bit groups, each of which is translated into a single digit in the base64 alphabet. When encoding a bit stream via the base64 encoding, the bit stream must be presumed to be ordered with the most-significant-bit first. That is, the first bit in the stream will be the high-order bit in the first 8-bit byte, and the eighth bit will be the low-order bit in the first 8-bit byte, and so on.
*/

16,不明显的联系

注释及其描述的代码之间的联系应该显而易见,以下注释就有些问题:

/*
 * start with an array that is big enough to hold all the pixels * (plus filter bytes), 
 * and an extra 200 bytes for header info */
this.pngBytes = new byte[((this.width + 1) * this.height * 3) + 200];

以上注释和代码里,过滤的字节指的是什么?与+1还是*3有关系?为什么用200?

这个注释就是失败的,因注释本身还需要更多的注释。

17,函数头的注释

短函数不需要太多描述。为只做一件事的短函数取个好名字,往往比写函数头的注释要好。

18,非公共代码中的Javadoc

虽然Javadoc对于公共API很有用,但对于不打算作为公共用途的代码就很没必要了。对此类代码加Javadoc形式的注释几乎等同于写八股文章。

问题:

1,你写注释的风格是什么样的?犯了以上哪些错误?

如何加入Root Cause读书会:请加我的微信brainstudio,我建立了一个群,方便通知各位写读后感及讨论。 

Clean Code所有讨论章节链接

共5条回复
cnsoft 回复于 2015年10月31日

Todo 有使用. 额外还会有call by 就是被谁调用. 会有比较随意性的注释.过了一段时间就忘了维护了. 也没清理掉. 最有趣的一次是被别人copy到另外一个函数里面使用...

相比之下, 功能点一直是我在意的地方. 就是这个功能能不能实现。至于怎么写反而不是最重要的. 至少有时间来整理. 所以会有一种hack code 存在. 这点可能不是所有人都能体会得到, 会以为没有什么不能实现的。实践中, 的确会有填不上的坑, 只能绕过去。对于只用接口的同学当然无所谓, 而做具体模块的同学就会更关心具体实现。

就像git的流程一样. 开完branch 要合并. 要删除掉. 注释也得走一遍这个模式比较科学. 注释和代码一样 没有维护保持最新就会引发连锁反应. 尤其是人多协作的时候。

尼克徐 回复于 2015年10月31日

1楼 @cnsoft

我觉得自从看了之后,对以前的代码开始看不顺眼了,注释方面也一样。

1, 有时候一些临时性的改动,就会加注释,并写了“Written by xxx”,其实没必要,版本控制系统已经帮助做了这个工作。

2, 好多代码都是因比较复杂而写的注释,这些代码应该被重构,而不是被注释。

3,注释不被更新,比不注释还要命,造成很多误会。

4, commit时,写的信息一定要详细,这其实也是注释的一部分。

昨天出了一个问题,和commit代码时的信息模糊有关。

我去年提交的一份代码,某处的commit信息特别不明确,导致不知道为什么那时要改动那个地方。

但现在出了一个bug,似乎把上次的改动去掉就好了,却也不敢动,因实在不知道上次改动是为什么...

而改动后万一出问题了,影响却不会小。

cnsoft 回复于 2015年10月31日

2楼 @尼克徐 不能乱动. 修bug同时引入新bug 死循环啊... 一定要慎重.

多人协作, 一个人光顾自己写的爽. 上午写的, 下午接口就变了. 有没有. 然后QA就报告你的bug来了. 扯远了. 哈哈.

monksa7 回复于 2015年11月01日

2楼 @尼克徐

现在了解重构啊? 没有原因无故删减代码啊?

坑死人啊

nkduqi 回复于 2015年11月06日
  1. 我的注释风格?到读这一章之前并没有想到一个注释需要注意这么多东西,我的风格中有不少问题:(1)喃喃自语是有的,例如定义一些变量时,总是喜欢在后面加个//来说明下,其实只要变量名字取得好,就不需要了;(2)无用注释,这跟我的编程习惯有关,我喜欢先用注释把一个函数的步骤写出来,然后再一点点补充代码,但是之后就不想删除那些注释了——这个问题在之前的“函数”章节其实讲到了,说明我的代码的抽象层级不同;我应该把这些分步骤的代码都提取成一个个的小函数,并配上良好的函数名。
  2. todo注释真得非常有用,在多人协作的时候尤其如此。我依赖TODO注释记录两类工作:(1)一些异常处理的代码,在程序整体架构出来之后统一思考异常处理的层次;(2)一些基础的,或者需要统筹规划的代码,在项目收尾时进行统一处理。
  3. 读了这一章,我最赞同的一句是:注释不能翻译代码,要解释无法用代码自明的意图。
  4. 合适的工具做合适的事情
登录 或者 注册