Clean Code第四章:注釋 --閱讀與討論

摘自《Clean Code》Chapter 4

注釋,是一種“必須的惡”。

若編程語言有足夠表達力,或我們長于用這些語言來表達意圖,就不那么需要注釋--也許根本不需要。

注釋的恰當用法是彌補我們在用代碼表達意圖時的失敗。

注意,注釋畢竟是一種失敗,是我們無法找到不用注釋就能表達的辦法,所以才用注釋。

如果發現自己需要寫注釋,想想看是否有辦法用代碼來表達。

每次用代碼表達,都應該夸獎一下自己。每次寫注釋,都應該做個鬼臉,感受一下自己在表達能力上的失敗。

因為程序員不能堅持維護注釋,所以注釋可能會撒謊。存在的時間越久,離所描述的代碼越遠。

例如以下例子,因為中間插入了代碼,注釋和所要描述的代碼距離很遠:

<pre>
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"
</pre>

一,注釋不能為了美化糟糕代碼

寫注釋常見的動機是糟糕的代碼的存在。

我們編寫了一個模塊,發現它令人困擾亂七八糟,所以就寫了注釋。

而此時需要做的,反而是要把代碼清理干凈!

帶有少量注釋的整潔代碼,要比有大量注釋的零碎而復雜的代碼像樣的多。

二,用代碼來闡述

程序員總是傾向于認為代碼不足以解釋行為,但其實只要多想想,就能找到辦法來用代碼闡述行為。

有時候只是改變函數名就可以。

例如如下例子:

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

改成:

<pre>
if (employee.isEligibleForFullBenefits())
</pre>

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

三,好注釋

有些注釋是必須的和需要寫的。

當然,真正好的注釋是你想出辦法不去寫注釋。

1,法律信息

版權和著作權聲明是必須和有理由在每個源文件開頭注釋處放置的內容。

<pre>
// 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.
</pre>

這類注釋不應是合同或法典。只要有可能,就指向一份標準許可或其他外部文檔,而不要把所有條款放在注釋中。

2,提供信息的注釋

注釋有時是為了提供基本信息:

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

3,對意圖的注釋

有時候,注釋不僅提供了信息,還提供了某個決定后的意圖:

<pre>
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());
</pre>

4,闡釋

有時候,注釋把某些晦澀難明的參數或返回值翻譯為某種可讀形式。

通常,更好的方法是盡量讓參數或返回值自身就足夠清楚,但如果參數或返回值是某個標準庫的一部分,或者你不能修改的代碼,幫助闡釋其意義就會有用。

<pre>
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
}
</pre>

5,警示

有時候,用于警告其他程序員會出現某種后果的注釋也是有用的。

<pre>
// 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);
}
</pre>

又例如:

<pre>
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;
}
</pre>

6,TODO注釋

有時,有理由用//TODO形式在源代碼中放置要做的工作列表。
<pre>
//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;
}
</pre>
7,放大

注釋可以用來放大某種看來不合理的代碼的重要性:
<pre>
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()));
</pre>

8,公共API中的Javdoc

良好描述的公共API是非常令人滿意的。如果在編寫公共API,請為它編寫良好的Javadoc。

四,壞注釋

1, 喃喃自語

有時候,作者太著急或者沒花心思,結果注釋成了喃喃自語不知所云。

<pre>
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
}
}
</pre>

2,多余的注釋

有的注釋純屬多余,讀注釋花的時間可能比讀代碼花的時間長。

<pre>
// 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");
}
}
</pre>

3, 誤導性注釋

有時候,注釋有誤導性,例如前面出現過的這段:

<pre>
// 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");
}
}
</pre>

從注釋里看,在this.closed時函數會立即返回,但實際上,函數會有一段休眠時間然后再判斷。

如果程序員只看了注釋,則很容易就期望this.closed為真時立即返回,但代碼中就會有沒有預期到的延遲。

4,循規式注釋

所謂每個函數都要有Javadoc或者每個變量都要有注釋的規矩是很愚蠢的,徒然讓代碼變得散亂。

<pre>
/** *

  • @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);
    }
    </pre>

5,日志式注釋

有人會在每次編輯代碼時,在模塊開始處添加一條注釋。

因為我們現在有了源碼控制系統(svn,git等)了,這類冗長的記錄只會讓模塊變得凌亂,應當全部刪除。

<pre>

  • 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);
    </pre>

6,廢話注釋

有時候會看到純然時廢話的注釋

<pre>
/**

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

7,Javadoc廢話

Javadoc也可能是廢話。下列Javadoc來自某知名開源庫,其注釋的目的是什么?

<pre>
/** The name. /
private String name;
/
* The version. /
private String version;
/
* The licenceName. /
private String licenceName;
/
* The version. */
private String info;
</pre>

8,能用函數或變量時就別用注釋

例如以下代碼和注釋:

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

可以改為:

<pre>
ArrayList moduleDependees = smodule.getDependSubsystems();
String ourSubSystem = subSysMod.getSubSystem();
if (moduleDependees.contains(ourSubSystem))
</pre>

9,位置標記

有時,程序員喜歡在源代碼中標記某個特別位置,但基本上都無用。

<pre>
// Actions //////////////////////////////////
</pre>

10,括號后面的注釋

有時程序員會在深度嵌套結構的函數中,在括號后面放置特殊的注釋。

但如果你發現想標記右括號,往往要做的是縮短函數(見函數一章)。

<pre>
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
}
</pre>

11,歸屬與署名

例如:
<pre>
/* Added by Rick */
</pre>
這是源代碼控制系統做的事情。

如果你寫注釋在那里,隨著一年又一年的過去,注釋就會越來越不準確,越來越與原作者沒關系。

12,注釋掉代碼

把代碼注釋掉,并遺留在那里是很討厭的行為。而我們有了源代碼控制系統,一定不要留下這些遺留物。

<pre>
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()));
</pre>

13,注釋中有HTML標簽

注釋中的HTML標簽會讓注釋很難讀。

如果需要呈現網頁,那也應該由工具生成標簽,而不是寫在注釋里。

14,非本地信息

如果一定要寫注釋,請確保它描述了離它最近的代碼,別給出其他地方代碼的信息。

例如以下代碼給出了一個默認端口,而該端口不是由此段代碼所能控制。

如果設默認端口的代碼被修改,此注釋就變成了誤導。

<pre>
/**

  • Port on which fitnesse would run. Defaults to <b>8082</b>. *
  • @param fitnessePort
    */
    public void setFitnessePort(int fitnessePort) {
    this.fitnessePort = fitnessePort;
    }
    </pre>
    15,信息過多

別再注釋中添加有趣的歷史性話題和無關的細節描述,這些只會增加閱讀代碼的負擔。

以下注釋除了RFC文檔編號外,其他細節對讀者完全沒必要。

<pre>
/*
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.
*/
</pre>
16,不明顯的聯系

注釋及其描述的代碼之間的聯系應該顯而易見,以下注釋就有些問題:

<pre>
/*

  • 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];

</pre>
以上注釋和代碼里,過濾的字節指的是什么?與+1還是*3有關系?為什么用200?

這個注釋就是失敗的,因注釋本身還需要更多的注釋。

17,函數頭的注釋

短函數不需要太多描述。為只做一件事的短函數取個好名字,往往比寫函數頭的注釋要好。

18,非公共代碼中的Javadoc

雖然Javadoc對于公共API很有用,但對于不打算作為公共用途的代碼就很沒必要了。對此類代碼加Javadoc形式的注釋幾乎等同于寫八股文章。

問題:

1,你寫注釋的風格是什么樣的?犯了以上哪些錯誤?

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容