前言

只有代码能告诉你它做的事,它是唯一准确的。尽管有时候我们也需要注释,但是还是要尽量减少它们。

1
2
3
4
5
6
7
8
9
MockRequest request;  
private final String HTTP_DATE_REGEXP =  
	"[SMTWF][a-z]{2}\\,\\s[0-9]{2}\\s[JSFMASOND][a-z]{2}\\s" +  
	"[0-9]{4}\\s[0-9]{2}\\:[0-9]{2}\\:[0-9]{2}\\sGMT";  
private Response response;  
private FitNessContext context;  
private FileResponder responder;  
private Locale saveLocale;  
// Example: "Tue, 02 Apr 2003 22:18:49 GMT"

上面这段代码说明了什么问题呢?仔细看最后一行注释,它本应该是放在 HTTP_DATE_REGEXP 附近的,可能是后续开发者在变更代码时没有注意到这行注释,从而在二者之间插入了其他代码,这就让注释具有了误导性。

4.1 注释不能美化糟糕的代码

看到糟糕代码的时候,首先应想到如何把代码弄干净,而不是通过注释来美化它。

带有少量注释的整洁而有表现力的代码,要比带有大量注释的零碎而复杂的代码好的多。

4.2 用代码来阐述

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

上面的代码显然不如下面的代码简单明了:

1
if (employee.isEligibleForFullBenefits())

很多时候,只需要创建一个描述了与注释所言同一事物的函数即可。

4.3 好注释

有些注释是必须的。但是,最好还是尽量不写注释。

4.3.1 法律信息

示例如下:

1
2
// 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.

版权及著作权声明有理由在每个源文件开头注释处放置,好在这些注释可以被 IDE 自动折叠起来。

不应该完整列出全部合同,最好是放一个外部文档的链接。

4.3.2 提供信息的注释

注释有时可以提供有用的基本信息。例如,下面通过注释来解释某个抽象方法的返回值

1
2
3
4
/**  
 * @return an instance of the Responder being tested.  
 */
protected abstract Responder responderInstance();

不过,还是应该尽量通过函数名称来传达信息。例如,上面的代码可以改为:

1
protected abstract Responder responderBeingTested();

下面这个注释可能更加有意义些:

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

不过,最好还是把这段代码移动到某个转换日期和时间格式的类中。

4.3.3 对意图的解释

有时,注释不仅提供了有关实现的有用信息,而且还提供了某个决定后面的意图。

下面的例子表示:在对比两个对象时,作者决定将他的类放置在比其他东西更高的位置。

1
2
3
4
5
6
7
8
9
public int compareTo(Object o) {
	if (o instanceof WikiPagePath) {
		WikiPagePath p = (WikiPagePath) o;
		String compressedName = StringUtil.join(names, "");
		String compressedArgumentName = StringUtil.join(p.names, "");
		return compressedName.compareTo(compressedArgumentName);
	}
	return 1; // we are greater because we are the right type.
}

下面的例子更好。你也许不同意程序员给这个问题提供的解决方案,但至少你知道他想干什么。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
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.3.4 阐释

注释可以把某些晦涩难懂的参数或返回值的意义翻译为某种可读形式。

尽管我们应该尽量让参数或者返回值足够清楚,但是如果参数或返回值是你无法修改的代码(三方库代码),我们就需要注释来阐释。

1
2
3
4
5
6
7
public void testCompareTo() throws Exception {
	WikiPagePath a = PathParser.parse("PageA");
	WikiPagePath b = PathParser.parse("PageB");

	assertTrue(a.compareTo(a) == 0); // a == a
	assertTrue(a.compareTo(b) != 0); // a != b
}

但是,使用注释来阐释晦涩的代码,这回让我们很难确保注释的正确性,因此,永远需要小心谨慎,或者考虑是否有更好的办法。

4.3.5 警示

注释可以用来警示其他程序员可能会出现某种后果。

下面的示例解释了为什么要关闭某个特定的测试用例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
/**
 * 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)
}

我们现在可能会加上 @Ignore 注解用来关闭测试用例,例如 @Ignore("Takes too long to run")

在 Junit4 之前,惯用的做法是在方法名前加上下划线。

下面的代码可以有效阻止程序员以效率为由使用静态初始化器。

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

4.3.6 TODO 注释

我们有时需要 //TODO 形式在源代码中放置要做的工作列表。

下面的例子解释了为什么该函数的实现部分无所作为,将来应该是什么样子:

1
2
3
4
5
//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;
}

TODO 是一种程序员认为应该做,但是由于某些原因目前还没做的工作。

不能应为 TODO 的存在而在系统中保留糟糕的代码,现代 IDE 能迅速定位 TODO 注解的位置,因此我们需要定期查看,并删除不再需要的 TODO 注释。

4.3.7 放大

注释可以用来放大某种看来不合理之物的重要性。

1
2
3
4
5
6
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()));

4.3.8 公共 API 中的 Javadoc

如果你在编写公共 API,就该为它编写良好的 Javadoc。

但是要注意,Javadoc 也可能误导、不适用或者提供错误信息。

4.4 坏注释

4.4.1 喃喃自语

如果决定要写注释,那就要花必要的时间确保写出最好的注释。

例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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
	}
}

catch 代码块中的注释含义并不清楚:

  • 如果出现 IOException 就表示没有属性文件,此时载入默认配置。
  • 那么应该由谁来装载呢?
  • 是在 loadProperties.load 之前还是之后装载?
  • loadProperties.load 捕获异常、装载默认设置、再向上传递异常以忽略它?
  • loadProperties.load 在尝试载入文件前就装载所有默认设置?
  • 作者想要告诉自己,将来要回过头写装载默认设置的代码?

4.4.2 多余的注释

下面这段代码头部的注释纯属多余。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
/**
 * 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");
		}
	}
}

这段注释不能提供比代码更多的信息,甚至误导读者接受不精确的信息。